Skip to content

Commit 20d2494

Browse files
committed
feat(otel): automatic setup based on config
1 parent e7eaaa8 commit 20d2494

File tree

5 files changed

+710
-0
lines changed

5 files changed

+710
-0
lines changed

lib/sentry/application.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ defmodule Sentry.Application do
2828
end
2929

3030
integrations_config = Config.integrations()
31+
otel_config = Keyword.get(integrations_config, :opentelemetry, [])
32+
33+
# Configure OTel SDK before supervisor starts (and ideally before :opentelemetry starts)
34+
if Config.tracing?() do
35+
maybe_configure_otel_sdk(otel_config)
36+
end
3137

3238
maybe_span_storage =
3339
if Config.tracing?() do
@@ -69,6 +75,7 @@ defmodule Sentry.Application do
6975
with {:ok, pid} <-
7076
Supervisor.start_link(children, strategy: :one_for_one, name: Sentry.Supervisor) do
7177
start_integrations(integrations_config)
78+
maybe_setup_otel_instrumentations(otel_config)
7279
maybe_add_logger_handler()
7380
{:ok, pid}
7481
end
@@ -137,6 +144,21 @@ defmodule Sentry.Application do
137144
|> Enum.any?(fn %{module: module} -> module == Sentry.LoggerHandler end)
138145
end
139146

147+
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
148+
defp maybe_configure_otel_sdk(otel_config) do
149+
Sentry.OpenTelemetry.Setup.maybe_configure_otel_sdk(otel_config)
150+
end
151+
152+
defp maybe_setup_otel_instrumentations(otel_config) do
153+
if Config.tracing?() do
154+
Sentry.OpenTelemetry.Setup.maybe_setup_instrumentations(otel_config)
155+
end
156+
end
157+
else
158+
defp maybe_configure_otel_sdk(_otel_config), do: :ok
159+
defp maybe_setup_otel_instrumentations(_otel_config), do: :ok
160+
end
161+
140162
# In tests, we do not run a global rate limiter; tests start their own when
141163
# they need it.
142164
if Mix.env() == :test do

lib/sentry/config.ex

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,94 @@ defmodule Sentry.Config do
168168
"""
169169
]
170170
]
171+
],
172+
opentelemetry: [
173+
type: :keyword_list,
174+
default: [],
175+
doc: """
176+
Configuration for automatic OpenTelemetry setup when tracing is enabled
177+
(`:traces_sample_rate` or `:traces_sampler` is set). When tracing is enabled, the SDK
178+
automatically configures the OpenTelemetry SDK (span processor, sampler, propagator)
179+
and sets up available instrumentation libraries.
180+
181+
The SDK only sets OpenTelemetry application config if the user hasn't already configured
182+
it via `config :opentelemetry` entries, ensuring backward compatibility.
183+
184+
*Available since 12.1.0*.
185+
""",
186+
keys: [
187+
auto_setup: [
188+
type: :boolean,
189+
default: true,
190+
doc: """
191+
Whether to auto-configure the OpenTelemetry SDK with Sentry's span processor,
192+
sampler, and propagator. Set to `false` if you want to configure the OTel SDK manually
193+
via `config :opentelemetry` entries.
194+
"""
195+
],
196+
sampler_opts: [
197+
type: :keyword_list,
198+
default: [],
199+
doc: """
200+
Options passed to `Sentry.OpenTelemetry.Sampler`. For example, use `drop: ["span_name"]`
201+
to drop specific spans from sampling.
202+
"""
203+
],
204+
phoenix: [
205+
type: {:or, [:boolean, :keyword_list]},
206+
default: true,
207+
doc: """
208+
Auto-setup `OpentelemetryPhoenix` if available. Pass a keyword list to provide options
209+
(e.g., `[adapter: :bandit]`). Set to `false` to disable.
210+
"""
211+
],
212+
bandit: [
213+
type: :boolean,
214+
default: true,
215+
doc: """
216+
Auto-setup `OpentelemetryBandit` if available. Set to `false` to disable.
217+
"""
218+
],
219+
cowboy: [
220+
type: :boolean,
221+
default: true,
222+
doc: """
223+
Auto-setup `OpentelemetryCowboy` if available. Set to `false` to disable.
224+
"""
225+
],
226+
oban: [
227+
type: :boolean,
228+
default: true,
229+
doc: """
230+
Auto-setup `OpentelemetryOban` if available. Set to `false` to disable.
231+
"""
232+
],
233+
ecto: [
234+
type: {:or, [:boolean, :keyword_list]},
235+
default: false,
236+
doc: """
237+
Auto-setup `OpentelemetryEcto` if available. Requires the `:repos` option with a list
238+
of repo telemetry prefixes (e.g., `[repos: [[:my_app, :repo]]]`). Additional options
239+
are passed through to `OpentelemetryEcto.setup/2`. Defaults to `false` since repo
240+
names cannot be auto-detected.
241+
"""
242+
],
243+
live_view: [
244+
type: :boolean,
245+
default: true,
246+
doc: """
247+
Auto-setup `Sentry.OpenTelemetry.LiveViewPropagator` for distributed tracing context
248+
propagation to LiveView processes. Set to `false` to disable.
249+
"""
250+
],
251+
logger_metadata: [
252+
type: :boolean,
253+
default: true,
254+
doc: """
255+
Auto-setup `OpentelemetryLoggerMetadata` if available. Set to `false` to disable.
256+
"""
257+
]
258+
]
171259
]
172260
]
173261

lib/sentry/opentelemetry/setup.ex

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
2+
defmodule Sentry.OpenTelemetry.Setup do
3+
@moduledoc false
4+
5+
require Logger
6+
7+
@doc """
8+
Configures the OpenTelemetry SDK with Sentry's span processor, sampler, and propagator.
9+
10+
Called during `Sentry.Application.start/2`, before the supervisor starts.
11+
Only applies configuration if the user hasn't already configured it via
12+
`config :opentelemetry` entries.
13+
"""
14+
@spec maybe_configure_otel_sdk(keyword()) :: :ok
15+
def maybe_configure_otel_sdk(otel_config) do
16+
unless Keyword.get(otel_config, :auto_setup) == false do
17+
maybe_set_span_processor()
18+
maybe_set_sampler(Keyword.get(otel_config, :sampler_opts, []))
19+
maybe_set_propagators()
20+
end
21+
22+
:ok
23+
end
24+
25+
@doc """
26+
Auto-detects and sets up available instrumentation libraries.
27+
28+
Called after the Sentry supervisor starts. Respects per-instrumentation config
29+
to enable/disable individual libraries.
30+
"""
31+
@spec maybe_setup_instrumentations(keyword()) :: :ok
32+
def maybe_setup_instrumentations(otel_config) do
33+
# Order matters: LiveView propagator MUST come before Phoenix
34+
maybe_setup(:live_view, otel_config, fn _opts ->
35+
setup_if_available(Sentry.OpenTelemetry.LiveViewPropagator, :setup, [])
36+
end)
37+
38+
maybe_setup(:bandit, otel_config, fn _opts ->
39+
setup_if_available(OpentelemetryBandit, :setup, [])
40+
end)
41+
42+
maybe_setup(:cowboy, otel_config, fn _opts ->
43+
setup_if_available(OpentelemetryCowboy, :setup, [])
44+
end)
45+
46+
maybe_setup(:phoenix, otel_config, fn opts ->
47+
setup_if_available(OpentelemetryPhoenix, :setup, [opts])
48+
end)
49+
50+
maybe_setup(:oban, otel_config, fn _opts ->
51+
setup_if_available(OpentelemetryOban, :setup, [])
52+
end)
53+
54+
maybe_setup(:ecto, otel_config, fn opts ->
55+
repos = Keyword.get(opts, :repos, [])
56+
ecto_opts = Keyword.drop(opts, [:repos])
57+
58+
for repo <- repos do
59+
setup_if_available(OpentelemetryEcto, :setup, [repo, ecto_opts])
60+
end
61+
end)
62+
63+
maybe_setup(:logger_metadata, otel_config, fn _opts ->
64+
setup_if_available(OpentelemetryLoggerMetadata, :setup, [])
65+
end)
66+
67+
:ok
68+
end
69+
70+
# OTel SDK configuration
71+
72+
defp maybe_set_span_processor do
73+
if Application.get_env(:opentelemetry, :span_processor) == nil and
74+
Application.get_env(:opentelemetry, :processors) == nil do
75+
if otel_started?() do
76+
Logger.warning(
77+
"[Sentry] OpenTelemetry has already started. " <>
78+
"Cannot auto-configure span processor. " <>
79+
"Add `config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []}` " <>
80+
"to your config or ensure :sentry starts before :opentelemetry."
81+
)
82+
else
83+
Application.put_env(
84+
:opentelemetry,
85+
:span_processor,
86+
{Sentry.OpenTelemetry.SpanProcessor, []}
87+
)
88+
end
89+
end
90+
end
91+
92+
defp maybe_set_sampler(sampler_opts) do
93+
if Application.get_env(:opentelemetry, :sampler) == nil do
94+
if otel_started?() do
95+
Logger.warning(
96+
"[Sentry] OpenTelemetry has already started. " <>
97+
"Cannot auto-configure sampler. " <>
98+
"Add `config :opentelemetry, sampler: {Sentry.OpenTelemetry.Sampler, #{inspect(sampler_opts)}}` " <>
99+
"to your config or ensure :sentry starts before :opentelemetry."
100+
)
101+
else
102+
Application.put_env(
103+
:opentelemetry,
104+
:sampler,
105+
{Sentry.OpenTelemetry.Sampler, sampler_opts}
106+
)
107+
end
108+
end
109+
end
110+
111+
defp maybe_set_propagators do
112+
if Application.get_env(:opentelemetry, :text_map_propagators) == nil do
113+
propagators = [:trace_context, :baggage, Sentry.OpenTelemetry.Propagator]
114+
115+
if otel_started?() do
116+
# Propagators can be reconfigured at runtime
117+
composite = :otel_propagator_text_map_composite.create(propagators)
118+
:opentelemetry.set_text_map_propagator(composite)
119+
else
120+
Application.put_env(:opentelemetry, :text_map_propagators, propagators)
121+
end
122+
end
123+
end
124+
125+
# Instrumentation setup helpers
126+
127+
defp maybe_setup(key, otel_config, setup_fn) do
128+
config_value = Keyword.get(otel_config, key, default_for(key))
129+
130+
unless config_value == false do
131+
opts = if is_list(config_value), do: config_value, else: []
132+
133+
try do
134+
setup_fn.(opts)
135+
rescue
136+
e ->
137+
Logger.warning(
138+
"[Sentry] Failed to auto-setup #{key} instrumentation: #{Exception.message(e)}"
139+
)
140+
end
141+
end
142+
end
143+
144+
defp setup_if_available(module, function, args) do
145+
if Code.ensure_loaded?(module) do
146+
apply(module, function, args)
147+
end
148+
end
149+
150+
defp default_for(:ecto), do: false
151+
defp default_for(_key), do: true
152+
153+
defp otel_started? do
154+
case List.keyfind(Application.started_applications(), :opentelemetry, 0) do
155+
nil -> false
156+
_ -> true
157+
end
158+
end
159+
end
160+
end

0 commit comments

Comments
 (0)