Skip to content

Commit 13be2c4

Browse files
committed
Relax version reqs on opentelemetry deps
This enables installing sentry in apps with old opentelemetry libs, that are not compatible with tracing, in which case tracing is simply not loaded and cannot be used as a feature. Closes #928
1 parent f83b508 commit 13be2c4

File tree

17 files changed

+634
-121
lines changed

17 files changed

+634
-121
lines changed

lib/sentry/config.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,10 @@ defmodule Sentry.Config do
701701
def integrations, do: fetch!(:integrations)
702702

703703
@spec tracing?() :: boolean()
704-
def tracing?, do: not is_nil(fetch!(:traces_sample_rate)) or not is_nil(get(:traces_sampler))
704+
def tracing? do
705+
(Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() and
706+
not is_nil(fetch!(:traces_sample_rate))) or not is_nil(get(:traces_sampler))
707+
end
705708

706709
@spec put_config(atom(), term()) :: :ok
707710
def put_config(key, value) when is_atom(key) do

lib/sentry/opentelemetry/sampler.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
if Code.ensure_loaded?(:otel_sampler) do
1+
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
22
defmodule Sentry.OpenTelemetry.Sampler do
33
@moduledoc false
44

lib/sentry/opentelemetry/span_processor.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
if Code.ensure_loaded?(OpenTelemetry) do
1+
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
22
defmodule Sentry.OpenTelemetry.SpanProcessor do
33
@moduledoc false
44

lib/sentry/opentelemetry/span_record.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
if Code.ensure_loaded?(OpenTelemetry) do
1+
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
22
defmodule Sentry.OpenTelemetry.SpanRecord do
33
@moduledoc false
44

Lines changed: 110 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,153 @@
1-
defmodule Sentry.OpenTelemetry.SpanStorage do
2-
@moduledoc false
3-
use GenServer
1+
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
2+
defmodule Sentry.OpenTelemetry.SpanStorage do
3+
@moduledoc false
4+
use GenServer
45

5-
defstruct [:cleanup_interval, :table_name]
6+
defstruct [:cleanup_interval, :table_name]
67

7-
alias Sentry.OpenTelemetry.SpanRecord
8+
alias Sentry.OpenTelemetry.SpanRecord
89

9-
@cleanup_interval :timer.minutes(5)
10+
@cleanup_interval :timer.minutes(5)
1011

11-
@span_ttl 30 * 60
12+
@span_ttl 30 * 60
1213

13-
@spec start_link(keyword()) :: GenServer.on_start()
14-
def start_link(opts) when is_list(opts) do
15-
name = Keyword.get(opts, :name, __MODULE__)
16-
GenServer.start_link(__MODULE__, opts, name: name)
17-
end
14+
@spec start_link(keyword()) :: GenServer.on_start()
15+
def start_link(opts) when is_list(opts) do
16+
name = Keyword.get(opts, :name, __MODULE__)
17+
GenServer.start_link(__MODULE__, opts, name: name)
18+
end
1819

19-
@impl true
20-
def init(opts) do
21-
table_name = Keyword.get(opts, :table_name, default_table_name())
22-
cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval)
20+
@impl true
21+
def init(opts) do
22+
table_name = Keyword.get(opts, :table_name, default_table_name())
23+
cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval)
2324

24-
_ = :ets.new(table_name, [:named_table, :public, :ordered_set])
25+
_ = :ets.new(table_name, [:named_table, :public, :ordered_set])
2526

26-
schedule_cleanup(cleanup_interval)
27+
schedule_cleanup(cleanup_interval)
2728

28-
{:ok, %__MODULE__{cleanup_interval: cleanup_interval, table_name: table_name}}
29-
end
29+
{:ok, %__MODULE__{cleanup_interval: cleanup_interval, table_name: table_name}}
30+
end
3031

31-
@impl true
32-
def handle_info(:cleanup_stale_spans, state) do
33-
cleanup_stale_spans(state.table_name)
34-
schedule_cleanup(state.cleanup_interval)
32+
@impl true
33+
def handle_info(:cleanup_stale_spans, state) do
34+
cleanup_stale_spans(state.table_name)
35+
schedule_cleanup(state.cleanup_interval)
3536

36-
{:noreply, state}
37-
end
37+
{:noreply, state}
38+
end
3839

39-
@spec store_span(SpanRecord.t(), keyword()) :: true
40-
def store_span(span_data, opts \\ []) do
41-
table_name = Keyword.get(opts, :table_name, default_table_name())
42-
stored_at = System.system_time(:second)
40+
@spec store_span(SpanRecord.t(), keyword()) :: true
41+
def store_span(span_data, opts \\ []) do
42+
table_name = Keyword.get(opts, :table_name, default_table_name())
43+
stored_at = System.system_time(:second)
4344

44-
if span_data.parent_span_id == nil do
45-
:ets.insert(table_name, {{:root_span, span_data.span_id}, span_data, stored_at})
46-
else
47-
key = {:child_span, span_data.parent_span_id, span_data.span_id}
45+
if span_data.parent_span_id == nil do
46+
:ets.insert(table_name, {{:root_span, span_data.span_id}, span_data, stored_at})
47+
else
48+
key = {:child_span, span_data.parent_span_id, span_data.span_id}
4849

49-
:ets.insert(table_name, {key, span_data, stored_at})
50+
:ets.insert(table_name, {key, span_data, stored_at})
51+
end
5052
end
51-
end
5253

53-
@spec get_root_span(String.t(), keyword()) :: SpanRecord.t() | nil
54-
def get_root_span(span_id, opts \\ []) do
55-
table_name = Keyword.get(opts, :table_name, default_table_name())
54+
@spec get_root_span(String.t(), keyword()) :: SpanRecord.t() | nil
55+
def get_root_span(span_id, opts \\ []) do
56+
table_name = Keyword.get(opts, :table_name, default_table_name())
5657

57-
case :ets.lookup(table_name, {:root_span, span_id}) do
58-
[{{:root_span, ^span_id}, span, _stored_at}] -> span
59-
[] -> nil
58+
case :ets.lookup(table_name, {:root_span, span_id}) do
59+
[{{:root_span, ^span_id}, span, _stored_at}] -> span
60+
[] -> nil
61+
end
6062
end
61-
end
6263

63-
@spec get_child_spans(String.t(), keyword()) :: [SpanRecord.t()]
64-
def get_child_spans(parent_span_id, opts \\ []) do
65-
table_name = Keyword.get(opts, :table_name, default_table_name())
64+
@spec get_child_spans(String.t(), keyword()) :: [SpanRecord.t()]
65+
def get_child_spans(parent_span_id, opts \\ []) do
66+
table_name = Keyword.get(opts, :table_name, default_table_name())
6667

67-
get_all_descendants(parent_span_id, table_name)
68-
end
68+
get_all_descendants(parent_span_id, table_name)
69+
end
6970

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

75-
nested_descendants =
76-
Enum.flat_map(direct_children, fn child ->
77-
get_all_descendants(child.span_id, table_name)
78-
end)
76+
nested_descendants =
77+
Enum.flat_map(direct_children, fn child ->
78+
get_all_descendants(child.span_id, table_name)
79+
end)
7980

80-
(direct_children ++ nested_descendants)
81-
|> Enum.sort_by(& &1.start_time)
82-
end
81+
(direct_children ++ nested_descendants)
82+
|> Enum.sort_by(& &1.start_time)
83+
end
8384

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

89-
key =
90-
if parent_span_id == nil do
91-
{:root_span, span_data.span_id}
92-
else
93-
{:child_span, parent_span_id, span_data.span_id}
94-
end
90+
key =
91+
if parent_span_id == nil do
92+
{:root_span, span_data.span_id}
93+
else
94+
{:child_span, parent_span_id, span_data.span_id}
95+
end
9596

96-
:ets.update_element(table_name, key, [{2, span_data}, {3, stored_at}])
97+
:ets.update_element(table_name, key, [{2, span_data}, {3, stored_at}])
9798

98-
:ok
99-
end
99+
:ok
100+
end
100101

101-
@spec remove_root_span(String.t(), keyword()) :: :ok
102-
def remove_root_span(span_id, opts \\ []) do
103-
table_name = Keyword.get(opts, :table_name, default_table_name())
104-
key = {:root_span, span_id}
102+
@spec remove_root_span(String.t(), keyword()) :: :ok
103+
def remove_root_span(span_id, opts \\ []) do
104+
table_name = Keyword.get(opts, :table_name, default_table_name())
105+
key = {:root_span, span_id}
105106

106-
:ets.select_delete(table_name, [{{key, :_, :_}, [], [true]}])
107-
remove_child_spans(span_id, table_name: table_name)
107+
:ets.select_delete(table_name, [{{key, :_, :_}, [], [true]}])
108+
remove_child_spans(span_id, table_name: table_name)
108109

109-
:ok
110-
end
110+
:ok
111+
end
111112

112-
@spec remove_child_spans(String.t(), keyword()) :: :ok
113-
def remove_child_spans(parent_span_id, opts) do
114-
table_name = Keyword.get(opts, :table_name, default_table_name())
113+
@spec remove_child_spans(String.t(), keyword()) :: :ok
114+
def remove_child_spans(parent_span_id, opts) do
115+
table_name = Keyword.get(opts, :table_name, default_table_name())
115116

116-
:ets.select_delete(table_name, [
117-
{{{:child_span, parent_span_id, :_}, :_, :_}, [], [true]}
118-
])
117+
:ets.select_delete(table_name, [
118+
{{{:child_span, parent_span_id, :_}, :_, :_}, [], [true]}
119+
])
119120

120-
:ok
121-
end
121+
:ok
122+
end
122123

123-
defp schedule_cleanup(interval) do
124-
Process.send_after(self(), :cleanup_stale_spans, interval)
125-
end
124+
defp schedule_cleanup(interval) do
125+
Process.send_after(self(), :cleanup_stale_spans, interval)
126+
end
126127

127-
defp cleanup_stale_spans(table_name) do
128-
now = System.system_time(:second)
129-
cutoff_time = now - @span_ttl
128+
defp cleanup_stale_spans(table_name) do
129+
now = System.system_time(:second)
130+
cutoff_time = now - @span_ttl
130131

131-
root_match_spec = [
132-
{{{:root_span, :"$1"}, :_, :"$2"}, [{:<, :"$2", cutoff_time}], [:"$1"]}
133-
]
132+
root_match_spec = [
133+
{{{:root_span, :"$1"}, :_, :"$2"}, [{:<, :"$2", cutoff_time}], [:"$1"]}
134+
]
134135

135-
expired_root_spans = :ets.select(table_name, root_match_spec)
136+
expired_root_spans = :ets.select(table_name, root_match_spec)
136137

137-
Enum.each(expired_root_spans, fn span_id ->
138-
remove_root_span(span_id, table_name: table_name)
139-
end)
138+
Enum.each(expired_root_spans, fn span_id ->
139+
remove_root_span(span_id, table_name: table_name)
140+
end)
140141

141-
child_match_spec = [
142-
{{{:child_span, :_, :_}, :_, :"$1"}, [{:<, :"$1", cutoff_time}], [true]}
143-
]
142+
child_match_spec = [
143+
{{{:child_span, :_, :_}, :_, :"$1"}, [{:<, :"$1", cutoff_time}], [true]}
144+
]
144145

145-
:ets.select_delete(table_name, child_match_spec)
146-
end
146+
:ets.select_delete(table_name, child_match_spec)
147+
end
147148

148-
defp default_table_name do
149-
Module.concat(__MODULE__, ETSTable)
149+
defp default_table_name do
150+
Module.concat(__MODULE__, ETSTable)
151+
end
150152
end
151153
end
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
defmodule Sentry.OpenTelemetry.VersionChecker do
2+
@moduledoc false
3+
4+
require Logger
5+
6+
@minimum_versions %{
7+
opentelemetry: "1.5.0",
8+
opentelemetry_api: "1.4.0",
9+
opentelemetry_exporter: "1.0.0",
10+
opentelemetry_semantic_conventions: "1.27.0"
11+
}
12+
13+
@doc """
14+
Checks if the installed OpenTelemetry dependencies meet the minimum version requirements.
15+
16+
Returns `{:ok, :compatible}` if all required dependencies are present and meet minimum versions.
17+
Returns `{:error, reason}` if dependencies are missing or incompatible.
18+
"""
19+
@spec check_compatibility() :: {:ok, :compatible} | {:error, term()}
20+
def check_compatibility do
21+
case check_all_dependencies() do
22+
[] ->
23+
{:ok, :compatible}
24+
25+
errors ->
26+
{:error, {:incompatible_versions, errors}}
27+
end
28+
end
29+
30+
@doc """
31+
Returns true if OpenTelemetry tracing should be enabled based on version compatibility.
32+
"""
33+
@spec tracing_compatible?() :: boolean()
34+
def tracing_compatible? do
35+
case check_compatibility() do
36+
{:ok, :compatible} -> true
37+
{:error, _} -> false
38+
end
39+
end
40+
41+
defp check_all_dependencies do
42+
@minimum_versions
43+
|> Enum.flat_map(fn {dep, min_version} ->
44+
case check_dependency_version(dep, min_version) do
45+
:ok -> []
46+
{:error, reason} -> [{dep, reason}]
47+
end
48+
end)
49+
end
50+
51+
defp check_dependency_version(dep, min_version) do
52+
case get_loaded_version(dep) do
53+
{:ok, loaded_version} ->
54+
if version_compatible?(loaded_version, min_version) do
55+
:ok
56+
else
57+
{:error, {:version_too_old, loaded_version, min_version}}
58+
end
59+
60+
{:error, :not_loaded} ->
61+
{:error, :not_loaded}
62+
end
63+
end
64+
65+
defp get_loaded_version(dep) do
66+
case Application.loaded_applications() do
67+
apps when is_list(apps) ->
68+
case List.keyfind(apps, dep, 0) do
69+
{^dep, _description, version} ->
70+
{:ok, to_string(version)}
71+
72+
nil ->
73+
{:error, :not_loaded}
74+
end
75+
76+
_ ->
77+
{:error, :not_loaded}
78+
end
79+
end
80+
81+
defp version_compatible?(loaded_version, min_version) do
82+
case Version.compare(loaded_version, min_version) do
83+
:gt -> true
84+
:eq -> true
85+
:lt -> false
86+
end
87+
rescue
88+
Version.InvalidVersionError ->
89+
# If we can't parse the version, assume it's incompatible
90+
false
91+
end
92+
end

mix.exs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ defmodule Sentry.Mixfile do
118118
{:floki, ">= 0.30.0", only: :test},
119119
{:oban, "~> 2.17 and >= 2.17.6", only: [:test]},
120120
{:quantum, "~> 3.0", only: [:test]},
121-
{:opentelemetry, "~> 1.5", optional: true},
122-
{:opentelemetry_api, "~> 1.4", optional: true},
123-
{:opentelemetry_exporter, "~> 1.0", optional: true},
124-
{:opentelemetry_semantic_conventions, "~> 1.27", optional: true}
121+
{:opentelemetry, ">= 0.0.0", optional: true},
122+
{:opentelemetry_api, ">= 0.0.0", optional: true},
123+
{:opentelemetry_exporter, ">= 0.0.0", optional: true},
124+
{:opentelemetry_semantic_conventions, ">= 0.0.0", optional: true}
125125
]
126126
end
127127

0 commit comments

Comments
 (0)