Skip to content

Commit 1b6248b

Browse files
committed
wip - add sentry-trace header propagator for DT
1 parent c282acd commit 1b6248b

File tree

2 files changed

+437
-0
lines changed

2 files changed

+437
-0
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
2+
defmodule Sentry.OpenTelemetry.Propagator do
3+
@moduledoc """
4+
OpenTelemetry propagator for Sentry distributed tracing.
5+
6+
This propagator implements the `sentry-trace` and `sentry-baggage` header propagation
7+
to enable distributed tracing across service boundaries. It follows the W3C Trace Context.
8+
9+
## Usage
10+
11+
Add the propagator to your OpenTelemetry configuration:
12+
13+
config :opentelemetry,
14+
text_map_propagators: [
15+
:trace_context,
16+
:baggage,
17+
Sentry.OpenTelemetry.Propagator
18+
]
19+
20+
This will enable automatic propagation of Sentry trace context in HTTP headers.
21+
"""
22+
23+
import Bitwise
24+
25+
require Record
26+
require OpenTelemetry.Tracer, as: Tracer
27+
28+
@behaviour :otel_propagator_text_map
29+
30+
@fields Record.extract(:span_ctx, from_lib: "opentelemetry_api/include/opentelemetry.hrl")
31+
Record.defrecordp(:span_ctx, @fields)
32+
33+
@sentry_trace_key "sentry-trace"
34+
@sentry_baggage_key "baggage"
35+
@sentry_trace_ctx_key :"sentry-trace"
36+
@sentry_baggage_ctx_key :"sentry-baggage"
37+
38+
@impl true
39+
def fields(_opts) do
40+
[@sentry_trace_key, @sentry_baggage_key]
41+
end
42+
43+
@impl true
44+
def inject(ctx, carrier, setter, _opts) do
45+
case Tracer.current_span_ctx(ctx) do
46+
span_ctx(trace_id: tid, span_id: sid, trace_flags: flags) when tid != 0 and sid != 0 ->
47+
sentry_trace_header = encode_sentry_trace({tid, sid, flags})
48+
carrier = setter.(@sentry_trace_key, sentry_trace_header, carrier)
49+
50+
# Inject baggage if it exists in the context
51+
# Note: :otel_ctx.get_value/2 returns the key itself if value not found
52+
baggage_value = :otel_ctx.get_value(ctx, @sentry_baggage_ctx_key, :not_found)
53+
54+
if is_binary(baggage_value) and baggage_value != :not_found do
55+
setter.(@sentry_baggage_key, baggage_value, carrier)
56+
else
57+
carrier
58+
end
59+
60+
_ ->
61+
carrier
62+
end
63+
end
64+
65+
@impl true
66+
def extract(ctx, carrier, _keys_fun, getter, _opts) do
67+
case getter.(@sentry_trace_key, carrier) do
68+
:undefined ->
69+
ctx
70+
71+
header when is_binary(header) ->
72+
case decode_sentry_trace(header) do
73+
{:ok, {trace_hex, span_hex, sampled}} ->
74+
ctx =
75+
ctx
76+
|> :otel_ctx.set_value(@sentry_trace_ctx_key, {trace_hex, span_hex, sampled})
77+
|> maybe_set_baggage(getter.(@sentry_baggage_key, carrier))
78+
79+
trace_id = hex_to_int(trace_hex)
80+
span_id = hex_to_int(span_hex)
81+
82+
# Always use sampled (1) to simulate a sampled trace on the OTel side
83+
trace_flags = 1
84+
85+
remote_span_ctx =
86+
:otel_tracer.from_remote_span(trace_id, span_id, trace_flags)
87+
88+
Tracer.set_current_span(ctx, remote_span_ctx)
89+
90+
{:error, _reason} ->
91+
# Invalid header format, skip propagation
92+
ctx
93+
end
94+
95+
_ ->
96+
ctx
97+
end
98+
end
99+
100+
# Encode trace ID, span ID, and sampled flag to sentry-trace header format
101+
# Format: {trace_id}-{span_id}-{sampled}
102+
defp encode_sentry_trace({trace_id_int, span_id_int, trace_flags}) do
103+
sampled = if (trace_flags &&& 1) == 1, do: "1", else: "0"
104+
int_to_hex(trace_id_int, 16) <> "-" <> int_to_hex(span_id_int, 8) <> "-" <> sampled
105+
end
106+
107+
# Decode sentry-trace header
108+
# Format: {trace_id}-{span_id}-{sampled} or {trace_id}-{span_id}
109+
defp decode_sentry_trace(
110+
<<trace_hex::binary-size(32), "-", span_hex::binary-size(16), "-",
111+
sampled::binary-size(1)>>
112+
) do
113+
{:ok, {trace_hex, span_hex, sampled == "1"}}
114+
end
115+
116+
defp decode_sentry_trace(<<trace_hex::binary-size(32), "-", span_hex::binary-size(16)>>) do
117+
{:ok, {trace_hex, span_hex, false}}
118+
end
119+
120+
defp decode_sentry_trace(_invalid) do
121+
{:error, :invalid_format}
122+
end
123+
124+
defp maybe_set_baggage(ctx, :undefined), do: ctx
125+
defp maybe_set_baggage(ctx, ""), do: ctx
126+
defp maybe_set_baggage(ctx, nil), do: ctx
127+
128+
defp maybe_set_baggage(ctx, baggage) when is_binary(baggage) do
129+
:otel_ctx.set_value(ctx, @sentry_baggage_ctx_key, baggage)
130+
end
131+
132+
# Convert hex string to integer
133+
defp hex_to_int(hex) do
134+
hex
135+
|> Base.decode16!(case: :mixed)
136+
|> :binary.decode_unsigned()
137+
end
138+
139+
# Convert integer to hex string with padding
140+
defp int_to_hex(value, num_bytes) do
141+
value
142+
|> :binary.encode_unsigned()
143+
|> bin_pad_left(num_bytes)
144+
|> Base.encode16(case: :lower)
145+
end
146+
147+
# Pad binary to specified number of bytes
148+
defp bin_pad_left(bin, total_bytes) do
149+
missing = total_bytes - byte_size(bin)
150+
if missing > 0, do: :binary.copy(<<0>>, missing) <> bin, else: bin
151+
end
152+
end
153+
end

0 commit comments

Comments
 (0)