Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 4 additions & 1 deletion lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,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
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
73 changes: 73 additions & 0 deletions lib/sentry/opentelemetry/version_checker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule Sentry.OpenTelemetry.VersionChecker do
@moduledoc false

@minimum_versions %{
opentelemetry: "1.5.0",
opentelemetry_api: "1.4.0",
opentelemetry_exporter: "1.0.0",
opentelemetry_semantic_conventions: "1.27.0"
}

@spec tracing_compatible?() :: boolean()
def tracing_compatible? do
case check_compatibility() do
{:ok, :compatible} -> true
{:error, _} -> false
end
end

@spec check_compatibility() :: {:ok, :compatible} | {:error, term()}
def check_compatibility do
case check_all_dependencies() do
[] ->
{:ok, :compatible}

errors ->
{:error, {:incompatible_versions, errors}}
end
end

defp check_all_dependencies do
@minimum_versions
|> Enum.flat_map(fn {dep, min_version} ->
case check_dependency_version(dep, min_version) do
:ok -> []
{:error, reason} -> [{dep, reason}]
end
end)
end

defp check_dependency_version(dep, min_version) do
case get_loaded_version(dep) do
{:ok, loaded_version} ->
if version_compatible?(loaded_version, min_version) do
:ok
else
{:error, {:version_too_old, loaded_version, min_version}}
end

{:error, :not_loaded} ->
{:error, :not_loaded}
end
end

defp get_loaded_version(dep) do
apps = Application.loaded_applications()

case List.keyfind(apps, dep, 0) do
{^dep, _description, version} ->
{:ok, to_string(version)}

nil ->
{:error, :not_loaded}
end
end

defp version_compatible?(loaded_version, min_version) do
case Version.compare(loaded_version, min_version) do
:gt -> true
:eq -> true
:lt -> false
end
end
end
Loading
Loading