Skip to content

Commit 8ded98a

Browse files
authored
SpanProcessor with better Sampler (#902)
* Revert "Revert "Add SpanProcessor for OpenTelemetry (#875)"" This reverts commit 2ced90e. * Better span sampler (#903) * Better span sampler This updates OpenTelemetry.Sampler so that it uses `traces_sample_rate` for sampling spans before they get sent to the span processor. This way we're not processing spans when they are dropped during sampling, which was previously the case as the Client was responsible for sampling just before attemping to send a transaction. * Enhance sampling logic to record discarded transactions * Clarify trace-level sampling decision-making Previously it incorrectly referred to "parent" where in reality we're dealing with spans from the same trace and respect sampling decisions that were already made for a given trace. This is not the same thing as parent/child spans that we're handling in the SpanProcessor as this type of relationship is established post-sampling. * Remove unnecessary sleep calls in sampler tests * Fix flaky test * Clean up make_sampling_decision * Simplify client reports for dropped transactions * Update traces_sample_rate default value to be nil * Account for `nil` sample_rate when sampling * Use put_test_config * Update traces_sample_rate spec to allow nil value * Support for traces_sampler option (#910) * Add support for `traces_sampler` config option * Update docs * Remove unnecessary attribute merging * Call traces_sampler only for root spans * Reuse sampling decision logic * Add test for handling invalid traces_sampler return values * Add tests for SamplingContext access functions * Add basic integration tests for OpenTelemetry (#912) * Update CHANGELOG
1 parent d5c171a commit 8ded98a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3516
-66
lines changed

.dialyzer_ignore.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[
22
{"test/support/example_plug_application.ex"},
3-
{"test/support/test_helpers.ex"}
3+
{"test/support/test_helpers.ex"},
4+
{"lib/sentry/opentelemetry/sampler.ex", :pattern_match, 1}
45
]

CHANGELOG.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,44 @@
11
## Unreleased
22

3-
- Tweak credit card regex handling for OTP-28 ([#898](https://github.com/getsentry/sentry-elixir/pull/898))
3+
This release comes with a beta support for Traces using OpenTelemetry - please test it out and report any issues you find.
4+
5+
### New features
6+
7+
- Beta support for Traces using OpenTelemetry ([#902](https://github.com/getsentry/sentry-elixir/pull/902))
8+
9+
To enable Tracing in your Phoenix application, you need to add the following to your `mix.exs`:
10+
11+
```elixir
12+
def deps do
13+
[
14+
# ...
15+
{:sentry, "~> 11.0.0"},
16+
{:opentelemetry, "~> 1.5"},
17+
{:opentelemetry_api, "~> 1.4"},
18+
{:opentelemetry_exporter, "~> 1.0"},
19+
{:opentelemetry_semantic_conventions, "~> 1.27"},
20+
{:opentelemetry_phoenix, "~> 2.0"},
21+
{:opentelemetry_ecto, "~> 1.2"},
22+
# ...
23+
]
24+
```
25+
26+
And then configure Tracing in Sentry and OpenTelemetry in your `config.exs`:
27+
28+
```elixir
29+
config :sentry,
30+
# ...
31+
traces_sample_rate: 1.0 # any value between 0 and 1.0 enables tracing
32+
33+
config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []}
34+
config :opentelemetry, sampler: {Sentry.OpenTelemetry.Sampler, []}
35+
```
436
- Add installer (based on Igniter) ([#876](https://github.com/getsentry/sentry-elixir/pull/876))
537

38+
### Various improvements
39+
40+
- Tweak credit card regex handling for OTP-28 ([#898](https://github.com/getsentry/sentry-elixir/pull/898))
41+
642
# Changelog
743

844
## 10.10.0

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 & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,12 @@ defmodule Sentry.Client do
110110
@spec send_transaction(Transaction.t(), keyword()) ::
111111
{:ok, transaction_id :: String.t()}
112112
| {:error, ClientError.t()}
113-
| :unsampled
114113
| :excluded
115114
def send_transaction(%Transaction{} = transaction, opts \\ []) do
116115
opts = NimbleOptions.validate!(opts, Options.send_transaction_schema())
117116

118117
result_type = Keyword.get_lazy(opts, :result, &Config.send_result/0)
119118
client = Keyword.get_lazy(opts, :client, &Config.client/0)
120-
sample_rate = Keyword.get_lazy(opts, :sample_rate, &Config.sample_rate/0)
121119
before_send = Keyword.get_lazy(opts, :before_send, &Config.before_send/0)
122120
after_send_event = Keyword.get_lazy(opts, :after_send_event, &Config.after_send_event/0)
123121

@@ -126,16 +124,11 @@ defmodule Sentry.Client do
126124
Application.get_env(:sentry, :request_retries, Transport.default_retries())
127125
end)
128126

129-
with :ok <- sample_event(sample_rate),
130-
{:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send) do
127+
with {:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send) do
131128
send_result = encode_and_send(transaction, result_type, client, request_retries)
132129
_ignored = maybe_call_after_send(transaction, send_result, after_send_event)
133130
send_result
134131
else
135-
:unsampled ->
136-
ClientReport.Sender.record_discarded_events(:sample_rate, [transaction])
137-
:unsampled
138-
139132
:excluded ->
140133
:excluded
141134
end

lib/sentry/client_report/sender.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,21 @@ defmodule Sentry.ClientReport.Sender do
1717
GenServer.start_link(__MODULE__, nil, name: Keyword.get(opts, :name, __MODULE__))
1818
end
1919

20+
def record_discarded_events(reason, info, genserver \\ __MODULE__)
21+
22+
@spec record_discarded_events(atom(), String.t(), GenServer.server()) :: :ok
23+
def record_discarded_events(reason, data_category, genserver)
24+
when is_binary(data_category) do
25+
GenServer.cast(genserver, {:record_discarded_events, reason, data_category})
26+
end
27+
2028
@spec record_discarded_events(atom(), [item], GenServer.server()) :: :ok
2129
when item:
2230
Sentry.Attachment.t()
2331
| Sentry.CheckIn.t()
2432
| ClientReport.t()
2533
| Sentry.Event.t()
26-
| Sentry.Transaction.t()
27-
def record_discarded_events(reason, event_items, genserver \\ __MODULE__)
34+
def record_discarded_events(reason, event_items, genserver)
2835
when is_list(event_items) do
2936
# We silently ignore events whose reasons aren't valid because we have to add it to the allowlist in Snuba
3037
# https://develop.sentry.dev/sdk/client-reports/

lib/sentry/config.ex

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
defmodule Sentry.Config do
22
@moduledoc false
33

4+
@typedoc """
5+
A function that determines the sample rate for transaction events.
6+
7+
The function receives a sampling context map and should return a boolean or a float between `0.0` and `1.0`.
8+
"""
9+
@type traces_sampler_function :: (map() -> boolean() | float()) | {module(), atom()}
10+
411
integrations_schema = [
512
max_expected_check_in_time: [
613
type: :integer,
@@ -143,6 +150,49 @@ defmodule Sentry.Config do
143150
be used as the value for this option.
144151
"""
145152
],
153+
traces_sample_rate: [
154+
type: {:custom, __MODULE__, :__validate_traces_sample_rate__, []},
155+
default: nil,
156+
doc: """
157+
The sample rate for transaction events. A value between `0.0` and `1.0` (inclusive).
158+
A value of `0.0` means no transactions will be sampled, while `1.0` means all transactions
159+
will be sampled.
160+
161+
This value is also used to determine if tracing is enabled: if it's not `nil`, tracing is enabled.
162+
163+
Tracing requires OpenTelemetry packages to work. See [the
164+
OpenTelemetry setup documentation](https://opentelemetry.io/docs/languages/erlang/getting-started/)
165+
for guides on how to set it up.
166+
"""
167+
],
168+
traces_sampler: [
169+
type: {:custom, __MODULE__, :__validate_traces_sampler__, []},
170+
default: nil,
171+
type_doc: "`t:traces_sampler_function/0` or `nil`",
172+
doc: """
173+
A function that determines the sample rate for transaction events. This function
174+
receives a sampling context struct and should return a boolean or a float between `0.0` and `1.0`.
175+
176+
The sampling context contains:
177+
- `:parent_sampled` - boolean indicating if the parent trace span was sampled (nil if no parent)
178+
- `:transaction_context` - map with transaction information (name, op, etc.)
179+
180+
If both `:traces_sampler` and `:traces_sample_rate` are configured, `:traces_sampler` takes precedence.
181+
182+
Example:
183+
```elixir
184+
traces_sampler: fn sampling_context ->
185+
case sampling_context.transaction_context.op do
186+
"http.server" -> 0.1 # Sample 10% of HTTP requests
187+
"db.query" -> 0.01 # Sample 1% of database queries
188+
_ -> false # Don't sample other operations
189+
end
190+
end
191+
```
192+
193+
This value is also used to determine if tracing is enabled: if it's not `nil`, tracing is enabled.
194+
"""
195+
],
146196
included_environments: [
147197
type: {:or, [{:in, [:all]}, {:list, {:or, [:atom, :string]}}]},
148198
deprecated: "Use :dsn to control whether to send events to Sentry.",
@@ -607,6 +657,12 @@ defmodule Sentry.Config do
607657
@spec sample_rate() :: float()
608658
def sample_rate, do: fetch!(:sample_rate)
609659

660+
@spec traces_sample_rate() :: nil | float()
661+
def traces_sample_rate, do: fetch!(:traces_sample_rate)
662+
663+
@spec traces_sampler() :: traces_sampler_function() | nil
664+
def traces_sampler, do: get(:traces_sampler)
665+
610666
@spec hackney_opts() :: keyword()
611667
def hackney_opts, do: fetch!(:hackney_opts)
612668

@@ -644,6 +700,9 @@ defmodule Sentry.Config do
644700
@spec integrations() :: keyword()
645701
def integrations, do: fetch!(:integrations)
646702

703+
@spec tracing?() :: boolean()
704+
def tracing?, do: not is_nil(fetch!(:traces_sample_rate)) or not is_nil(get(:traces_sampler))
705+
647706
@spec put_config(atom(), term()) :: :ok
648707
def put_config(key, value) when is_atom(key) do
649708
unless key in @valid_keys do
@@ -743,6 +802,35 @@ defmodule Sentry.Config do
743802
end
744803
end
745804

805+
def __validate_traces_sample_rate__(value) do
806+
if is_nil(value) or (is_float(value) and value >= 0.0 and value <= 1.0) do
807+
{:ok, value}
808+
else
809+
{:error,
810+
"expected :traces_sample_rate to be nil or a value between 0.0 and 1.0 (included), got: #{inspect(value)}"}
811+
end
812+
end
813+
814+
def __validate_traces_sampler__(nil), do: {:ok, nil}
815+
816+
def __validate_traces_sampler__(fun) when is_function(fun, 1) do
817+
{:ok, fun}
818+
end
819+
820+
def __validate_traces_sampler__({module, function})
821+
when is_atom(module) and is_atom(function) do
822+
if function_exported?(module, function, 1) do
823+
{:ok, {module, function}}
824+
else
825+
{:error, "function #{module}.#{function}/1 is not exported"}
826+
end
827+
end
828+
829+
def __validate_traces_sampler__(other) do
830+
{:error,
831+
"expected :traces_sampler to be nil, a function with arity 1, or a {module, function} tuple, got: #{inspect(other)}"}
832+
end
833+
746834
def __validate_json_library__(nil) do
747835
{:error, "nil is not a valid value for the :json_library option"}
748836
end

0 commit comments

Comments
 (0)