Skip to content

Commit f85d93d

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 f85d93d

File tree

18 files changed

+508
-121
lines changed

18 files changed

+508
-121
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ erl_crash.dump
1414
/priv/sentry.map
1515

1616
test_integrations/phoenix_app/db
17+
18+
test_integrations/*/_build
19+
test_integrations/*/deps

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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
defmodule Sentry.OpenTelemetry.VersionChecker do
2+
@moduledoc false
3+
4+
@minimum_versions %{
5+
opentelemetry: "1.5.0",
6+
opentelemetry_api: "1.4.0",
7+
opentelemetry_exporter: "1.0.0",
8+
opentelemetry_semantic_conventions: "1.27.0"
9+
}
10+
11+
@spec tracing_compatible?() :: boolean()
12+
def tracing_compatible? do
13+
case check_compatibility() do
14+
{:ok, :compatible} -> true
15+
{:error, _} -> false
16+
end
17+
end
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+
defp check_all_dependencies do
31+
@minimum_versions
32+
|> Enum.flat_map(fn {dep, min_version} ->
33+
case check_dependency_version(dep, min_version) do
34+
:ok -> []
35+
{:error, reason} -> [{dep, reason}]
36+
end
37+
end)
38+
end
39+
40+
defp check_dependency_version(dep, min_version) do
41+
case get_loaded_version(dep) do
42+
{:ok, loaded_version} ->
43+
if version_compatible?(loaded_version, min_version) do
44+
:ok
45+
else
46+
{:error, {:version_too_old, loaded_version, min_version}}
47+
end
48+
49+
{:error, :not_loaded} ->
50+
{:error, :not_loaded}
51+
end
52+
end
53+
54+
defp get_loaded_version(dep) do
55+
apps = Application.loaded_applications()
56+
57+
case List.keyfind(apps, dep, 0) do
58+
{^dep, _description, version} ->
59+
{:ok, to_string(version)}
60+
61+
nil ->
62+
{:error, :not_loaded}
63+
end
64+
end
65+
66+
defp version_compatible?(loaded_version, min_version) do
67+
case Version.compare(loaded_version, min_version) do
68+
:gt -> true
69+
:eq -> true
70+
:lt -> false
71+
end
72+
end
73+
end

mix.exs

Lines changed: 5 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

@@ -148,6 +148,7 @@ defmodule Sentry.Mixfile do
148148
if Version.match?(System.version(), ">= 1.16.0") do
149149
run_integration_tests("umbrella", args)
150150
run_integration_tests("phoenix_app", args)
151+
run_integration_tests("legacy_otel", args)
151152
else
152153
Mix.shell().info("Skipping integration tests for Elixir versions < 1.16")
153154
end

0 commit comments

Comments
 (0)