From 9741df26fe8b9ac8ab0687ca2aedefc738cf0f12 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 17 Aug 2026 08:11:34 +0000 Subject: [PATCH] refactor(tracing): Unify OpenTelemetry trace context extraction --- lib/sentry/event.ex | 24 +--------- lib/sentry/metrics.ex | 44 +++---------------- lib/sentry/opentelemetry/trace_context.ex | 30 +++++++++++++ test/sentry/metrics_integration_test.exs | 20 +++++++++ .../sentry/opentelemetry/integration_test.exs | 2 + 5 files changed, 60 insertions(+), 60 deletions(-) create mode 100644 lib/sentry/opentelemetry/trace_context.ex diff --git a/lib/sentry/event.ex b/lib/sentry/event.ex index ff8169738..36adea83a 100644 --- a/lib/sentry/event.ex +++ b/lib/sentry/event.ex @@ -502,32 +502,12 @@ defmodule Sentry.Event do case current_trace_context() do nil -> contexts - trace -> Map.put(contexts, :trace, trace) + {trace_id, span_id} -> Map.put(contexts, :trace, %{trace_id: trace_id, span_id: span_id}) end end if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do - defp current_trace_context do - case :otel_tracer.current_span_ctx() do - :undefined -> - nil - - span_ctx -> - trace_id = :otel_span.trace_id(span_ctx) - span_id = :otel_span.span_id(span_ctx) - - if trace_id != 0 and span_id != 0 do - %{trace_id: format_id(trace_id, 32), span_id: format_id(span_id, 16)} - end - end - end - - defp format_id(id, length) do - id - |> Integer.to_string(16) - |> String.pad_leading(length, "0") - |> String.downcase() - end + defp current_trace_context, do: Sentry.OpenTelemetry.TraceContext.current() else defp current_trace_context, do: nil end diff --git a/lib/sentry/metrics.ex b/lib/sentry/metrics.ex index abc31fc67..7a88f1449 100644 --- a/lib/sentry/metrics.ex +++ b/lib/sentry/metrics.ex @@ -42,7 +42,7 @@ defmodule Sentry.Metrics do """ @moduledoc since: "13.0.0" - alias Sentry.{ClientReport, Config, LoggerUtils, Metric, TelemetryProcessor} + alias Sentry.{ClientReport, Config, Metric, TelemetryProcessor} @doc """ Records a counter metric. @@ -117,8 +117,7 @@ defmodule Sentry.Metrics do unit = Keyword.get(opts, :unit) attributes = Keyword.get(opts, :attributes, %{}) - {trace_id, span_id} = extract_trace_context() - trace_id = trace_id || generate_trace_id() + {trace_id, span_id} = current_trace_context() || {generate_trace_id(), nil} # Build metric struct metric = %Metric{ @@ -146,41 +145,10 @@ defmodule Sentry.Metrics do :ok end - defp extract_trace_context do - case :otel_tracer.current_span_ctx() do - :undefined -> - {nil, nil} - - span_ctx -> - trace_id = :otel_span.trace_id(span_ctx) - span_id = :otel_span.span_id(span_ctx) - - if trace_id != 0 and span_id != 0 do - {format_trace_id(trace_id), format_span_id(span_id)} - else - {nil, nil} - end - end - rescue - e in [UndefinedFunctionError, ArgumentError] -> - LoggerUtils.debug("Failed to extract OpenTelemetry trace context: #{inspect(e)}") - {nil, nil} - end - - # Format trace_id as 32-character hex string - defp format_trace_id(trace_id) when is_integer(trace_id) do - trace_id - |> Integer.to_string(16) - |> String.pad_leading(32, "0") - |> String.downcase() - end - - # Format span_id as 16-character hex string - defp format_span_id(span_id) when is_integer(span_id) do - span_id - |> Integer.to_string(16) - |> String.pad_leading(16, "0") - |> String.downcase() + if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do + defp current_trace_context, do: Sentry.OpenTelemetry.TraceContext.current() + else + defp current_trace_context, do: nil end # Generate a random trace_id as fallback when no active span exists diff --git a/lib/sentry/opentelemetry/trace_context.ex b/lib/sentry/opentelemetry/trace_context.ex new file mode 100644 index 000000000..68bc87462 --- /dev/null +++ b/lib/sentry/opentelemetry/trace_context.ex @@ -0,0 +1,30 @@ +if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do + defmodule Sentry.OpenTelemetry.TraceContext do + @moduledoc false + + @type t() :: {trace_id :: String.t(), span_id :: String.t()} + + @spec current() :: t() | nil + def current do + case :otel_tracer.current_span_ctx() do + :undefined -> + nil + + span_ctx -> + trace_id = :otel_span.trace_id(span_ctx) + span_id = :otel_span.span_id(span_ctx) + + if trace_id != 0 and span_id != 0 do + {format_id(trace_id, 32), format_id(span_id, 16)} + end + end + end + + defp format_id(id, length) do + id + |> Integer.to_string(16) + |> String.pad_leading(length, "0") + |> String.downcase() + end + end +end diff --git a/test/sentry/metrics_integration_test.exs b/test/sentry/metrics_integration_test.exs index 46f9a3df4..fc3f9cc96 100644 --- a/test/sentry/metrics_integration_test.exs +++ b/test/sentry/metrics_integration_test.exs @@ -1,6 +1,8 @@ defmodule Sentry.MetricsIntegrationTest do use Sentry.Case, async: false + require OpenTelemetry.Tracer, as: Tracer + import Sentry.TestHelpers import Sentry.Test.Assertions @@ -128,4 +130,22 @@ defmodule Sentry.MetricsIntegrationTest do assert_sentry_metric(:distribution, name: "distribution.metric") end end + + describe "trace context on recorded metrics" do + test "records the metric with the trace of the surrounding span", ctx do + put_test_config(traces_sample_rate: 1.0) + + Tracer.with_span "checkout" do + Metrics.count("orders.placed", 1) + end + + envelopes = collect_envelopes(ctx.ref, 2) + + assert [%{"items" => [metric]}] = extract_metric_items(envelopes) + assert [transaction] = extract_transactions(envelopes) + + assert metric["trace_id"] == transaction["contexts"]["trace"]["trace_id"] + assert metric["span_id"] == transaction["contexts"]["trace"]["span_id"] + end + end end diff --git a/test/sentry/opentelemetry/integration_test.exs b/test/sentry/opentelemetry/integration_test.exs index d3fa275a4..cc4cd2170 100644 --- a/test/sentry/opentelemetry/integration_test.exs +++ b/test/sentry/opentelemetry/integration_test.exs @@ -11,6 +11,7 @@ defmodule Sentry.OpenTelemetry.IntegrationTest do assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) assert Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + assert Code.ensure_loaded?(Sentry.OpenTelemetry.TraceContext) assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanStorage) false -> @@ -18,6 +19,7 @@ defmodule Sentry.OpenTelemetry.IntegrationTest do refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanProcessor) refute Code.ensure_loaded?(Sentry.OpenTelemetry.Sampler) refute Code.ensure_loaded?(Sentry.OpenTelemetry.SpanRecord) + refute Code.ensure_loaded?(Sentry.OpenTelemetry.TraceContext) # SpanStorage should always be defined as it doesn't depend on OpenTelemetry directly assert Code.ensure_loaded?(Sentry.OpenTelemetry.SpanStorage) end