Skip to content

feat(tracing): Add W3C traceparent support #4249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion sentry_sdk/integrations/opentelemetry/propagator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
SENTRY_TRACE_HEADER_NAME,
W3C_TRACE_HEADER_NAME,
)
from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data

Expand Down Expand Up @@ -103,6 +104,7 @@ def inject(self, carrier, context=None, setter=default_setter):
return

setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent())
setter.set(carrier, W3C_TRACE_HEADER_NAME, sentry_span.to_w3c_traceparent())

if sentry_span.containing_transaction:
baggage = sentry_span.containing_transaction.get_baggage()
Expand All @@ -114,4 +116,4 @@ def inject(self, carrier, context=None, setter=default_setter):
@property
def fields(self):
# type: () -> Set[str]
return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME}
return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME, W3C_TRACE_HEADER_NAME}
31 changes: 30 additions & 1 deletion sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
SENTRY_TRACE_HEADER_NAME,
W3C_TRACE_HEADER_NAME,
NoOpSpan,
Span,
Transaction,
Expand Down Expand Up @@ -533,6 +534,30 @@ def get_traceparent(self, *args, **kwargs):
# Fall back to isolation scope's traceparent. It always has one
return self.get_isolation_scope().get_traceparent()

def _get_w3c_traceparent(self, *args, **kwargs):
# type: (Any, Any) -> Optional[str]
"""
Returns the W3C "traceparent" header from the
currently active span or the scopes Propagation Context.
"""
client = self.get_client()

# If we have an active span, return traceparent from there
if has_tracing_enabled(client.options) and self.span is not None:
return self.span.to_w3c_traceparent()

# If this scope has a propagation context, return traceparent from there
if self._propagation_context is not None:
traceparent = "00-%s-%s-%s" % (
self._propagation_context.trace_id,
self._propagation_context.span_id,
"01" if self._propagation_context.parent_sampled is True else "00",
)
return traceparent

# Fall back to isolation scope's traceparent. It always has one
return self.get_isolation_scope()._get_w3c_traceparent()

def get_baggage(self, *args, **kwargs):
# type: (Any, Any) -> Optional[Baggage]
"""
Expand Down Expand Up @@ -608,13 +633,17 @@ def trace_propagation_meta(self, *args, **kwargs):
def iter_headers(self):
# type: () -> Iterator[Tuple[str, str]]
"""
Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context.
Creates a generator which returns the `sentry-trace`, `traceparent`, and `baggage` headers from the Propagation Context.
"""
if self._propagation_context is not None:
traceparent = self.get_traceparent()
if traceparent is not None:
yield SENTRY_TRACE_HEADER_NAME, traceparent

w3c_traceparent = self._get_w3c_traceparent()
if w3c_traceparent is not None:
yield W3C_TRACE_HEADER_NAME, w3c_traceparent

dsc = self.get_dynamic_sampling_context()
if dsc is not None:
baggage = Baggage(dsc).serialize()
Expand Down
27 changes: 26 additions & 1 deletion sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class TransactionKwargs(SpanKwargs, total=False):

BAGGAGE_HEADER_NAME = "baggage"
SENTRY_TRACE_HEADER_NAME = "sentry-trace"
W3C_TRACE_HEADER_NAME = "traceparent"


# Transaction source
Expand Down Expand Up @@ -513,7 +514,14 @@ def continue_from_headers(

if sentrytrace_kwargs is not None:
kwargs.update(sentrytrace_kwargs)
else:
w3c_traceparent_kwargs = extract_w3c_traceparent_data(
headers.get(W3C_TRACE_HEADER_NAME)
)
if w3c_traceparent_kwargs is not None:
kwargs.update(w3c_traceparent_kwargs)

if sentrytrace_kwargs is not None or w3c_traceparent_kwargs is not None:
# If there's an incoming sentry-trace but no incoming baggage header,
# for instance in traces coming from older SDKs,
# baggage will be empty and immutable and won't be populated as head SDK.
Expand All @@ -527,7 +535,8 @@ def continue_from_headers(
def iter_headers(self):
# type: () -> Iterator[Tuple[str, str]]
"""
Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers.
Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers,
as well as a ``traceparent`` header for W3C compatibility.
If the span's containing transaction doesn't yet have a ``baggage`` value,
this will cause one to be generated and stored.
"""
Expand All @@ -539,6 +548,7 @@ def iter_headers(self):
return

yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()
yield W3C_TRACE_HEADER_NAME, self.to_w3c_traceparent()

baggage = self.containing_transaction.get_baggage().serialize()
if baggage:
Expand Down Expand Up @@ -584,6 +594,16 @@ def to_traceparent(self):

return traceparent

def to_w3c_traceparent(self):
# type: () -> str
if self.sampled is True:
trace_flags = "01"
else:
trace_flags = "00"

traceparent = "00-%s-%s-%s" % (self.trace_id, self.span_id, trace_flags)
return traceparent

def to_baggage(self):
# type: () -> Optional[Baggage]
"""Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage`
Expand Down Expand Up @@ -1236,6 +1256,10 @@ def to_traceparent(self):
# type: () -> str
return ""

def to_w3c_traceparent(self):
# type: () -> str
return ""

def to_baggage(self):
# type: () -> Optional[Baggage]
return None
Expand Down Expand Up @@ -1358,6 +1382,7 @@ async def my_async_function():
Baggage,
EnvironHeaders,
extract_sentrytrace_data,
extract_w3c_traceparent_data,
_generate_sample_rand,
has_tracing_enabled,
maybe_create_breadcrumbs_from_span,
Expand Down
48 changes: 48 additions & 0 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@
"[ \t]*$" # whitespace
)

W3C_TRACE_REGEX = re.compile(
"^[ \t]*" # whitespace
"[0-9]{2}?" # version
"-?([0-9a-f]{32})?" # trace_id
"-?([0-9a-f]{16})?" # span_id
"-?([0-9]{2})?" # trace-flags
"[ \t]*$" # whitespace
)

# This is a normal base64 regex, modified to reflect that fact that we strip the
# trailing = or == off
Expand Down Expand Up @@ -341,6 +349,36 @@ def extract_sentrytrace_data(header):
}


def extract_w3c_traceparent_data(header):
# type: (Optional[str]) -> Optional[Dict[str, Union[str, bool, None]]]
"""
Given a `traceparent` header string, return a dictionary of data.
"""
if not header or not header.startswith("00-"):
return None

match = W3C_TRACE_REGEX.match(header)
if not match:
return None

trace_id, parent_span_id, trace_flags = match.groups()
parent_sampled = None

if trace_id:
trace_id = "{:032x}".format(int(trace_id, 16))
if parent_span_id:
parent_span_id = "{:016x}".format(int(parent_span_id, 16))
if trace_flags:
trace_flags_byte = int(trace_flags, 16)
parent_sampled = (trace_flags_byte & 0x01) == 1

return {
"trace_id": trace_id,
"parent_span_id": parent_span_id,
"parent_sampled": parent_sampled,
}


def _format_sql(cursor, sql):
# type: (Any, str) -> Optional[str]

Expand Down Expand Up @@ -422,6 +460,15 @@ def from_incoming_data(cls, incoming_data):
propagation_context = PropagationContext()
propagation_context.update(sentrytrace_data)

if sentry_trace_header is None:
w3c_trace_header = normalized_data.get(W3C_TRACE_HEADER_NAME)
if w3c_trace_header:
w3c_trace_data = extract_w3c_traceparent_data(w3c_trace_header)
if w3c_trace_data is not None:
if propagation_context is None:
propagation_context = PropagationContext()
propagation_context.update(w3c_trace_data)

if propagation_context is not None:
propagation_context._fill_sample_rand()

Expand Down Expand Up @@ -898,6 +945,7 @@ def _sample_rand_range(parent_sampled, sample_rate):
BAGGAGE_HEADER_NAME,
LOW_QUALITY_TRANSACTION_SOURCES,
SENTRY_TRACE_HEADER_NAME,
W3C_TRACE_HEADER_NAME,
)

if TYPE_CHECKING:
Expand Down
1 change: 1 addition & 0 deletions tests/integrations/aiohttp/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ async def hello(request):
"User-Agent": request["headers"]["User-Agent"],
"baggage": mock.ANY,
"sentry-trace": mock.ANY,
"traceparent": mock.ANY,
}


Expand Down
95 changes: 93 additions & 2 deletions tests/integrations/asgi/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,21 @@ async def test_trace_from_headers_if_performance_enabled(

trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
w3c_trace_header = "00-082b43a4192642f0b136d5159a501701-6e8f22c393e68f19-01"

# If both sentry-trace and traceparent headers are present, sentry-trace takes precedence.
# See: https://github.com/getsentry/team-sdks/issues/41

with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
events = capture_events()
await client.get("/", headers={"sentry-trace": sentry_trace_header})
await client.get(
"/",
headers={
"sentry-trace": sentry_trace_header,
"traceparent": w3c_trace_header,
},
)

msg_event, error_event, transaction_event = events

Expand Down Expand Up @@ -330,11 +340,92 @@ async def test_trace_from_headers_if_performance_disabled(

trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
w3c_trace_header = "00-082b43a4192642f0b136d5159a501701-6e8f22c393e68f19-01"

# If both sentry-trace and traceparent headers are present, sentry-trace takes precedence.
# See: https://github.com/getsentry/team-sdks/issues/41

with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
events = capture_events()
await client.get("/", headers={"sentry-trace": sentry_trace_header})
await client.get(
"/",
headers={
"sentry-trace": sentry_trace_header,
"traceparent": w3c_trace_header,
},
)

msg_event, error_event = events

assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id

assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]["trace_id"] == trace_id


@pytest.mark.asyncio
async def test_trace_from_w3c_headers_if_performance_enabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
):
sentry_init(traces_sample_rate=1.0)
app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg)

trace_id = "582b43a4192642f0b136d5159a501701"
w3c_trace_header = "00-{}-{}-{}".format(trace_id, "6e8f22c393e68f19", "01")

with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
events = capture_events()
await client.get(
"/",
headers={
"traceparent": w3c_trace_header,
},
)

msg_event, error_event, transaction_event = events

assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]

assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]

assert transaction_event["contexts"]["trace"]
assert "trace_id" in transaction_event["contexts"]["trace"]

assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id


@pytest.mark.asyncio
async def test_trace_from_w3c_headers_if_performance_disabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
):
sentry_init()
app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg)

trace_id = "582b43a4192642f0b136d5159a501701"
w3c_trace_header = "00-{}-{}-{}".format(trace_id, "6e8f22c393e68f19", "01")

with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
events = capture_events()
await client.get(
"/",
headers={
"traceparent": w3c_trace_header,
},
)

msg_event, error_event = events

Expand Down
Loading