8000 feat(tracing): Handle `tracestate` HTTP headers/correlation context e… · getsentry/sentry-python@7ebc84b · GitHub
[go: up one dir, main page]

Skip to content

Commit 7ebc84b

Browse files
committed
feat(tracing): Handle tracestate HTTP headers/correlation context envelope headers (#971)
1 parent ce1d0d8 commit 7ebc84b

File tree

11 files changed

+722
-105
lines changed

11 files changed

+722
-105
lines changed

sentry_sdk/client.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from sentry_sdk.utils import ContextVar
2323
from sentry_sdk.sessions import SessionFlusher
2424
from sentry_sdk.envelope import Envelope
25+
from sentry_sdk.tracing_utils import reinflate_tracestate
2526

2627
from sentry_sdk._types import MYPY
2728

@@ -329,15 +330,29 @@ def capture_event(
329330
attachments = hint.get("attachments")
330331
is_transaction = event_opt.get("type") == "transaction"
331332

333+
# this is outside of the `if` immediately below because even if we don't
334+
# use the value, we want to make sure we remove it before the event is
335+
# sent (which the `.pop()` does)
336+
raw_tracestate = (
337+
event_opt.get("contexts", {}).get("trace", {}).pop("tracestate", "")
338+
)
339+
340+
# Transactions or events with attachments should go to the /envelope/
341+
# endpoint.
332342
if is_transaction or attachments:
333-
# Transactions or events with attachments should go to the
334-
# /envelope/ endpoint.
335-
envelope = Envelope(
336-
headers={
337-
"event_id": event_opt["event_id"],
338-
"sent_at": format_timestamp(datetime.utcnow()),
339-
}
343+
344+
headers = {
345+
"event_id": event_opt["event_id"],
346+
"sent_at": format_timestamp(datetime.utcnow()),
347+
}
348+
349+
tracestate_data = reinflate_tracestate(
350+
raw_tracestate.replace("sentry=", "")
340351
)
352+
if tracestate_data:
353+
headers["trace"] = tracestate_data
354+
355+
envelope = Envelope(headers=headers)
341356

342357
if is_transaction:
343358
envelope.add_transaction(event_opt)

sentry_sdk/hub.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,8 @@ def iter_trace_propagation_headers(self, span=None):
699699
if not propagate_traces:
700700
return
701701

702-
yield "sentry-trace", span.to_traceparent()
702+
for header in span.iter_headers():
703+
yield header
703704

704705

705706
GLOBAL_HUB = Hub()

sentry_sdk/tracing.py

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
from sentry_sdk.utils import logger
1010
from sentry_sdk.tracing_utils import (
11-
SENTRY_TRACE_REGEX,
1211
EnvironHeaders,
12+
compute_tracestate_entry,
13+
extract_sentrytrace_data,
14+
extract_tracestate_data,
1315
has_tracing_enabled,
1416
is_valid_sample_rate,
1517
maybe_create_breadcrumbs_from_span,
@@ -210,11 +212,12 @@ def continue_from_environ(
210212
# type: (...) -> Transaction
211213
"""
212214
Create a Transaction with the given params, then add in data pulled from
213-
the 'sentry-trace' header in the environ (if any) before returning the
214-
Transaction.
215+
the 'sentry-trace' and 'tracestate' headers from the environ (if any)
216+
before returning the Transaction.
215217
216-
If the 'sentry-trace' header is malformed or missing, just create and
217-
return a Transaction instance with the given params.
218+
This is different from `continue_from_headers` in that it assumes header
219+
names in the form "HTTP_HEADER_NAME" - such as you would get from a wsgi
220+
environ - rather than the form "header-name".
218221
"""
219222
if cls is Span:
220223
logger.warning(
@@ -231,28 +234,30 @@ def continue_from_headers(
231234
):
232235
# type: (...) -> Transaction
233236
"""
234-
Create a Transaction with the given params, then add in data pulled from
235-
the 'sentry-trace' header (if any) before returning the Transaction.
236-
237-
If the 'sentry-trace' header is malformed or missing, just create and
238-
return a Transaction instance with the given params.
237+
Create a transaction with the given params (including any data pulled from
238+
the 'sentry-trace' and 'tracestate' headers).
239239
"""
240+
# TODO move this to the Transaction class
240241
if cls is Span:
241242
logger.warning(
242243
"Deprecated: use Transaction.continue_from_headers "
243244
"instead of Span.continue_from_headers."
244245
)
245-
transaction = Transaction.from_traceparent(
246-
headers.get("sentry-trace"), **kwargs
247-
)
248-
if transaction is None:
249-
transaction = Transaction(**kwargs)
246+
247+
kwargs.update(extract_sentrytrace_data(headers.get("sentry-trace")))
248+
kwargs.update(extract_tracestate_data(headers.get("tracestate")))
249+
250+
transaction = Transaction(**kwargs)
250251
transaction.same_process_as_parent = False
252+
251253
return transaction
252254

253255
def iter_headers(self):
254256
# type: () -> Generator[Tuple[str, str], None, None]
255257
yield "sentry-trace", self.to_traceparent()
258+
tracestate = self.to_tracestate()
259+
if tracestate:
260+
yield "tracestate", tracestate
256261

257262
@classmethod
258263
def from_traceparent(
@@ -262,46 +267,21 @@ def from_traceparent(
262267
):
263268
# type: (...) -> Optional[Transaction]
264269
"""
270+
DEPRECATED: Use Transaction.continue_from_headers(headers, **kwargs)
271+
265272
Create a Transaction with the given params, then add in data pulled from
266273
the given 'sentry-trace' header value before returning the Transaction.
267274
268-
If the header value is malformed or missing, just create and return a
269-
Transaction instance with the given params.
270275
"""
271-
if cls is Span:
272-
logger.warning(
273-
"Deprecated: use Transaction.from_traceparent "
274-
"instead of Span.from_traceparent."
275-
)
276+
logger.warning(
277+
"Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) "
278+
"instead of from_traceparent(traceparent, **kwargs)"
279+
)
276280

277281
if not traceparent:
278282
return None
279283

280-
if traceparent.startswith("00-") and traceparent.endswith("-00"):
281-
traceparent = traceparent[3:-3]
282-
283-
match = SENTRY_TRACE_REGEX.match(str(traceparent))
284-
if match is None:
285-
return None
286-
287-
trace_id, parent_span_id, sampled_str = match.groups()
288-
289-
if trace_id is not None:
290-
trace_id = "{:032x}".format(int(trace_id, 16))
291-
if parent_span_id is not None:
292-
parent_span_id = "{:016x}".format(int(parent_span_id, 16))
293-
294-
if sampled_str:
295-
parent_sampled = sampled_str != "0" # type: Optional[bool]
296-
else:
297-
parent_sampled = None
298-
299-
return Transaction(
300-
trace_id=trace_id,
301-
parent_span_id=parent_span_id,
302-
parent_sampled=parent_sampled,
303-
**kwargs
304-
)
284+
return cls.continue_from_headers({"sentry-trace": traceparent}, **kwargs)
305285

306286
def to_traceparent(self):
307287
# type: () -> str
@@ -312,6 +292,34 @@ def to_traceparent(self):
312292
sampled = "0"
313293
return "%s-%s-%s" % (self.trace_id, self.span_id, sampled)
314294

295+
def to_tracestate(self):
296+
# type: () -> Optional[str]
297+
"""
298+
Generates the `tracestate` header value to attach to outgoing requests.
299+
"""
300+
header_value = None
301+
302+
if isinstance(self, Transaction):
303+
transaction = self # type: Optional[Transaction]
304+
else:
305+
transaction = self._containing_transaction
306+
307+
# we should have the relevant values stored on the transaction, but if
308+
# this is an orphan span, make a new value
309+
if transaction:
310+
sentry_tracestate = transaction._sentry_tracestate
311+
third_party_tracestate = transaction._third_party_tracestate
312+
else:
313+
sentry_tracestate = compute_tracestate_entry(self)
314+
third_party_tracestate = None
315+
316+
header_value = sentry_tracestate
317+
318+
if third_party_tracestate:
319+
header_value = header_value + "," + third_party_tracestate
320+
321+
return header_value
322+
315323
def set_tag(self, key, value):
316324
# type: (str, Any) -> None
317325
self._tags[key] = value
@@ -418,16 +426,36 @@ def get_trace_context(self):
418426
if self.status:
419427
rv["status"] = self.status
420428

429+
if isinstance(self, Transaction):
430+
transaction = self # type: Optional[Transaction]
431+
else:
432+
transaction = self._containing_transaction
433+
434+
if transaction:
435+
rv["tracestate"] = transaction._sentry_tracestate
436+
421437
return rv
422438

423439

424440
class Transaction(Span):
425-
__slots__ = ("name", "parent_sampled")
441+
__slots__ = (
442+
"name",
443+
"parent_sampled",
444+
# the sentry portion of the `tracestate` header used to transmit
445+
# correlation context for server-side dynamic sampling, of the form
446+
# `sentry=xxxxx`, where `xxxxx` is the base64-encoded json of the
447+
# correlation context data, missing trailing any =
448+
"_sentry_tracestate",
449+
# tracestate data from other vendors, of the form `dogs=yes,cats=maybe`
450+
"_third_party_tracestate",
451+
)
426452

427453
def __init__(
428454
self,
429455
name="", # type: str
430456
parent_sampled=None, # type: Optional[bool]
457+
sentry_tracestate=None, # type: Optional[str]
458+
third_party_tracestate=None, # type: Optional[str]
431459
**kwargs # type: Any
432460
):
433461
# type: (...) -> None
@@ -443,6 +471,8 @@ def __init__(
443471
Span.__init__(self, **kwargs)
444472
self.name = name
445473
self.parent_sampled = parent_sampled
474+
self._sentry_tracestate = sentry_tracestate or compute_tracestate_entry(self)
475+
self._third_party_tracestate = third_party_tracestate
446476

447477
def __repr__(self):
448478
# type: () -> str

0 commit comments

Comments
 (0)
0