Skip to content

Commit 52e80f0

Browse files
authored
feat(tracing): Dynamic Sampling Context / Baggage continuation (#1485)
* `Baggage` class implementing sentry/third party/mutable logic with parsing from header and serialization * Parse incoming `baggage` header while starting transaction and store it on the transaction * Extract `dynamic_sampling_context` fields and add to the `trace` field in the envelope header while sending the transaction * Propagate the `baggage` header (only sentry fields / no third party as per spec) [DSC Spec](https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/)
1 parent 5ea8d6b commit 52e80f0

File tree

7 files changed

+294
-54
lines changed

7 files changed

+294
-54
lines changed

docs/conf.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525

2626
# -- Project information -----------------------------------------------------
2727

28-
project = u"sentry-python"
29-
copyright = u"2019, Sentry Team and Contributors"
30-
author = u"Sentry Team and Contributors"
28+
project = "sentry-python"
29+
copyright = "2019, Sentry Team and Contributors"
30+
author = "Sentry Team and Contributors"
3131

3232
release = "1.6.0"
3333
version = ".".join(release.split(".")[:2]) # The short X.Y version.
@@ -72,7 +72,7 @@
7272
# List of patterns, relative to source directory, that match files and
7373
# directories to ignore when looking for source files.
7474
# This pattern also affects html_static_path and html_extra_path.
75-
exclude_patterns = [u"_build", "Thumbs.db", ".DS_Store"]
75+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
7676

7777
# The name of the Pygments (syntax highlighting) style to use.
7878
pygments_style = None
@@ -140,8 +140,8 @@
140140
(
141141
master_doc,
142142
"sentry-python.tex",
143-
u"sentry-python Documentation",
144-
u"Sentry Team and Contributors",
143+
"sentry-python Documentation",
144+
"Sentry Team and Contributors",
145145
"manual",
146146
)
147147
]
@@ -151,7 +151,7 @@
151151

152152
# One entry per manual page. List of tuples
153153
# (source start file, name, description, authors, manual section).
154-
man_pages = [(master_doc, "sentry-python", u"sentry-python Documentation", [author], 1)]
154+
man_pages = [(master_doc, "sentry-python", "sentry-python Documentation", [author], 1)]
155155

156156

157157
# -- Options for Texinfo output ----------------------------------------------
@@ -163,7 +163,7 @@
163163
(
164164
master_doc,
165165
"sentry-python",
166-
u"sentry-python Documentation",
166+
"sentry-python Documentation",
167167
author,
168168
"sentry-python",
169169
"One line description of project.",

sentry_sdk/client.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,12 @@ def capture_event(
373373
event_opt.get("contexts", {}).get("trace", {}).pop("tracestate", "")
374374
)
375375

376+
dynamic_sampling_context = (
377+
event_opt.get("contexts", {})
378+
.get("trace", {})
379+
.pop("dynamic_sampling_context", {})
380+
)
381+
376382
# Transactions or events with attachments should go to the /envelope/
377383
# endpoint.
378384
if is_transaction or attachments:
@@ -382,11 +388,15 @@ def capture_event(
382388
"sent_at": format_timestamp(datetime.utcnow()),
383389
}
384390

385-
tracestate_data = raw_tracestate and reinflate_tracestate(
386-
raw_tracestate.replace("sentry=", "")
387-
)
388-
if tracestate_data and has_tracestate_enabled():
389-
headers["trace"] = tracestate_data
391+
if has_tracestate_enabled():
392+
tracestate_data = raw_tracestate and reinflate_tracestate(
393+
raw_tracestate.replace("sentry=", "")
394+
)
395+
396+
if tracestate_data:
397+
headers["trace"] = tracestate_data
398+
elif dynamic_sampling_context:
399+
headers["trace"] = dynamic_sampling_context
390400

391401
envelope = Envelope(headers=headers)
392402

sentry_sdk/tracing.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def continue_from_environ(
215215
# type: (...) -> Transaction
216216
"""
217217
Create a Transaction with the given params, then add in data pulled from
218-
the 'sentry-trace' and 'tracestate' headers from the environ (if any)
218+
the 'sentry-trace', 'baggage' and 'tracestate' headers from the environ (if any)
219219
before returning the Transaction.
220220
221221
This is different from `continue_from_headers` in that it assumes header
@@ -238,7 +238,7 @@ def continue_from_headers(
238238
# type: (...) -> Transaction
239239
"""
240240
Create a transaction with the given params (including any data pulled from
241-
the 'sentry-trace' and 'tracestate' headers).
241+
the 'sentry-trace', 'baggage' and 'tracestate' headers).
242242
"""
243243
# TODO move this to the Transaction class
244244
if cls is Span:
@@ -247,7 +247,17 @@ def continue_from_headers(
247247
"instead of Span.continue_from_headers."
248248
)
249249

250-
kwargs.update(extract_sentrytrace_data(headers.get("sentry-trace")))
250+
# TODO-neel move away from this kwargs stuff, it's confusing and opaque
251+
# make more explicit
252+
baggage = Baggage.from_incoming_header(headers.get("baggage"))
253+
kwargs.update({"baggage": baggage})
254+
255+
sentrytrace_kwargs = extract_sentrytrace_data(headers.get("sentry-trace"))
256+
257+
if sentrytrace_kwargs is not None:
258+
kwargs.update(sentrytrace_kwargs)
259+
baggage.freeze
260+
251261
kwargs.update(extract_tracestate_data(headers.get("tracestate")))
252262

253263
transaction = Transaction(**kwargs)
@@ -258,7 +268,7 @@ def continue_from_headers(
258268
def iter_headers(self):
259269
# type: () -> Iterator[Tuple[str, str]]
260270
"""
261-
Creates a generator which returns the span's `sentry-trace` and
271+
Creates a generator which returns the span's `sentry-trace`, `baggage` and
262272
`tracestate` headers.
263273
264274
If the span's containing transaction doesn't yet have a
@@ -274,6 +284,9 @@ def iter_headers(self):
274284
if tracestate:
275285
yield "tracestate", tracestate
276286

287+
if self.containing_transaction and self.containing_transaction._baggage:
288+
yield "baggage", self.containing_transaction._baggage.serialize()
289+
277290
@classmethod
278291
def from_traceparent(
279292
cls,
@@ -460,7 +473,7 @@ def get_trace_context(self):
460473
"parent_span_id": self.parent_span_id,
461474
"op": self.op,
462475
"description": self.description,
463-
}
476+
} # type: Dict[str, Any]
464477
if self.status:
465478
rv["status"] = self.status
466479

@@ -473,6 +486,12 @@ def get_trace_context(self):
473486
if sentry_tracestate:
474487
rv["tracestate"] = sentry_tracestate
475488

489+
# TODO-neel populate fresh if head SDK
490+
if self.containing_transaction and self.containing_transaction._baggage:
491+
rv[
492+
"dynamic_sampling_context"
493+
] = self.containing_transaction._baggage.dynamic_sampling_context()
494+
476495
return rv
477496

478497

@@ -488,6 +507,7 @@ class Transaction(Span):
488507
# tracestate data from other vendors, of the form `dogs=yes,cats=maybe`
489508
"_third_party_tracestate",
490509
"_measurements",
510+
"_baggage",
491511
)
492512

493513
def __init__(
@@ -496,6 +516,7 @@ def __init__(
496516
parent_sampled=None, # type: Optional[bool]
497517
sentry_tracestate=None, # type: Optional[str]
498518
third_party_tracestate=None, # type: Optional[str]
519+
baggage=None, # type: Optional[Baggage]
499520
**kwargs # type: Any
500521
):
501522
# type: (...) -> None
@@ -517,6 +538,7 @@ def __init__(
517538
self._sentry_tracestate = sentry_tracestate
518539
self._third_party_tracestate = third_party_tracestate
519540
self._measurements = {} # type: Dict[str, Any]
541+
self._baggage = baggage
520542

521543
def __repr__(self):
522544
# type: () -> str
@@ -734,6 +756,7 @@ def _set_initial_sampling_decision(self, sampling_context):
734756
# Circular imports
735757

736758
from sentry_sdk.tracing_utils import (
759+
Baggage,
737760
EnvironHeaders,
738761
compute_tracestate_entry,
739762
extract_sentrytrace_data,

sentry_sdk/tracing_utils.py

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
to_string,
1717
from_base64,
1818
)
19-
from sentry_sdk._compat import PY2
19+
from sentry_sdk._compat import PY2, iteritems
2020
from sentry_sdk._types import MYPY
2121

2222
if PY2:
2323
from collections import Mapping
24+
from urllib import quote, unquote
2425
else:
2526
from collections.abc import Mapping
27+
from urllib.parse import quote, unquote
2628

2729
if MYPY:
2830
import typing
@@ -211,27 +213,29 @@ def maybe_create_breadcrumbs_from_span(hub, span):
211213

212214

213215
def extract_sentrytrace_data(header):
214-
# type: (Optional[str]) -> typing.Mapping[str, Union[str, bool, None]]
216+
# type: (Optional[str]) -> Optional[typing.Mapping[str, Union[str, bool, None]]]
215217
"""
216218
Given a `sentry-trace` header string, return a dictionary of data.
217219
"""
218-
trace_id = parent_span_id = parent_sampled = None
220+
if not header:
221+
return None
219222

220-
if header:
221-
if header.startswith("00-") and header.endswith("-00"):
222-
header = header[3:-3]
223+
if header.startswith("00-") and header.endswith("-00"):
224+
header = header[3:-3]
223225

224-
match = SENTRY_TRACE_REGEX.match(header)
226+
match = SENTRY_TRACE_REGEX.match(header)
227+
if not match:
228+
return None
225229

226-
if match:
227-
trace_id, parent_span_id, sampled_str = match.groups()
230+
trace_id, parent_span_id, sampled_str = match.groups()
231+
parent_sampled = None
228232

229-
if trace_id:
230-
trace_id = "{:032x}".format(int(trace_id, 16))
231-
if parent_span_id:
232-
parent_span_id = "{:016x}".format(int(parent_span_id, 16))
233-
if sampled_str:
234-
parent_sampled = sampled_str != "0"
233+
if trace_id:
234+
trace_id = "{:032x}".format(int(trace_id, 16))
235+
if parent_span_id:
236+
parent_span_id = "{:016x}".format(int(parent_span_id, 16))
237+
if sampled_str:
238+
parent_sampled = sampled_str != "0"
235239

236240
return {
237241
"trace_id": trace_id,
@@ -413,6 +417,86 @@ def has_custom_measurements_enabled():
413417
return bool(options and options["_experiments"].get("custom_measurements"))
414418

415419

420+
class Baggage(object):
421+
__slots__ = ("sentry_items", "third_party_items", "mutable")
422+
423+
SENTRY_PREFIX = "sentry-"
424+
SENTRY_PREFIX_REGEX = re.compile("^sentry-")
425+
426+
# DynamicSamplingContext
427+
DSC_KEYS = [
428+
"trace_id",
429+
"public_key",
430+
"sample_rate",
431+
"release",
432+
"environment",
433+
"transaction",
434+
"user_id",
435+
"user_segment",
436+
]
437+
438+
def __init__(
439+
self,
440+
sentry_items, # type: Dict[str, str]
441+
third_party_items="", # type: str
442+
mutable=True, # type: bool
443+
):
444+
self.sentry_items = sentry_items
445+
self.third_party_items = third_party_items
446+
self.mutable = mutable
447+
448+
@classmethod
449+
def from_incoming_header(cls, header):
450+
# type: (Optional[str]) -> Baggage
451+
"""
452+
freeze if incoming header already has sentry baggage
453+
"""
454+
sentry_items = {}
455+
third_party_items = ""
456+
mutable = True
457+
458+
if header:
459+
for item in header.split(","):
460+
item = item.strip()
461+
key, val = item.split("=")
462+
if Baggage.SENTRY_PREFIX_REGEX.match(key):
463+
baggage_key = unquote(key.split("-")[1])
464+
sentry_items[baggage_key] = unquote(val)
465+
mutable = False
466+
else:
467+
third_party_items += ("," if third_party_items else "") + item
468+
469+
return Baggage(sentry_items, third_party_items, mutable)
470+
471+
def freeze(self):
472+
# type: () -> None
473+
self.mutable = False
474+
475+
def dynamic_sampling_context(self):
476+
# type: () -> Dict[str, str]
477+
header = {}
478+
479+
for key in Baggage.DSC_KEYS:
480+
item = self.sentry_items.get(key)
481+
if item:
482+
header[key] = item
483+
484+
return header
485+
486+
def serialize(self, include_third_party=False):
487+
# type: (bool) -> str
488+
items = []
489+
490+
for key, val in iteritems(self.sentry_items):
491+
item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(val)
492+
items.append(item)
493+
494+
if include_third_party:
495+
items.append(self.third_party_items)
496+
497+
return ",".join(items)
498+
499+
416500
# Circular imports
417501

418502
if MYPY:

0 commit comments

Comments
 (0)