Skip to content

Commit 203d4d1

Browse files
committed
Add external_propagation_context support
If we are on an external tracing system like otel, we allow registering a new source of `trace_id/span_id` that takes precedence over the scope's propagation_context. * Also reworked logs and metrics to use `get_trace_context` * Cleaned up handling of `get_trace_context` that is still messy but a bit more clearer now regarding which underlying `propagation_context` is used
1 parent 2397b15 commit 203d4d1

File tree

7 files changed

+126
-43
lines changed

7 files changed

+126
-43
lines changed

sentry_sdk/client.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -928,17 +928,18 @@ def _capture_log(self, log):
928928
if release is not None and "sentry.release" not in log["attributes"]:
929929
log["attributes"]["sentry.release"] = release
930930

931-
span = current_scope.span
932-
if span is not None and "sentry.trace.parent_span_id" not in log["attributes"]:
933-
log["attributes"]["sentry.trace.parent_span_id"] = span.span_id
934-
935-
if log.get("trace_id") is None:
936-
transaction = current_scope.transaction
937-
propagation_context = isolation_scope.get_active_propagation_context()
938-
if transaction is not None:
939-
log["trace_id"] = transaction.trace_id
940-
elif propagation_context is not None:
941-
log["trace_id"] = propagation_context.trace_id
931+
trace_context = current_scope.get_trace_context()
932+
trace_id = trace_context.get("trace_id")
933+
span_id = trace_context.get("span_id")
934+
935+
if trace_id is not None and log.get("trace_id") is None:
936+
log["trace_id"] = trace_id
937+
938+
if (
939+
span_id is not None
940+
and "sentry.trace.parent_span_id" not in log["attributes"]
941+
):
942+
log["attributes"]["sentry.trace.parent_span_id"] = span_id
942943

943944
# The user, if present, is always set on the isolation scope.
944945
if isolation_scope._user is not None:
@@ -977,6 +978,7 @@ def _capture_metric(self, metric):
977978
if metric is None:
978979
return
979980

981+
current_scope = sentry_sdk.get_current_scope()
980982
isolation_scope = sentry_sdk.get_isolation_scope()
981983

982984
metric["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
@@ -990,16 +992,13 @@ def _capture_metric(self, metric):
990992
if release is not None and "sentry.release" not in metric["attributes"]:
991993
metric["attributes"]["sentry.release"] = release
992994

993-
span = sentry_sdk.get_current_span()
994-
metric["trace_id"] = "00000000-0000-0000-0000-000000000000"
995+
trace_context = current_scope.get_trace_context()
996+
trace_id = trace_context.get("trace_id")
997+
span_id = trace_context.get("span_id")
995998

996-
if span:
997-
metric["trace_id"] = span.trace_id
998-
metric["span_id"] = span.span_id
999-
else:
1000-
propagation_context = isolation_scope.get_active_propagation_context()
1001-
if propagation_context and propagation_context.trace_id:
1002-
metric["trace_id"] = propagation_context.trace_id
999+
metric["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000"
1000+
if span_id is not None:
1001+
metric["span_id"] = span_id
10031002

10041003
if isolation_scope._user is not None:
10051004
for metric_attribute, user_attribute in (

sentry_sdk/scope.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@
107107

108108
global_event_processors = [] # type: List[EventProcessor]
109109

110+
# A function returning a (trace_id, span_id) tuple
111+
# from an external tracing source (such as otel)
112+
_external_propagation_context_fn = None # type: Optional[Callable[[], Optional[Tuple[str, str]]]]
113+
110114

111115
class ScopeType(Enum):
112116
CURRENT = "current"
@@ -142,6 +146,25 @@ def add_global_event_processor(processor):
142146
global_event_processors.append(processor)
143147

144148

149+
def register_external_propagation_context(fn):
150+
# type: (Callable[[], Optional[Tuple[str, str]]]) -> None
151+
global _external_propagation_context_fn
152+
_external_propagation_context_fn = fn
153+
154+
155+
def remove_external_propagation_context():
156+
# type: () -> None
157+
global _external_propagation_context_fn
158+
_external_propagation_context_fn = None
159+
160+
161+
def get_external_propagation_context():
162+
# type: () -> Optional[Tuple[str, str]]
163+
return (
164+
_external_propagation_context_fn() if _external_propagation_context_fn else None
165+
)
166+
167+
145168
def _attr_setter(fn):
146169
# type: (Any) -> Any
147170
return property(fset=fn, doc=fn.__doc__)
@@ -562,21 +585,29 @@ def get_baggage(self, *args, **kwargs):
562585
return self.get_isolation_scope().get_baggage()
563586

564587
def get_trace_context(self):
565-
# type: () -> Any
588+
# type: () -> Dict[str, Any]
566589
"""
567590
Returns the Sentry "trace" context from the Propagation Context.
568591
"""
569-
if self._propagation_context is None:
570-
return None
592+
if has_tracing_enabled(self.get_client().options) and self._span is not None:
593+
return self._span.get_trace_context()
571594

572-
trace_context = {
573-
"trace_id": self._propagation_context.trace_id,
574-
"span_id": self._propagation_context.span_id,
575-
"parent_span_id": self._propagation_context.parent_span_id,
576-
"dynamic_sampling_context": self.get_dynamic_sampling_context(),
577-
} # type: Dict[str, Any]
595+
# if we are tracing externally (otel), those values take precedence
596+
external_propagation_context = get_external_propagation_context()
597+
if external_propagation_context:
598+
trace_id, span_id = external_propagation_context
599+
return {"trace_id": trace_id, "span_id": span_id}
578600

579-
return trace_context
601+
propagation_context = self.get_active_propagation_context()
602+
if propagation_context is None:
603+
return {}
604+
605+
return {
606+
"trace_id": propagation_context.trace_id,
607+
"span_id": propagation_context.span_id,
608+
"parent_span_id": propagation_context.parent_span_id,
609+
"dynamic_sampling_context": self.get_dynamic_sampling_context(),
610+
}
580611

581612
def trace_propagation_meta(self, *args, **kwargs):
582613
# type: (*Any, **Any) -> str
@@ -1438,10 +1469,7 @@ def _apply_contexts_to_event(self, event, hint, options):
14381469

14391470
# Add "trace" context
14401471
if contexts.get("trace") is None:
1441-
if has_tracing_enabled(options) and self._span is not None:
1442-
contexts["trace"] = self._span.get_trace_context()
1443-
else:
1444-
contexts["trace"] = self.get_trace_context()
1472+
contexts["trace"] = self.get_trace_context()
14451473

14461474
def _apply_flags_to_event(self, event, hint, options):
14471475
# type: (Event, Hint, Optional[Dict[str, Any]]) -> None

tests/integrations/logging/test_logging.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,10 @@ def test_logger_with_all_attributes(sentry_init, capture_envelopes):
478478

479479
assert attributes.pop("sentry.sdk.name").startswith("sentry.python")
480480

481+
assert "sentry.trace.parent_span_id" in attributes
482+
assert isinstance(attributes["sentry.trace.parent_span_id"], str)
483+
del attributes["sentry.trace.parent_span_id"]
484+
481485
# Assert on the remaining non-dynamic attributes.
482486
assert attributes == {
483487
"foo": "bar",

tests/integrations/loguru/test_loguru.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,10 @@ def test_logger_with_all_attributes(
458458

459459
assert attributes.pop("sentry.sdk.name").startswith("sentry.python")
460460

461+
assert "sentry.trace.parent_span_id" in attributes
462+
assert isinstance(attributes["sentry.trace.parent_span_id"], str)
463+
del attributes["sentry.trace.parent_span_id"]
464+
461465
# Assert on the remaining non-dynamic attributes.
462466
assert attributes == {
463467
"logger.name": "tests.integrations.loguru.test_loguru",

tests/test_logs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ def test_logs_tied_to_transactions(sentry_init, capture_envelopes):
310310
"""
311311
Log messages are also tied to transactions.
312312
"""
313-
sentry_init(enable_logs=True)
313+
sentry_init(enable_logs=True, traces_sample_rate=1.0)
314314
envelopes = capture_envelopes()
315315

316316
with sentry_sdk.start_transaction(name="test-transaction") as trx:
@@ -326,7 +326,7 @@ def test_logs_tied_to_spans(sentry_init, capture_envelopes):
326326
"""
327327
Log messages are also tied to spans.
328328
"""
329-
sentry_init(enable_logs=True)
329+
sentry_init(enable_logs=True, traces_sample_rate=1.0)
330330
envelopes = capture_envelopes()
331331

332332
with sentry_sdk.start_transaction(name="test-transaction"):

tests/test_metrics.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def test_metrics_with_span(sentry_init, capture_envelopes):
122122
sentry_init(traces_sample_rate=1.0)
123123
envelopes = capture_envelopes()
124124

125-
with sentry_sdk.start_transaction(op="test", name="test-span"):
125+
with sentry_sdk.start_transaction(op="test", name="test-span") as transaction:
126126
sentry_sdk.metrics.count("test.span.counter", 1)
127127

128128
get_client().flush()
@@ -131,24 +131,26 @@ def test_metrics_with_span(sentry_init, capture_envelopes):
131131
assert len(metrics) == 1
132132

133133
assert metrics[0]["trace_id"] is not None
134-
assert metrics[0]["trace_id"] != "00000000-0000-0000-0000-000000000000"
135-
assert metrics[0]["span_id"] is not None
134+
assert metrics[0]["trace_id"] == transaction.trace_id
135+
assert metrics[0]["span_id"] == transaction.span_id
136136

137137

138138
def test_metrics_tracing_without_performance(sentry_init, capture_envelopes):
139139
sentry_init()
140140
envelopes = capture_envelopes()
141141

142-
sentry_sdk.metrics.count("test.span.counter", 1)
142+
with sentry_sdk.isolation_scope() as isolation_scope:
143+
sentry_sdk.metrics.count("test.span.counter", 1)
143144

144145
get_client().flush()
145146

146147
metrics = envelopes_to_metrics(envelopes)
147148
assert len(metrics) == 1
148149

149-
assert metrics[0]["trace_id"] is not None
150-
assert metrics[0]["trace_id"] != "00000000-0000-0000-0000-000000000000"
151-
assert metrics[0]["span_id"] is None
150+
propagation_context = isolation_scope._propagation_context
151+
assert propagation_context is not None
152+
assert metrics[0]["trace_id"] == propagation_context.trace_id
153+
assert metrics[0]["span_id"] == propagation_context.span_id
152154

153155

154156
def test_metrics_before_send(sentry_init, capture_envelopes):

tests/test_scope.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use_isolation_scope,
1717
use_scope,
1818
should_send_default_pii,
19+
register_external_propagation_context,
20+
remove_external_propagation_context,
1921
)
2022

2123

@@ -971,3 +973,47 @@ def test_handle_error_on_token_reset_isolation_scope(error_cls, scope_manager):
971973

972974
mock_capture.assert_called_once()
973975
mock_current_scope.reset.assert_called_once_with(mock_current_token)
976+
977+
978+
def test_trace_context_tracing(sentry_init):
979+
sentry_init(traces_sample_rate=1.0)
980+
981+
with sentry_sdk.start_transaction(name="trx") as transaction:
982+
with sentry_sdk.start_span(op="span1"):
983+
with sentry_sdk.start_span(op="span2") as span:
984+
trace_context = sentry_sdk.get_current_scope().get_trace_context()
985+
986+
assert trace_context["trace_id"] == transaction.trace_id
987+
assert trace_context["span_id"] == span.span_id
988+
assert trace_context["parent_span_id"] == span.parent_span_id
989+
assert "dynamic_sampling_context" in trace_context
990+
991+
992+
def test_trace_context_external_tracing(sentry_init):
993+
sentry_init()
994+
995+
def external_propagation_context():
996+
return ("trace_id_foo", "span_id_bar")
997+
998+
register_external_propagation_context(external_propagation_context)
999+
1000+
trace_context = sentry_sdk.get_current_scope().get_trace_context()
1001+
1002+
assert trace_context["trace_id"] == "trace_id_foo"
1003+
assert trace_context["span_id"] == "span_id_bar"
1004+
1005+
remove_external_propagation_context()
1006+
1007+
1008+
def test_trace_context_without_performance(sentry_init):
1009+
sentry_init()
1010+
1011+
with sentry_sdk.isolation_scope() as isolation_scope:
1012+
trace_context = sentry_sdk.get_current_scope().get_trace_context()
1013+
1014+
propagation_context = isolation_scope._propagation_context
1015+
assert propagation_context is not None
1016+
assert trace_context["trace_id"] == propagation_context.trace_id
1017+
assert trace_context["span_id"] == propagation_context.span_id
1018+
assert trace_context["parent_span_id"] == propagation_context.parent_span_id
1019+
assert "dynamic_sampling_context" in trace_context

0 commit comments

Comments
 (0)