Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ erl_crash.dump
/priv/sentry.map

test_integrations/phoenix_app/db

test_integrations/*/_build
test_integrations/*/deps
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

#### Various improvements

- Allow any version of opentelemetry deps and verify minimum versions internally - this makes it possible to use `sentry` *with tracing disabled* along with older versions of opentelemetry deps ([#931](https://github.com/getsentry/sentry-elixir/pull/931))

## 11.0.2

### Bug fixes
Expand Down
25 changes: 24 additions & 1 deletion lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ defmodule Sentry.Config do
|> normalize_included_environments()
|> normalize_environment()
|> handle_deprecated_before_send()
|> warn_traces_sample_rate_without_dependencies()

{:error, error} ->
raise ArgumentError, """
Expand Down Expand Up @@ -701,7 +702,10 @@ defmodule Sentry.Config do
def integrations, do: fetch!(:integrations)

@spec tracing?() :: boolean()
def tracing?, do: not is_nil(fetch!(:traces_sample_rate)) or not is_nil(get(:traces_sampler))
def tracing? do
(Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() and
not is_nil(fetch!(:traces_sample_rate))) or not is_nil(get(:traces_sampler))
end

@spec put_config(atom(), term()) :: :ok
def put_config(key, value) when is_atom(key) do
Expand Down Expand Up @@ -762,6 +766,25 @@ defmodule Sentry.Config do
end
end

defp warn_traces_sample_rate_without_dependencies(opts) do
traces_sample_rate = Keyword.get(opts, :traces_sample_rate)

if not is_nil(traces_sample_rate) and
not Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
require Logger

Logger.warning("""
Sentry tracing is configured with traces_sample_rate: #{inspect(traces_sample_rate)}, \
but the required OpenTelemetry dependencies are not satisfied. \
Tracing will be disabled. Please ensure you have compatible versions of: \
opentelemetry (>= 1.5.0), opentelemetry_api (>= 1.4.0), \
opentelemetry_exporter (>= 1.0.0), and opentelemetry_semantic_conventions (>= 1.27.0).
""")
end

opts
end

defp normalize_environment(config) do
Keyword.update!(config, :environment_name, &to_string/1)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/sentry/opentelemetry/sampler.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
if Code.ensure_loaded?(:otel_sampler) do
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
defmodule Sentry.OpenTelemetry.Sampler do
@moduledoc false

Expand Down
2 changes: 1 addition & 1 deletion lib/sentry/opentelemetry/span_processor.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
if Code.ensure_loaded?(OpenTelemetry) do
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
defmodule Sentry.OpenTelemetry.SpanProcessor do
@moduledoc false

Expand Down
2 changes: 1 addition & 1 deletion lib/sentry/opentelemetry/span_record.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
if Code.ensure_loaded?(OpenTelemetry) do
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
defmodule Sentry.OpenTelemetry.SpanRecord do
@moduledoc false

Expand Down
218 changes: 110 additions & 108 deletions lib/sentry/opentelemetry/span_storage.ex
Original file line number Diff line number Diff line change
@@ -1,151 +1,153 @@
defmodule Sentry.OpenTelemetry.SpanStorage do
@moduledoc false
use GenServer
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
defmodule Sentry.OpenTelemetry.SpanStorage do
@moduledoc false
use GenServer

defstruct [:cleanup_interval, :table_name]
defstruct [:cleanup_interval, :table_name]

alias Sentry.OpenTelemetry.SpanRecord
alias Sentry.OpenTelemetry.SpanRecord

@cleanup_interval :timer.minutes(5)
@cleanup_interval :timer.minutes(5)

@span_ttl 30 * 60
@span_ttl 30 * 60

@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) when is_list(opts) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) when is_list(opts) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end

@impl true
def init(opts) do
table_name = Keyword.get(opts, :table_name, default_table_name())
cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval)
@impl true
def init(opts) do
table_name = Keyword.get(opts, :table_name, default_table_name())
cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval)

_ = :ets.new(table_name, [:named_table, :public, :ordered_set])
_ = :ets.new(table_name, [:named_table, :public, :ordered_set])

schedule_cleanup(cleanup_interval)
schedule_cleanup(cleanup_interval)

{:ok, %__MODULE__{cleanup_interval: cleanup_interval, table_name: table_name}}
end
{:ok, %__MODULE__{cleanup_interval: cleanup_interval, table_name: table_name}}
end

@impl true
def handle_info(:cleanup_stale_spans, state) do
cleanup_stale_spans(state.table_name)
schedule_cleanup(state.cleanup_interval)
@impl true
def handle_info(:cleanup_stale_spans, state) do
cleanup_stale_spans(state.table_name)
schedule_cleanup(state.cleanup_interval)

{:noreply, state}
end
{:noreply, state}
end

@spec store_span(SpanRecord.t(), keyword()) :: true
def store_span(span_data, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
stored_at = System.system_time(:second)
@spec store_span(SpanRecord.t(), keyword()) :: true
def store_span(span_data, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
stored_at = System.system_time(:second)

if span_data.parent_span_id == nil do
:ets.insert(table_name, {{:root_span, span_data.span_id}, span_data, stored_at})
else
key = {:child_span, span_data.parent_span_id, span_data.span_id}
if span_data.parent_span_id == nil do
:ets.insert(table_name, {{:root_span, span_data.span_id}, span_data, stored_at})
else
key = {:child_span, span_data.parent_span_id, span_data.span_id}

:ets.insert(table_name, {key, span_data, stored_at})
:ets.insert(table_name, {key, span_data, stored_at})
end
end
end

@spec get_root_span(String.t(), keyword()) :: SpanRecord.t() | nil
def get_root_span(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
@spec get_root_span(String.t(), keyword()) :: SpanRecord.t() | nil
def get_root_span(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())

case :ets.lookup(table_name, {:root_span, span_id}) do
[{{:root_span, ^span_id}, span, _stored_at}] -> span
[] -> nil
case :ets.lookup(table_name, {:root_span, span_id}) do
[{{:root_span, ^span_id}, span, _stored_at}] -> span
[] -> nil
end
end
end

@spec get_child_spans(String.t(), keyword()) :: [SpanRecord.t()]
def get_child_spans(parent_span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
@spec get_child_spans(String.t(), keyword()) :: [SpanRecord.t()]
def get_child_spans(parent_span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())

get_all_descendants(parent_span_id, table_name)
end
get_all_descendants(parent_span_id, table_name)
end

defp get_all_descendants(parent_span_id, table_name) do
direct_children =
:ets.match_object(table_name, {{:child_span, parent_span_id, :_}, :_, :_})
|> Enum.map(fn {_key, span_data, _stored_at} -> span_data end)
defp get_all_descendants(parent_span_id, table_name) do
direct_children =
:ets.match_object(table_name, {{:child_span, parent_span_id, :_}, :_, :_})
|> Enum.map(fn {_key, span_data, _stored_at} -> span_data end)

nested_descendants =
Enum.flat_map(direct_children, fn child ->
get_all_descendants(child.span_id, table_name)
end)
nested_descendants =
Enum.flat_map(direct_children, fn child ->
get_all_descendants(child.span_id, table_name)
end)

(direct_children ++ nested_descendants)
|> Enum.sort_by(& &1.start_time)
end
(direct_children ++ nested_descendants)
|> Enum.sort_by(& &1.start_time)
end

@spec update_span(SpanRecord.t(), keyword()) :: :ok
def update_span(%{parent_span_id: parent_span_id} = span_data, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
stored_at = System.system_time(:second)
@spec update_span(SpanRecord.t(), keyword()) :: :ok
def update_span(%{parent_span_id: parent_span_id} = span_data, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
stored_at = System.system_time(:second)

key =
if parent_span_id == nil do
{:root_span, span_data.span_id}
else
{:child_span, parent_span_id, span_data.span_id}
end
key =
if parent_span_id == nil do
{:root_span, span_data.span_id}
else
{:child_span, parent_span_id, span_data.span_id}
end

:ets.update_element(table_name, key, [{2, span_data}, {3, stored_at}])
:ets.update_element(table_name, key, [{2, span_data}, {3, stored_at}])

:ok
end
:ok
end

@spec remove_root_span(String.t(), keyword()) :: :ok
def remove_root_span(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
key = {:root_span, span_id}
@spec remove_root_span(String.t(), keyword()) :: :ok
def remove_root_span(span_id, opts \\ []) do
table_name = Keyword.get(opts, :table_name, default_table_name())
key = {:root_span, span_id}

:ets.select_delete(table_name, [{{key, :_, :_}, [], [true]}])
remove_child_spans(span_id, table_name: table_name)
:ets.select_delete(table_name, [{{key, :_, :_}, [], [true]}])
remove_child_spans(span_id, table_name: table_name)

:ok
end
:ok
end

@spec remove_child_spans(String.t(), keyword()) :: :ok
def remove_child_spans(parent_span_id, opts) do
table_name = Keyword.get(opts, :table_name, default_table_name())
@spec remove_child_spans(String.t(), keyword()) :: :ok
def remove_child_spans(parent_span_id, opts) do
table_name = Keyword.get(opts, :table_name, default_table_name())

:ets.select_delete(table_name, [
{{{:child_span, parent_span_id, :_}, :_, :_}, [], [true]}
])
:ets.select_delete(table_name, [
{{{:child_span, parent_span_id, :_}, :_, :_}, [], [true]}
])

:ok
end
:ok
end

defp schedule_cleanup(interval) do
Process.send_after(self(), :cleanup_stale_spans, interval)
end
defp schedule_cleanup(interval) do
Process.send_after(self(), :cleanup_stale_spans, interval)
end

defp cleanup_stale_spans(table_name) do
now = System.system_time(:second)
cutoff_time = now - @span_ttl
defp cleanup_stale_spans(table_name) do
now = System.system_time(:second)
cutoff_time = now - @span_ttl

root_match_spec = [
{{{:root_span, :"$1"}, :_, :"$2"}, [{:<, :"$2", cutoff_time}], [:"$1"]}
]
root_match_spec = [
{{{:root_span, :"$1"}, :_, :"$2"}, [{:<, :"$2", cutoff_time}], [:"$1"]}
]

expired_root_spans = :ets.select(table_name, root_match_spec)
expired_root_spans = :ets.select(table_name, root_match_spec)

Enum.each(expired_root_spans, fn span_id ->
remove_root_span(span_id, table_name: table_name)
end)
Enum.each(expired_root_spans, fn span_id ->
remove_root_span(span_id, table_name: table_name)
end)

child_match_spec = [
{{{:child_span, :_, :_}, :_, :"$1"}, [{:<, :"$1", cutoff_time}], [true]}
]
child_match_spec = [
{{{:child_span, :_, :_}, :_, :"$1"}, [{:<, :"$1", cutoff_time}], [true]}
]

:ets.select_delete(table_name, child_match_spec)
end
:ets.select_delete(table_name, child_match_spec)
end

defp default_table_name do
Module.concat(__MODULE__, ETSTable)
defp default_table_name do
Module.concat(__MODULE__, ETSTable)
end
end
end
Loading