sentry-elixir icon indicating copy to clipboard operation
sentry-elixir copied to clipboard

traceparent support

Open spunkedy opened this issue 5 months ago • 8 comments

when the traceparent header is passed on the requests to phoenix, it is currently not supported to be able to pull in the parent span so that we can support having distributed tracing until the full otel support is implemented

spunkedy avatar Jul 12 '25 16:07 spunkedy

trace

spunkedy avatar Jul 12 '25 16:07 spunkedy

      {:sentry, git: "https://github.com/spunkedy/sentry-elixir", branch: "feature-enable-traceparent"},

has been tested with passing the headers

spunkedy avatar Jul 12 '25 16:07 spunkedy

Thank you for the PR! 💜 We actually need to introduce an opentelemetry propagator as we have an architectural design for this functionality that is followed by other SDKs and we need the Elixir SDK to follow it as well 🙂

If this is something you'd be interested in doing - you could port what we do in the Ruby SDK: https://github.com/getsentry/sentry-ruby/blob/master/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb

solnic avatar Jul 21 '25 13:07 solnic

There's an older implementation, but I tested it and it doesn't work with the latest packages: https://github.com/scripbox/opentelemetry_sentry/blob/v0.1.1/lib/opentelemetry_sentry/propagator.ex

I believe the main issue is that baggage is not being propagated

venkatd avatar Jul 30 '25 22:07 venkatd

I wanted to mention that we have a working prototype of distributed tracing. It requires a custom propagator and some tweaks to the span processor to support forcing a transaction w/ http requests.

I don't have time at work to take this to completion but sharing in case it's helpful to anyone.

We use this custom propagator.

defmodule Ex.Sentry.OpenTelemetryPropagator do
  @behaviour :otel_propagator_text_map

  require Record
  require OpenTelemetry.Tracer, as: Tracer

  @fields Record.extract(:span_ctx, from_lib: "opentelemetry_api/include/opentelemetry.hrl")
  Record.defrecordp(:span_ctx, @fields)

  @sentry_trace_key "sentry-trace"
  @sentry_baggage_key "sentry-baggage"
  @sentry_trace_ctx_key :"sentry-trace"
  @sentry_baggage_ctx_key :"sentry-baggage"

  @impl true
  def fields(_opts),
    do: [@sentry_trace_key, @sentry_baggage_key]

  @impl true
  def inject(ctx, carrier, setter, _opts) do
    case Tracer.current_span_ctx(ctx) do
      span_ctx(trace_id: tid, span_id: sid, trace_flags: flags) when tid != 0 and sid != 0 ->
        setter.(@sentry_trace_key, encode_sentry_trace_id({tid, sid, flags}), carrier)

      _ ->
        carrier
    end
  end

  @impl true
  def extract(ctx, carrier, _keys_fun, getter, _opts) do
    case getter.(@sentry_trace_key, carrier) do
      :undefined ->
        ctx

      header when is_binary(header) ->
        {trace_hex, span_hex, sampled} = decode_sentry_trace_id(header)

        ctx =
          ctx
          |> :otel_ctx.set_value(@sentry_trace_ctx_key, {trace_hex, span_hex, sampled})
          |> :otel_ctx.set_value(
            @sentry_baggage_ctx_key,
            baggage(getter.(@sentry_baggage_key, carrier))
          )

        # Create a remote, sampled parent span in the OTEL context.
        # We will set to "always sample" because Sentry will decide real sampling
        remote_ctx =
          :otel_tracer.from_remote_span(hex_to_int(trace_hex), hex_to_int(span_hex), 1)

        Tracer.set_current_span(ctx, remote_ctx)
    end
  end

  defp encode_sentry_trace_id({trace_id_int, span_id_int, sampled}) do
    sampled = if sampled, do: "1", else: "0"

    int_to_hex(trace_id_int, 16) <> "-" <> int_to_hex(span_id_int, 8) <> "-" <> sampled
  end

  defp decode_sentry_trace_id(
         <<trace_hex::binary-size(32), "-", span_hex::binary-size(16), "-",
           sampled::binary-size(1)>>
       ),
       do: {trace_hex, span_hex, sampled == "1"}

  defp decode_sentry_trace_id(<<trace_hex::binary-size(32), "-", span_hex::binary-size(16)>>),
    do: {trace_hex, span_hex, false}

  defp baggage(:undefined), do: nil
  defp baggage(""), do: nil
  defp baggage(bin) when is_binary(bin), do: bin

  defp hex_to_int(hex) do
    hex
    |> Base.decode16!(case: :mixed)
    |> :binary.decode_unsigned()
  end

  defp int_to_hex(value, num_bytes) do
    value
    |> :binary.encode_unsigned()
    |> bin_pad_left(num_bytes)
    |> Base.encode16(case: :lower)
  end

  defp bin_pad_left(bin, total_bytes) do
    missing = total_bytes - byte_size(bin)
    if missing > 0, do: :binary.copy(<<0>>, missing) <> bin, else: bin
  end
end

There are some details I'm not certain of but it is working for us.

For the span processor, we do need to ensure a new transaction is created. If we don't do this (from what I tested) Sentry will not show a a distributed trace.

I think a simpler check for an http request in the span processor would be as follows. I don't think it is necessary to test all of the fields.

  defp is_http_server_request_span?(%{kind: kind, attributes: attributes}) do
    kind == :server and Map.has_key?(attributes, to_string(HTTPAttributes.http_request_method()))
  end

Then the check can be:

    is_transaction_root =
      span_record.parent_span_id == nil or
        is_http_server_request_span?(span_record)

You can see it in action here:

CleanShot 2025-08-10 at 12 45 05

venkatd avatar Aug 10 '25 10:08 venkatd

@venkatd thanks man, very helpful!

solnic avatar Aug 12 '25 15:08 solnic

no updates for this topic?

kubosuke avatar Nov 19 '25 11:11 kubosuke

@kubosuke it's a WIP, I'll be opening a PR before the end of the week, got it working already just need to clean up the implementation :)

image

solnic avatar Nov 19 '25 11:11 solnic