From 0e7108d3b39692e4e424ec82ac6a5b6ba4a19ae1 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 27 Jan 2025 11:50:22 +0000 Subject: [PATCH 1/3] Add SpanProcessor for OpenTelemetry --- config/config.exs | 5 + lib/sentry/application.ex | 1 + lib/sentry/opentelemetry/sampler.ex | 27 ++ lib/sentry/opentelemetry/span_processor.ex | 199 ++++++++++ lib/sentry/opentelemetry/span_record.ex | 72 ++++ lib/sentry/opentelemetry/span_storage.ex | 132 +++++++ mix.exs | 13 +- mix.lock | 11 + test/event_test.exs | 2 +- test/sentry/opentelemetry/sampler_test.exs | 19 + .../opentelemetry/span_processor_test.exs | 126 ++++++ .../opentelemetry/span_storage_test.exs | 361 ++++++++++++++++++ test_integrations/phoenix_app/mix.exs | 7 +- test_integrations/phoenix_app/mix.lock | 11 + test_integrations/umbrella/mix.lock | 5 +- 15 files changed, 984 insertions(+), 7 deletions(-) create mode 100644 lib/sentry/opentelemetry/sampler.ex create mode 100644 lib/sentry/opentelemetry/span_processor.ex create mode 100644 lib/sentry/opentelemetry/span_record.ex create mode 100644 lib/sentry/opentelemetry/span_storage.ex create mode 100644 test/sentry/opentelemetry/sampler_test.exs create mode 100644 test/sentry/opentelemetry/span_processor_test.exs create mode 100644 test/sentry/opentelemetry/span_storage_test.exs diff --git a/config/config.exs b/config/config.exs index d63cc9d2..f11974e1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -16,3 +16,8 @@ if config_env() == :test do end config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason) + +config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []} + +config :opentelemetry, + sampler: {Sentry.OpenTelemetry.Sampler, [drop: ["Elixir.Oban.Stager process"]]} diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index 8f373728..ae2bd121 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -32,6 +32,7 @@ defmodule Sentry.Application do Sentry.Sources, Sentry.Dedupe, Sentry.ClientReport.Sender, + Sentry.OpenTelemetry.SpanStorage, {Sentry.Integrations.CheckInIDMappings, [ max_expected_check_in_time: diff --git a/lib/sentry/opentelemetry/sampler.ex b/lib/sentry/opentelemetry/sampler.ex new file mode 100644 index 00000000..7581e090 --- /dev/null +++ b/lib/sentry/opentelemetry/sampler.ex @@ -0,0 +1,27 @@ +defmodule Sentry.OpenTelemetry.Sampler do + @moduledoc false + + def setup(config) do + config + end + + def description(_) do + "SentrySampler" + end + + def should_sample( + _ctx, + _trace_id, + _links, + span_name, + _span_kind, + _attributes, + config + ) do + if span_name in config[:drop] do + {:drop, [], []} + else + {:record_and_sample, [], []} + end + end +end diff --git a/lib/sentry/opentelemetry/span_processor.ex b/lib/sentry/opentelemetry/span_processor.ex new file mode 100644 index 00000000..02f556e9 --- /dev/null +++ b/lib/sentry/opentelemetry/span_processor.ex @@ -0,0 +1,199 @@ +if Code.ensure_loaded?(OpenTelemetry) do + defmodule Sentry.OpenTelemetry.SpanProcessor do + @moduledoc false + + require OpenTelemetry.SemConv.ClientAttributes, as: ClientAttributes + require OpenTelemetry.SemConv.Incubating.DBAttributes, as: DBAttributes + require OpenTelemetry.SemConv.Incubating.HTTPAttributes, as: HTTPAttributes + require OpenTelemetry.SemConv.Incubating.URLAttributes, as: URLAttributes + require OpenTelemetry.SemConv.Incubating.MessagingAttributes, as: MessagingAttributes + @behaviour :otel_span_processor + + require Logger + + alias Sentry.{Transaction, OpenTelemetry.SpanStorage, OpenTelemetry.SpanRecord} + alias Sentry.Interfaces.Span + + @impl true + def on_start(_ctx, otel_span, _config) do + span_record = SpanRecord.new(otel_span) + + SpanStorage.store_span(span_record) + + otel_span + end + + @impl true + def on_end(otel_span, _config) do + span_record = SpanRecord.new(otel_span) + + SpanStorage.update_span(span_record) + + if span_record.parent_span_id == nil do + root_span_record = SpanStorage.get_root_span(span_record.span_id) + child_span_records = SpanStorage.get_child_spans(span_record.span_id) + transaction = build_transaction(root_span_record, child_span_records) + + result = + case Sentry.send_transaction(transaction) do + {:ok, _id} -> + true + + :ignored -> + true + + {:error, error} -> + Logger.error("Failed to send transaction to Sentry: #{inspect(error)}") + {:error, :invalid_span} + end + + :ok = SpanStorage.remove_span(span_record.span_id) + + result + else + true + end + end + + @impl true + def force_flush(_config) do + :ok + end + + defp build_transaction(root_span_record, child_span_records) do + root_span = build_span(root_span_record) + child_spans = Enum.map(child_span_records, &build_span(&1)) + + Transaction.new(%{ + span_id: root_span.span_id, + transaction: transaction_name(root_span_record), + transaction_info: %{source: :custom}, + start_timestamp: root_span_record.start_time, + timestamp: root_span_record.end_time, + contexts: %{ + trace: build_trace_context(root_span_record), + otel: build_otel_context(root_span_record) + }, + spans: child_spans + }) + end + + defp transaction_name( + %{attributes: %{unquote(to_string(MessagingAttributes.messaging_system())) => :oban}} = + span_record + ) do + span_record.attributes["oban.job.worker"] + end + + defp transaction_name(span_record), do: span_record.name + + defp build_trace_context(span_record) do + {op, description} = get_op_description(span_record) + + %{ + trace_id: span_record.trace_id, + span_id: span_record.span_id, + parent_span_id: span_record.parent_span_id, + op: op, + description: description, + origin: span_record.origin, + data: span_record.attributes + } + end + + defp build_otel_context(span_record), do: span_record.attributes + + defp get_op_description( + %{ + attributes: %{ + unquote(to_string(HTTPAttributes.http_request_method())) => http_request_method + } + } = span_record + ) do + op = "http.#{span_record.kind}" + + client_address = + Map.get(span_record.attributes, to_string(ClientAttributes.client_address())) + + url_path = Map.get(span_record.attributes, to_string(URLAttributes.url_path())) + + description = + to_string(http_request_method) <> + ((client_address && " from #{client_address}") || "") <> + ((url_path && " #{url_path}") || "") + + {op, description} + end + + defp get_op_description( + %{attributes: %{unquote(to_string(DBAttributes.db_system())) => _db_system}} = + span_record + ) do + db_query_text = Map.get(span_record.attributes, "db.statement") + + {"db", db_query_text} + end + + defp get_op_description(%{ + attributes: + %{unquote(to_string(MessagingAttributes.messaging_system())) => :oban} = attributes + }) do + {"queue.process", attributes["oban.job.worker"]} + end + + defp get_op_description(span_record) do + {span_record.name, span_record.name} + end + + defp build_span(span_record) do + {op, description} = get_op_description(span_record) + + %Span{ + op: op, + description: description, + start_timestamp: span_record.start_time, + timestamp: span_record.end_time, + trace_id: span_record.trace_id, + span_id: span_record.span_id, + parent_span_id: span_record.parent_span_id, + origin: span_record.origin, + data: Map.put(span_record.attributes, "otel.kind", span_record.kind), + status: span_status(span_record) + } + end + + defp span_status(%{ + attributes: %{ + unquote(to_string(HTTPAttributes.http_response_status_code())) => + http_response_status_code + } + }) do + to_status(http_response_status_code) + end + + defp span_status(_span_record), do: nil + + # WebSocket upgrade spans doesn't have a HTTP status + defp to_status(nil), do: nil + + defp to_status(status) when status in 200..299, do: "ok" + + for {status, string} <- %{ + 400 => "invalid_argument", + 401 => "unauthenticated", + 403 => "permission_denied", + 404 => "not_found", + 409 => "already_exists", + 429 => "resource_exhausted", + 499 => "cancelled", + 500 => "internal_error", + 501 => "unimplemented", + 503 => "unavailable", + 504 => "deadline_exceeded" + } do + defp to_status(unquote(status)), do: unquote(string) + end + + defp to_status(_any), do: "unknown_error" + end +end diff --git a/lib/sentry/opentelemetry/span_record.ex b/lib/sentry/opentelemetry/span_record.ex new file mode 100644 index 00000000..0bfdae38 --- /dev/null +++ b/lib/sentry/opentelemetry/span_record.ex @@ -0,0 +1,72 @@ +if Code.ensure_loaded?(OpenTelemetry) do + defmodule Sentry.OpenTelemetry.SpanRecord do + require Record + require OpenTelemetry + + @fields Record.extract(:span, from_lib: "opentelemetry/include/otel_span.hrl") + Record.defrecordp(:span, @fields) + + defstruct @fields ++ [:origin] + + def new(otel_span) do + otel_attrs = span(otel_span) + + {:attributes, _, _, _, attributes} = otel_attrs[:attributes] + + origin = + case otel_attrs[:instrumentation_scope] do + {:instrumentation_scope, origin, _version, _} -> + origin + + _ -> + :undefined + end + + attrs = + otel_attrs + |> Keyword.delete(:attributes) + |> Keyword.merge( + trace_id: cast_trace_id(otel_attrs[:trace_id]), + span_id: cast_span_id(otel_attrs[:span_id]), + parent_span_id: cast_span_id(otel_attrs[:parent_span_id]), + origin: origin, + start_time: cast_timestamp(otel_attrs[:start_time]), + end_time: cast_timestamp(otel_attrs[:end_time]), + attributes: normalize_attributes(attributes) + ) + |> Map.new() + + struct(__MODULE__, attrs) + end + + defp normalize_attributes(attributes) do + Enum.map(attributes, fn {key, value} -> + {to_string(key), value} + end) + |> Map.new() + end + + defp cast_span_id(nil), do: nil + defp cast_span_id(:undefined), do: nil + defp cast_span_id(span_id), do: bytes_to_hex(span_id, 16) + + defp cast_trace_id(trace_id), do: bytes_to_hex(trace_id, 32) + + defp cast_timestamp(:undefined), do: nil + defp cast_timestamp(nil), do: nil + + defp cast_timestamp(timestamp) do + nano_timestamp = OpenTelemetry.timestamp_to_nano(timestamp) + {:ok, datetime} = DateTime.from_unix(div(nano_timestamp, 1_000_000), :millisecond) + + DateTime.to_iso8601(datetime) + end + + defp bytes_to_hex(bytes, length) do + case(:otel_utils.format_binary_string("~#{length}.16.0b", [bytes])) do + {:ok, result} -> result + {:error, _} -> raise "Failed to convert bytes to hex: #{inspect(bytes)}" + end + end + end +end diff --git a/lib/sentry/opentelemetry/span_storage.ex b/lib/sentry/opentelemetry/span_storage.ex new file mode 100644 index 00000000..dcd95111 --- /dev/null +++ b/lib/sentry/opentelemetry/span_storage.ex @@ -0,0 +1,132 @@ +defmodule Sentry.OpenTelemetry.SpanStorage do + @moduledoc false + use GenServer + + @table :span_storage + @cleanup_interval :timer.minutes(5) + @span_ttl :timer.minutes(30) + + @spec start_link(keyword()) :: GenServer.on_start() + def start_link(opts \\ []) do + name = Keyword.get(opts, :name, __MODULE__) + GenServer.start_link(__MODULE__, opts, name: name) + end + + @impl true + def init(opts) do + _table = + if :ets.whereis(@table) == :undefined do + :ets.new(@table, [:named_table, :public, :bag]) + end + + cleanup_interval = Keyword.get(opts, :cleanup_interval, @cleanup_interval) + schedule_cleanup(cleanup_interval) + + {:ok, %{cleanup_interval: cleanup_interval}} + end + + def store_span(span_data) when span_data.parent_span_id == nil do + stored_at = System.system_time(:second) + + case :ets.lookup(@table, {:root_span, span_data.span_id}) do + [] -> :ets.insert(@table, {{:root_span, span_data.span_id}, {span_data, stored_at}}) + _ -> :ok + end + end + + def store_span(span_data) do + stored_at = System.system_time(:second) + _ = :ets.insert(@table, {span_data.parent_span_id, {span_data, stored_at}}) + end + + def get_root_span(span_id) do + case :ets.lookup(@table, {:root_span, span_id}) do + [{{:root_span, ^span_id}, {span, _stored_at}}] -> span + [] -> nil + end + end + + def get_child_spans(parent_span_id) do + :ets.lookup(@table, parent_span_id) + |> Enum.map(fn {_parent_id, {span, _stored_at}} -> span end) + end + + def update_span(span_data) do + stored_at = System.system_time(:second) + + if span_data.parent_span_id == nil do + case :ets.lookup(@table, {:root_span, span_data.span_id}) do + [] -> + :ets.insert(@table, {{:root_span, span_data.span_id}, {span_data, stored_at}}) + + _ -> + :ets.delete(@table, {:root_span, span_data.span_id}) + :ets.insert(@table, {{:root_span, span_data.span_id}, {span_data, stored_at}}) + end + else + existing_spans = :ets.lookup(@table, span_data.parent_span_id) + + Enum.each(existing_spans, fn {parent_id, {span, stored_at}} -> + if span.span_id == span_data.span_id do + :ets.delete_object(@table, {parent_id, {span, stored_at}}) + :ets.insert(@table, {span_data.parent_span_id, {span_data, stored_at}}) + end + end) + end + + :ok + end + + def remove_span(span_id) do + case get_root_span(span_id) do + nil -> + :ok + + _root_span -> + :ets.delete(@table, {:root_span, span_id}) + remove_child_spans(span_id) + end + end + + def remove_child_spans(parent_span_id) do + :ets.delete(@table, parent_span_id) + :ok + end + + @impl true + def handle_info(:cleanup_stale_spans, state) do + cleanup_stale_spans() + schedule_cleanup(state.cleanup_interval) + {:noreply, state} + end + + defp schedule_cleanup(interval) do + Process.send_after(self(), :cleanup_stale_spans, interval) + end + + defp cleanup_stale_spans do + now = System.system_time(:second) + cutoff_time = now - @span_ttl + + :ets.match_object(@table, {{:root_span, :_}, {:_, :_}}) + |> Enum.each(fn {{:root_span, span_id}, {_span, stored_at}} -> + if stored_at < cutoff_time do + remove_span(span_id) + end + end) + + :ets.match_object(@table, {:_, {:_, :_}}) + |> Enum.each(fn {parent_id, {_span, stored_at}} = object -> + cond do + not is_nil(get_root_span(parent_id)) and stored_at < cutoff_time -> + :ets.delete_object(@table, object) + + is_nil(get_root_span(parent_id)) and stored_at < cutoff_time -> + :ets.delete_object(@table, object) + + true -> + :ok + end + end) + end +end diff --git a/mix.exs b/mix.exs index aa2ab7fe..f5878e66 100644 --- a/mix.exs +++ b/mix.exs @@ -73,7 +73,7 @@ defmodule Sentry.Mixfile do def application do [ mod: {Sentry.Application, []}, - extra_applications: [:logger], + extra_applications: extra_applications(Mix.env()), registered: [ Sentry.Dedupe, Sentry.Transport.SenderRegistry, @@ -82,13 +82,16 @@ defmodule Sentry.Mixfile do ] end + defp extra_applications(:test), do: [:logger, :opentelemetry] + defp extra_applications(_other), do: [:logger] + defp elixirc_paths(:test), do: ["test/support"] ++ elixirc_paths(:dev) defp elixirc_paths(_other), do: ["lib"] defp test_paths(nil), do: ["test"] defp test_paths(integration), do: ["test_integrations/#{integration}/test"] - defp deps do + defp deps() do [ {:nimble_options, "~> 1.0"}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0"}, @@ -111,7 +114,11 @@ defmodule Sentry.Mixfile do # Required by Phoenix.LiveView's testing {:floki, ">= 0.30.0", only: :test}, {:oban, "~> 2.17 and >= 2.17.6", only: [:test]}, - {:quantum, "~> 3.0", only: [:test]} + {:quantum, "~> 3.0", only: [:test]}, + {:opentelemetry, "~> 1.5", optional: true}, + {:opentelemetry_api, "~> 1.4", optional: true}, + {:opentelemetry_exporter, "~> 1.0", optional: true}, + {:opentelemetry_semantic_conventions, "~> 1.27", optional: true} ] end diff --git a/mix.lock b/mix.lock index 4d7db4c2..c7e33221 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,15 @@ %{ + "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, "bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"}, + "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, @@ -18,7 +21,10 @@ "excoveralls": {:hex, :excoveralls, "0.17.1", "83fa7906ef23aa7fc8ad7ee469c357a63b1b3d55dd701ff5b9ce1f72442b2874", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "95bc6fda953e84c60f14da4a198880336205464e75383ec0f570180567985ae0"}, "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, + "gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"}, + "grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, "hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -32,6 +38,10 @@ "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"}, + "opentelemetry": {:hex, :opentelemetry, "1.5.0", "7dda6551edfc3050ea4b0b40c0d2570423d6372b97e9c60793263ef62c53c3c2", [:rebar3], [{:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "cdf4f51d17b592fc592b9a75f86a6f808c23044ba7cf7b9534debbcc5c23b0ee"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.4.0", "63ca1742f92f00059298f478048dfb826f4b20d49534493d6919a0db39b6db04", [:mix, :rebar3], [], "hexpm", "3dfbbfaa2c2ed3121c5c483162836c4f9027def469c41578af5ef32589fcfc58"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.8.0", "5d546123230771ef4174e37bedfd77e3374913304cd6ea3ca82a2add49cd5d56", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.5.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "a1f9f271f8d3b02b81462a6bfef7075fd8457fdb06adff5d2537df5e2264d9af"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, @@ -47,6 +57,7 @@ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.27.0", "2c1c7fc922a329b9eb45ddf39113c998bbdeb28a534219cd884431e2aee1811e", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "51a5ad3dbd72d4694848965f3b5076e8b55d70eb8d5057fcddd536029ab8a23c"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, diff --git a/test/event_test.exs b/test/event_test.exs index a0fdec9c..79b3b9ae 100644 --- a/test/event_test.exs +++ b/test/event_test.exs @@ -475,7 +475,7 @@ defmodule Sentry.EventTest do exception = RuntimeError.exception("error") event = Sentry.Event.transform_exception(exception, []) - assert ["asn1", "bandit", "bypass" | _rest] = + assert ["acceptor_pool", "asn1", "bandit", "bypass" | _rest] = event.modules |> Map.keys() |> Enum.sort() diff --git a/test/sentry/opentelemetry/sampler_test.exs b/test/sentry/opentelemetry/sampler_test.exs new file mode 100644 index 00000000..026f432c --- /dev/null +++ b/test/sentry/opentelemetry/sampler_test.exs @@ -0,0 +1,19 @@ +defmodule Sentry.Opentelemetry.SamplerTest do + use Sentry.Case, async: false + + alias Sentry.OpenTelemetry.Sampler + + test "drops spans with the given name" do + assert {:drop, [], []} = + Sampler.should_sample(nil, nil, nil, "Elixir.Oban.Stager process", nil, nil, + drop: ["Elixir.Oban.Stager process"] + ) + end + + test "records and samples spans with the given name" do + assert {:record_and_sample, [], []} = + Sampler.should_sample(nil, nil, nil, "Elixir.Oban.Worker process", nil, nil, + drop: [] + ) + end +end diff --git a/test/sentry/opentelemetry/span_processor_test.exs b/test/sentry/opentelemetry/span_processor_test.exs new file mode 100644 index 00000000..5b170ca6 --- /dev/null +++ b/test/sentry/opentelemetry/span_processor_test.exs @@ -0,0 +1,126 @@ +defmodule Sentry.Opentelemetry.SpanProcessorTest do + use Sentry.Case, async: false + + import Sentry.TestHelpers + + alias Sentry.OpenTelemetry.SpanStorage + + setup do + on_exit(fn -> + # Only try to clean up tables if they exist + if :ets.whereis(:span_storage) != :undefined do + :ets.delete_all_objects(:span_storage) + end + end) + + :ok + end + + defmodule TestEndpoint do + require OpenTelemetry.Tracer, as: Tracer + + def instrumented_function do + Tracer.with_span "instrumented_function" do + :timer.sleep(100) + + child_instrumented_function("one") + child_instrumented_function("two") + end + end + + def child_instrumented_function(name) do + Tracer.with_span "child_instrumented_function_#{name}" do + :timer.sleep(140) + end + end + end + + test "sends captured root spans as transactions" do + put_test_config(environment_name: "test") + + Sentry.Test.start_collecting_sentry_reports() + + TestEndpoint.child_instrumented_function("one") + + assert [%Sentry.Transaction{} = transaction] = Sentry.Test.pop_sentry_transactions() + + assert transaction.event_id + assert transaction.environment == "test" + assert transaction.transaction_info == %{source: :custom} + assert_valid_iso8601(transaction.timestamp) + assert_valid_iso8601(transaction.start_timestamp) + assert transaction.timestamp > transaction.start_timestamp + assert_valid_trace_id(transaction.contexts.trace.trace_id) + assert length(transaction.spans) == 0 + end + + test "sends captured spans as transactions with child spans" do + put_test_config(environment_name: "test") + + Sentry.Test.start_collecting_sentry_reports() + + TestEndpoint.instrumented_function() + + assert [%Sentry.Transaction{} = transaction] = Sentry.Test.pop_sentry_transactions() + + assert_valid_iso8601(transaction.timestamp) + assert_valid_iso8601(transaction.start_timestamp) + assert transaction.timestamp > transaction.start_timestamp + assert length(transaction.spans) == 2 + + [child_span_one, child_span_two] = transaction.spans + assert child_span_one.op == "child_instrumented_function_one" + assert child_span_two.op == "child_instrumented_function_two" + assert child_span_one.parent_span_id == transaction.contexts.trace.span_id + assert child_span_two.parent_span_id == transaction.contexts.trace.span_id + + assert_valid_iso8601(child_span_one.timestamp) + assert_valid_iso8601(child_span_one.start_timestamp) + assert_valid_iso8601(child_span_two.timestamp) + assert_valid_iso8601(child_span_two.start_timestamp) + + assert child_span_one.timestamp > child_span_one.start_timestamp + assert child_span_two.timestamp > child_span_two.start_timestamp + assert transaction.timestamp >= child_span_one.timestamp + assert transaction.timestamp >= child_span_two.timestamp + assert transaction.start_timestamp <= child_span_one.start_timestamp + assert transaction.start_timestamp <= child_span_two.start_timestamp + + assert_valid_trace_id(transaction.contexts.trace.trace_id) + assert_valid_trace_id(child_span_one.trace_id) + assert_valid_trace_id(child_span_two.trace_id) + end + + test "removes span records from storage after sending a transaction" do + put_test_config(environment_name: "test") + + Sentry.Test.start_collecting_sentry_reports() + + TestEndpoint.instrumented_function() + + assert [%Sentry.Transaction{} = transaction] = Sentry.Test.pop_sentry_transactions() + + assert nil == SpanStorage.get_root_span(transaction.contexts.trace.span_id) + assert [] == SpanStorage.get_child_spans(transaction.contexts.trace.span_id) + end + + defp assert_valid_iso8601(timestamp) do + case DateTime.from_iso8601(timestamp) do + {:ok, datetime, _offset} -> + assert datetime.year >= 2023, "Expected year to be 2023 or later, got: #{datetime.year}" + assert is_binary(timestamp), "Expected timestamp to be a string" + assert String.ends_with?(timestamp, "Z"), "Expected timestamp to end with 'Z'" + + {:error, reason} -> + flunk("Invalid ISO8601 timestamp: #{timestamp}, reason: #{inspect(reason)}") + end + end + + defp assert_valid_trace_id(trace_id) do + assert is_binary(trace_id), "Expected trace_id to be a string" + assert String.length(trace_id) == 32, "Expected trace_id to be 32 characters long #{trace_id}" + + assert String.match?(trace_id, ~r/^[a-f0-9]{32}$/), + "Expected trace_id to be a lowercase hex string" + end +end diff --git a/test/sentry/opentelemetry/span_storage_test.exs b/test/sentry/opentelemetry/span_storage_test.exs new file mode 100644 index 00000000..df9f3ec7 --- /dev/null +++ b/test/sentry/opentelemetry/span_storage_test.exs @@ -0,0 +1,361 @@ +defmodule Sentry.OpenTelemetry.SpanStorageTest do + use ExUnit.Case, async: false + + alias Sentry.OpenTelemetry.{SpanStorage, SpanRecord} + + setup do + if :ets.whereis(:span_storage) != :undefined do + :ets.delete_all_objects(:span_storage) + else + start_supervised!(SpanStorage) + end + + on_exit(fn -> + if :ets.whereis(:span_storage) != :undefined do + :ets.delete_all_objects(:span_storage) + end + end) + + :ok + end + + describe "root spans" do + test "stores and retrieves a root span" do + root_span = %SpanRecord{ + span_id: "abc123", + parent_span_id: nil, + trace_id: "trace123", + name: "root_span" + } + + SpanStorage.store_span(root_span) + + assert ^root_span = SpanStorage.get_root_span("abc123") + end + + test "updates an existing root span" do + root_span = %SpanRecord{ + span_id: "abc123", + parent_span_id: nil, + trace_id: "trace123", + name: "root_span" + } + + updated_root_span = %SpanRecord{ + span_id: "abc123", + parent_span_id: nil, + trace_id: "trace123", + name: "updated_root_span" + } + + SpanStorage.store_span(root_span) + SpanStorage.update_span(updated_root_span) + + assert ^updated_root_span = SpanStorage.get_root_span("abc123") + end + + test "removes a root span" do + root_span = %SpanRecord{ + span_id: "abc123", + parent_span_id: nil, + trace_id: "trace123", + name: "root_span" + } + + SpanStorage.store_span(root_span) + assert root_span == SpanStorage.get_root_span("abc123") + + SpanStorage.remove_span("abc123") + assert nil == SpanStorage.get_root_span("abc123") + end + + test "removes root span and all its children" do + root_span = %SpanRecord{ + span_id: "root123", + parent_span_id: nil, + trace_id: "trace123", + name: "root_span" + } + + child_span1 = %SpanRecord{ + span_id: "child1", + parent_span_id: "root123", + trace_id: "trace123", + name: "child_span_1" + } + + child_span2 = %SpanRecord{ + span_id: "child2", + parent_span_id: "root123", + trace_id: "trace123", + name: "child_span_2" + } + + SpanStorage.store_span(root_span) + SpanStorage.store_span(child_span1) + SpanStorage.store_span(child_span2) + + assert root_span == SpanStorage.get_root_span("root123") + assert length(SpanStorage.get_child_spans("root123")) == 2 + + SpanStorage.remove_span("root123") + + assert nil == SpanStorage.get_root_span("root123") + assert [] == SpanStorage.get_child_spans("root123") + end + end + + describe "child spans" do + test "stores and retrieves child spans" do + child_span1 = %SpanRecord{ + span_id: "child1", + parent_span_id: "parent123", + trace_id: "trace123", + name: "child_span_1" + } + + child_span2 = %SpanRecord{ + span_id: "child2", + parent_span_id: "parent123", + trace_id: "trace123", + name: "child_span_2" + } + + SpanStorage.store_span(child_span1) + SpanStorage.store_span(child_span2) + + children = SpanStorage.get_child_spans("parent123") + assert length(children) == 2 + assert child_span1 in children + assert child_span2 in children + end + + test "updates an existing child span" do + child_span = %SpanRecord{ + span_id: "child1", + parent_span_id: "parent123", + trace_id: "trace123", + name: "child_span" + } + + updated_child_span = %SpanRecord{ + span_id: "child1", + parent_span_id: "parent123", + trace_id: "trace123", + name: "updated_child_span" + } + + SpanStorage.store_span(child_span) + SpanStorage.update_span(updated_child_span) + + children = SpanStorage.get_child_spans("parent123") + assert [^updated_child_span] = children + end + + test "removes child spans" do + child_span1 = %SpanRecord{ + span_id: "child1", + parent_span_id: "parent123", + trace_id: "trace123", + name: "child_span_1" + } + + child_span2 = %SpanRecord{ + span_id: "child2", + parent_span_id: "parent123", + trace_id: "trace123", + name: "child_span_2" + } + + SpanStorage.store_span(child_span1) + SpanStorage.store_span(child_span2) + assert length(SpanStorage.get_child_spans("parent123")) == 2 + + SpanStorage.remove_child_spans("parent123") + assert [] == SpanStorage.get_child_spans("parent123") + end + end + + test "handles complete span hierarchy" do + root_span = %SpanRecord{ + span_id: "root123", + parent_span_id: nil, + trace_id: "trace123", + name: "root_span" + } + + child_span1 = %SpanRecord{ + span_id: "child1", + parent_span_id: "root123", + trace_id: "trace123", + name: "child_span_1" + } + + child_span2 = %SpanRecord{ + span_id: "child2", + parent_span_id: "root123", + trace_id: "trace123", + name: "child_span_2" + } + + SpanStorage.store_span(root_span) + SpanStorage.store_span(child_span1) + SpanStorage.store_span(child_span2) + + assert ^root_span = SpanStorage.get_root_span("root123") + + children = SpanStorage.get_child_spans("root123") + assert length(children) == 2 + assert child_span1 in children + assert child_span2 in children + + SpanStorage.remove_span("root123") + SpanStorage.remove_child_spans("root123") + + assert nil == SpanStorage.get_root_span("root123") + assert [] == SpanStorage.get_child_spans("root123") + end + + describe "stale span cleanup" do + test "cleans up stale spans" do + start_supervised!({SpanStorage, cleanup_interval: 100, name: :cleanup_test}) + + root_span = %SpanRecord{ + span_id: "stale_root", + parent_span_id: nil, + trace_id: "trace123", + name: "stale_root_span" + } + + child_span = %SpanRecord{ + span_id: "stale_child", + parent_span_id: "stale_root", + trace_id: "trace123", + name: "stale_child_span" + } + + old_time = System.system_time(:second) - :timer.minutes(31) + :ets.insert(:span_storage, {{:root_span, "stale_root"}, {root_span, old_time}}) + :ets.insert(:span_storage, {"stale_root", {child_span, old_time}}) + + fresh_root_span = %SpanRecord{ + span_id: "fresh_root", + parent_span_id: nil, + trace_id: "trace123", + name: "fresh_root_span" + } + + SpanStorage.store_span(fresh_root_span) + + Process.sleep(200) + + assert nil == SpanStorage.get_root_span("stale_root") + assert [] == SpanStorage.get_child_spans("stale_root") + + assert SpanStorage.get_root_span("fresh_root") + end + + test "cleans up orphaned child spans" do + start_supervised!({SpanStorage, cleanup_interval: 100, name: :cleanup_test}) + + child_span = %SpanRecord{ + span_id: "stale_child", + parent_span_id: "non_existent_parent", + trace_id: "trace123", + name: "stale_child_span" + } + + old_time = System.system_time(:second) - :timer.minutes(31) + :ets.insert(:span_storage, {"non_existent_parent", {child_span, old_time}}) + + Process.sleep(200) + + assert [] == SpanStorage.get_child_spans("non_existent_parent") + end + + test "cleans up expired root spans with all their children regardless of child timestamps" do + start_supervised!({SpanStorage, cleanup_interval: 100, name: :cleanup_test}) + + root_span = %SpanRecord{ + span_id: "root123", + parent_span_id: nil, + trace_id: "trace123", + name: "root_span" + } + + old_child = %SpanRecord{ + span_id: "old_child", + parent_span_id: "root123", + trace_id: "trace123", + name: "old_child_span" + } + + fresh_child = %SpanRecord{ + span_id: "fresh_child", + parent_span_id: "root123", + trace_id: "trace123", + name: "fresh_child_span" + } + + old_time = System.system_time(:second) - :timer.minutes(31) + :ets.insert(:span_storage, {{:root_span, "root123"}, {root_span, old_time}}) + + :ets.insert(:span_storage, {"root123", {old_child, old_time}}) + SpanStorage.store_span(fresh_child) + + Process.sleep(200) + + assert nil == SpanStorage.get_root_span("root123") + assert [] == SpanStorage.get_child_spans("root123") + end + + test "handles mixed expiration times in child spans" do + start_supervised!({SpanStorage, cleanup_interval: 100, name: :cleanup_test}) + + root_span = %SpanRecord{ + span_id: "root123", + parent_span_id: nil, + trace_id: "trace123", + name: "root_span" + } + + old_child1 = %SpanRecord{ + span_id: "old_child1", + parent_span_id: "root123", + trace_id: "trace123", + name: "old_child_span_1" + } + + old_child2 = %SpanRecord{ + span_id: "old_child2", + parent_span_id: "root123", + trace_id: "trace123", + name: "old_child_span_2" + } + + fresh_child = %SpanRecord{ + span_id: "fresh_child", + parent_span_id: "root123", + trace_id: "trace123", + name: "fresh_child_span" + } + + SpanStorage.store_span(root_span) + + old_time = System.system_time(:second) - :timer.minutes(31) + :ets.insert(:span_storage, {"root123", {old_child1, old_time}}) + :ets.insert(:span_storage, {"root123", {old_child2, old_time}}) + + SpanStorage.store_span(fresh_child) + + Process.sleep(200) + + assert root_span == SpanStorage.get_root_span("root123") + children = SpanStorage.get_child_spans("root123") + assert length(children) == 1 + assert fresh_child in children + refute old_child1 in children + refute old_child2 in children + end + end +end diff --git a/test_integrations/phoenix_app/mix.exs b/test_integrations/phoenix_app/mix.exs index 2055e414..54e1e0b8 100644 --- a/test_integrations/phoenix_app/mix.exs +++ b/test_integrations/phoenix_app/mix.exs @@ -55,7 +55,12 @@ defmodule PhoenixApp.MixProject do {:bypass, "~> 2.1", only: :test}, {:hackney, "~> 1.18", only: :test}, - {:sentry, path: "../.."} + {:sentry, path: "../.."}, + + {:opentelemetry, "~> 1.5", optional: true}, + {:opentelemetry_api, "~> 1.4", optional: true}, + {:opentelemetry_exporter, "~> 1.0", optional: true}, + {:opentelemetry_semantic_conventions, "~> 1.27", optional: true} ] end diff --git a/test_integrations/phoenix_app/mix.lock b/test_integrations/phoenix_app/mix.lock index a14316ee..11d417cf 100644 --- a/test_integrations/phoenix_app/mix.lock +++ b/test_integrations/phoenix_app/mix.lock @@ -1,11 +1,14 @@ %{ + "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, "bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, @@ -15,7 +18,10 @@ "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, + "gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"}, + "grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"}, "hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -26,6 +32,10 @@ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "opentelemetry": {:hex, :opentelemetry, "1.5.0", "7dda6551edfc3050ea4b0b40c0d2570423d6372b97e9c60793263ef62c53c3c2", [:rebar3], [{:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "cdf4f51d17b592fc592b9a75f86a6f808c23044ba7cf7b9534debbcc5c23b0ee"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.4.0", "63ca1742f92f00059298f478048dfb826f4b20d49534493d6919a0db39b6db04", [:mix, :rebar3], [], "hexpm", "3dfbbfaa2c2ed3121c5c483162836c4f9027def469c41578af5ef32589fcfc58"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.8.0", "5d546123230771ef4174e37bedfd77e3374913304cd6ea3ca82a2add49cd5d56", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.5.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "a1f9f271f8d3b02b81462a6bfef7075fd8457fdb06adff5d2537df5e2264d9af"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, @@ -46,6 +56,7 @@ "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.27.0", "2c1c7fc922a329b9eb45ddf39113c998bbdeb28a534219cd884431e2aee1811e", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "51a5ad3dbd72d4694848965f3b5076e8b55d70eb8d5057fcddd536029ab8a23c"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, diff --git a/test_integrations/umbrella/mix.lock b/test_integrations/umbrella/mix.lock index ff9454dd..4aea5a19 100644 --- a/test_integrations/umbrella/mix.lock +++ b/test_integrations/umbrella/mix.lock @@ -1,7 +1,8 @@ %{ - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, + "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, From 5714b5f61bcff4f14ce8935c593fcc389bd5c4a3 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 19 Mar 2025 11:21:00 +0000 Subject: [PATCH 2/3] Move opentelemetry config to test env --- config/config.exs | 10 +++++----- test_integrations/phoenix_app/config/test.exs | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index f11974e1..45250236 100644 --- a/config/config.exs +++ b/config/config.exs @@ -13,11 +13,11 @@ if config_env() == :test do test_mode: true config :logger, backends: [] -end -config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason) + config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []} -config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []} + config :opentelemetry, + sampler: {Sentry.OpenTelemetry.Sampler, [drop: ["Elixir.Oban.Stager process"]]} +end -config :opentelemetry, - sampler: {Sentry.OpenTelemetry.Sampler, [drop: ["Elixir.Oban.Stager process"]]} +config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason) diff --git a/test_integrations/phoenix_app/config/test.exs b/test_integrations/phoenix_app/config/test.exs index 207b9cf2..f845417b 100644 --- a/test_integrations/phoenix_app/config/test.exs +++ b/test_integrations/phoenix_app/config/test.exs @@ -30,3 +30,5 @@ config :sentry, root_source_code_paths: [File.cwd!()], test_mode: true, send_result: :sync + +config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []} From bdcc5de53cdbdad2f8cd063f995a1ec75cde0676 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 19 Mar 2025 12:38:59 +0000 Subject: [PATCH 3/3] Add tracing config option --- config/config.exs | 3 ++- lib/sentry/application.ex | 9 ++++++++- lib/sentry/config.ex | 14 ++++++++++++++ test/sentry/config_test.exs | 9 +++++++++ test/sentry/opentelemetry/span_processor_test.exs | 6 +++--- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index 45250236..ccc26bf1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,7 +10,8 @@ if config_env() == :test do send_result: :sync, send_max_attempts: 1, dedup_events: false, - test_mode: true + test_mode: true, + tracing: true config :logger, backends: [] diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index ae2bd121..0bca688c 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -26,13 +26,19 @@ defmodule Sentry.Application do integrations_config = Config.integrations() + maybe_span_storage = + if Config.tracing?() do + [Sentry.OpenTelemetry.SpanStorage] + else + [] + end + children = [ {Registry, keys: :unique, name: Sentry.Transport.SenderRegistry}, Sentry.Sources, Sentry.Dedupe, Sentry.ClientReport.Sender, - Sentry.OpenTelemetry.SpanStorage, {Sentry.Integrations.CheckInIDMappings, [ max_expected_check_in_time: @@ -40,6 +46,7 @@ defmodule Sentry.Application do ]} ] ++ maybe_http_client_spec ++ + maybe_span_storage ++ [Sentry.Transport.SenderPool] cache_loaded_applications() diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index bb66ba83..4ac43022 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -125,6 +125,17 @@ defmodule Sentry.Config do be used as the value for this option. """ ], + tracing: [ + type: :boolean, + default: false, + doc: """ + Whether to enable tracing functionality based on OpenTelemetry. When enabled, + the Sentry SDK will use OpenTelemetry to collect and report distributed tracing + data to Sentry. + + This feature requires `opentelemetry` package and its integrations with Bandit, Phoenix or Ecto. + """ + ], included_environments: [ type: {:or, [{:in, [:all]}, {:list, {:or, [:atom, :string]}}]}, deprecated: "Use :dsn to control whether to send events to Sentry.", @@ -627,6 +638,9 @@ defmodule Sentry.Config do @spec integrations() :: keyword() def integrations, do: fetch!(:integrations) + @spec tracing?() :: boolean() + def tracing?, do: fetch!(:tracing) + @spec put_config(atom(), term()) :: :ok def put_config(key, value) when is_atom(key) do unless key in @valid_keys do diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 07ad8992..5b0b6988 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -157,6 +157,15 @@ defmodule Sentry.ConfigTest do end end + test ":tracing" do + assert Config.validate!(tracing: true)[:tracing] == true + assert Config.validate!([])[:tracing] == false + + assert_raise ArgumentError, ~r/invalid value for :tracing option/, fn -> + Config.validate!(tracing: "not_a_boolean") + end + end + test ":json_library" do assert Config.validate!(json_library: Jason)[:json_library] == Jason diff --git a/test/sentry/opentelemetry/span_processor_test.exs b/test/sentry/opentelemetry/span_processor_test.exs index 5b170ca6..291610f4 100644 --- a/test/sentry/opentelemetry/span_processor_test.exs +++ b/test/sentry/opentelemetry/span_processor_test.exs @@ -36,7 +36,7 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do end test "sends captured root spans as transactions" do - put_test_config(environment_name: "test") + put_test_config(environment_name: "test", tracing: true) Sentry.Test.start_collecting_sentry_reports() @@ -55,7 +55,7 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do end test "sends captured spans as transactions with child spans" do - put_test_config(environment_name: "test") + put_test_config(environment_name: "test", tracing: true) Sentry.Test.start_collecting_sentry_reports() @@ -92,7 +92,7 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do end test "removes span records from storage after sending a transaction" do - put_test_config(environment_name: "test") + put_test_config(environment_name: "test", tracing: true) Sentry.Test.start_collecting_sentry_reports()