Skip to content

Commit 55cacc1

Browse files
committed
Revert "Revert "Add SpanProcessor for OpenTelemetry (#875)""
This reverts commit 2ced90e.
1 parent a4f444f commit 55cacc1

File tree

20 files changed

+1185
-9
lines changed

20 files changed

+1185
-9
lines changed

config/config.exs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ if config_env() == :test do
1010
send_result: :sync,
1111
send_max_attempts: 1,
1212
dedup_events: false,
13-
test_mode: true
13+
test_mode: true,
14+
traces_sample_rate: 1.0
1415

1516
config :logger, backends: []
17+
18+
config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []}
19+
20+
config :opentelemetry,
21+
sampler: {Sentry.OpenTelemetry.Sampler, [drop: ["Elixir.Oban.Stager process"]]}
1622
end
1723

1824
config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason)

lib/sentry/application.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ defmodule Sentry.Application do
2626

2727
integrations_config = Config.integrations()
2828

29+
maybe_span_storage =
30+
if Config.tracing?() do
31+
[Sentry.OpenTelemetry.SpanStorage]
32+
else
33+
[]
34+
end
35+
2936
children =
3037
[
3138
{Registry, keys: :unique, name: Sentry.Transport.SenderRegistry},
@@ -39,6 +46,7 @@ defmodule Sentry.Application do
3946
]}
4047
] ++
4148
maybe_http_client_spec ++
49+
maybe_span_storage ++
4250
[Sentry.Transport.SenderPool]
4351

4452
cache_loaded_applications()

lib/sentry/client.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ defmodule Sentry.Client do
117117

118118
result_type = Keyword.get_lazy(opts, :result, &Config.send_result/0)
119119
client = Keyword.get_lazy(opts, :client, &Config.client/0)
120-
sample_rate = Keyword.get_lazy(opts, :sample_rate, &Config.sample_rate/0)
120+
sample_rate = Keyword.get_lazy(opts, :sample_rate, &Config.traces_sample_rate/0)
121121
before_send = Keyword.get_lazy(opts, :before_send, &Config.before_send/0)
122122
after_send_event = Keyword.get_lazy(opts, :after_send_event, &Config.after_send_event/0)
123123

lib/sentry/config.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ defmodule Sentry.Config do
143143
be used as the value for this option.
144144
"""
145145
],
146+
traces_sample_rate: [
147+
type: {:custom, __MODULE__, :__validate_traces_sample_rate__, []},
148+
default: 0.0,
149+
doc: """
150+
The sample rate for transaction events. A value between `0.0` and `1.0` (inclusive).
151+
A value of `0.0` means no transactions will be sampled, while `1.0` means all transactions
152+
will be sampled. This value is also used to determine if tracing is enabled: if it's
153+
greater than `0`, tracing is enabled.
154+
155+
Tracing requires OpenTelemetry packages to work. See [the
156+
OpenTelemetry setup documentation](https://opentelemetry.io/docs/languages/erlang/getting-started/)
157+
for guides on how to set it up.
158+
"""
159+
],
146160
included_environments: [
147161
type: {:or, [{:in, [:all]}, {:list, {:or, [:atom, :string]}}]},
148162
deprecated: "Use :dsn to control whether to send events to Sentry.",
@@ -607,6 +621,9 @@ defmodule Sentry.Config do
607621
@spec sample_rate() :: float()
608622
def sample_rate, do: fetch!(:sample_rate)
609623

624+
@spec traces_sample_rate() :: float()
625+
def traces_sample_rate, do: fetch!(:traces_sample_rate)
626+
610627
@spec hackney_opts() :: keyword()
611628
def hackney_opts, do: fetch!(:hackney_opts)
612629

@@ -644,6 +661,9 @@ defmodule Sentry.Config do
644661
@spec integrations() :: keyword()
645662
def integrations, do: fetch!(:integrations)
646663

664+
@spec tracing?() :: boolean()
665+
def tracing?, do: fetch!(:traces_sample_rate) > 0.0
666+
647667
@spec put_config(atom(), term()) :: :ok
648668
def put_config(key, value) when is_atom(key) do
649669
unless key in @valid_keys do
@@ -743,6 +763,15 @@ defmodule Sentry.Config do
743763
end
744764
end
745765

766+
def __validate_traces_sample_rate__(float) do
767+
if is_float(float) and float >= 0.0 and float <= 1.0 do
768+
{:ok, float}
769+
else
770+
{:error,
771+
"expected :traces_sample_rate to be a float between 0.0 and 1.0 (included), got: #{inspect(float)}"}
772+
end
773+
end
774+
746775
def __validate_json_library__(nil) do
747776
{:error, "nil is not a valid value for the :json_library option"}
748777
end

lib/sentry/opentelemetry/sampler.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
if Code.ensure_loaded?(:otel_sampler) do
2+
defmodule Sentry.OpenTelemetry.Sampler do
3+
@moduledoc false
4+
5+
@behaviour :otel_sampler
6+
7+
def setup(config) do
8+
config
9+
end
10+
11+
def description(_) do
12+
"SentrySampler"
13+
end
14+
15+
def should_sample(
16+
_ctx,
17+
_trace_id,
18+
_links,
19+
span_name,
20+
_span_kind,
21+
_attributes,
22+
config
23+
) do
24+
if span_name in config[:drop] do
25+
{:drop, [], []}
26+
else
27+
{:record_and_sample, [], []}
28+
end
29+
end
30+
end
31+
end
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
if Code.ensure_loaded?(OpenTelemetry) do
2+
defmodule Sentry.OpenTelemetry.SpanProcessor do
3+
@moduledoc false
4+
5+
@behaviour :otel_span_processor
6+
7+
require OpenTelemetry.SemConv.ClientAttributes, as: ClientAttributes
8+
require OpenTelemetry.SemConv.Incubating.DBAttributes, as: DBAttributes
9+
require OpenTelemetry.SemConv.Incubating.HTTPAttributes, as: HTTPAttributes
10+
require OpenTelemetry.SemConv.Incubating.URLAttributes, as: URLAttributes
11+
require OpenTelemetry.SemConv.Incubating.MessagingAttributes, as: MessagingAttributes
12+
13+
require Logger
14+
15+
alias Sentry.{Transaction, OpenTelemetry.SpanStorage, OpenTelemetry.SpanRecord}
16+
alias Sentry.Interfaces.Span
17+
18+
# This can be a no-op since we can postpone inserting the span into storage until on_end
19+
@impl :otel_span_processor
20+
def on_start(_ctx, otel_span, _config) do
21+
otel_span
22+
end
23+
24+
@impl :otel_span_processor
25+
def on_end(otel_span, _config) do
26+
span_record = SpanRecord.new(otel_span)
27+
28+
SpanStorage.store_span(span_record)
29+
30+
if span_record.parent_span_id == nil do
31+
child_span_records = SpanStorage.get_child_spans(span_record.span_id)
32+
transaction = build_transaction(span_record, child_span_records)
33+
34+
result =
35+
case Sentry.send_transaction(transaction) do
36+
{:ok, _id} ->
37+
true
38+
39+
:ignored ->
40+
true
41+
42+
{:error, error} ->
43+
Logger.warning("Failed to send transaction to Sentry: #{inspect(error)}")
44+
{:error, :invalid_span}
45+
end
46+
47+
:ok = SpanStorage.remove_root_span(span_record.span_id)
48+
49+
result
50+
else
51+
true
52+
end
53+
end
54+
55+
@impl :otel_span_processor
56+
def force_flush(_config) do
57+
:ok
58+
end
59+
60+
defp build_transaction(root_span_record, child_span_records) do
61+
root_span = build_span(root_span_record)
62+
child_spans = Enum.map(child_span_records, &build_span(&1))
63+
64+
Transaction.new(%{
65+
span_id: root_span.span_id,
66+
transaction: transaction_name(root_span_record),
67+
transaction_info: %{source: :custom},
68+
start_timestamp: root_span_record.start_time,
69+
timestamp: root_span_record.end_time,
70+
contexts: %{
71+
trace: build_trace_context(root_span_record)
72+
},
73+
spans: child_spans
74+
})
75+
end
76+
77+
defp transaction_name(
78+
%{attributes: %{unquote(to_string(MessagingAttributes.messaging_system())) => :oban}} =
79+
span_record
80+
) do
81+
span_record.attributes["oban.job.worker"]
82+
end
83+
84+
defp transaction_name(span_record), do: span_record.name
85+
86+
defp build_trace_context(span_record) do
87+
{op, description} = get_op_description(span_record)
88+
89+
%{
90+
trace_id: span_record.trace_id,
91+
span_id: span_record.span_id,
92+
parent_span_id: span_record.parent_span_id,
93+
op: op,
94+
description: description,
95+
origin: span_record.origin,
96+
data: span_record.attributes
97+
}
98+
end
99+
100+
defp get_op_description(
101+
%{
102+
attributes: %{
103+
unquote(to_string(HTTPAttributes.http_request_method())) => http_request_method
104+
}
105+
} = span_record
106+
) do
107+
op = "http.#{span_record.kind}"
108+
109+
client_address =
110+
Map.get(span_record.attributes, to_string(ClientAttributes.client_address()))
111+
112+
url_path = Map.get(span_record.attributes, to_string(URLAttributes.url_path()))
113+
114+
description =
115+
to_string(http_request_method) <>
116+
((client_address && " from #{client_address}") || "") <>
117+
((url_path && " #{url_path}") || "")
118+
119+
{op, description}
120+
end
121+
122+
defp get_op_description(
123+
%{attributes: %{unquote(to_string(DBAttributes.db_system())) => _db_system}} =
124+
span_record
125+
) do
126+
db_query_text = Map.get(span_record.attributes, "db.statement")
127+
128+
{"db", db_query_text}
129+
end
130+
131+
defp get_op_description(%{
132+
attributes:
133+
%{unquote(to_string(MessagingAttributes.messaging_system())) => :oban} = attributes
134+
}) do
135+
{"queue.process", attributes["oban.job.worker"]}
136+
end
137+
138+
defp get_op_description(span_record) do
139+
{span_record.name, span_record.name}
140+
end
141+
142+
defp build_span(span_record) do
143+
{op, description} = get_op_description(span_record)
144+
145+
%Span{
146+
op: op,
147+
description: description,
148+
start_timestamp: span_record.start_time,
149+
timestamp: span_record.end_time,
150+
trace_id: span_record.trace_id,
151+
span_id: span_record.span_id,
152+
parent_span_id: span_record.parent_span_id,
153+
origin: span_record.origin,
154+
data: Map.put(span_record.attributes, "otel.kind", span_record.kind),
155+
status: span_status(span_record)
156+
}
157+
end
158+
159+
defp span_status(%{
160+
attributes: %{
161+
unquote(to_string(HTTPAttributes.http_response_status_code())) =>
162+
http_response_status_code
163+
}
164+
}) do
165+
to_status(http_response_status_code)
166+
end
167+
168+
defp span_status(_span_record), do: nil
169+
170+
# WebSocket upgrade spans doesn't have a HTTP status
171+
defp to_status(nil), do: nil
172+
173+
defp to_status(status) when status in 200..299, do: "ok"
174+
175+
for {status, string} <- %{
176+
400 => "invalid_argument",
177+
401 => "unauthenticated",
178+
403 => "permission_denied",
179+
404 => "not_found",
180+
409 => "already_exists",
181+
429 => "resource_exhausted",
182+
499 => "cancelled",
183+
500 => "internal_error",
184+
501 => "unimplemented",
185+
503 => "unavailable",
186+
504 => "deadline_exceeded"
187+
} do
188+
defp to_status(unquote(status)), do: unquote(string)
189+
end
190+
191+
defp to_status(_any), do: "unknown_error"
192+
end
193+
end

0 commit comments

Comments
 (0)