Skip to content

Commit 2f540eb

Browse files
sentrivanasl0thentr0pyszokeasaurusrex
authored
feat(potel): Make tracing APIs use OTel in the background (#3242)
* Skeletons for new components * Add simple scope management whenever a context is attached * create a new otel context `_SCOPES_KEY` that will hold a tuple of `(curent_scope, isolation_scope)` * the `current_scope` will always be forked (like on every span creation/context update in practice) * note that this is on `attach`, so not on all copy-on-write context object creation but only on apis such as [`trace.use_span`](https://github.com/open-telemetry/opentelemetry-python/blob/ba22b165471bde2037620f2c850ab648a849fbc0/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547) or [`tracer.start_as_current_span`](https://github.com/open-telemetry/opentelemetry-python/blob/ba22b165471bde2037620f2c850ab648a849fbc0/opentelemetry-api/src/opentelemetry/trace/__init__.py#L329) * basically every otel `context` fork corresponds to our `current_scope` fork * the `isolation_scope` currently will not be forked * these will later be updated, for instance when we update our top level scope apis that fork isolation scope, that will also have a corresponding change in this `attach` function * Don't parse DSN twice * wip * Skeletons for new components * Skeletons for new components * Add simple scope management whenever a context is attached * create a new otel context `_SCOPES_KEY` that will hold a tuple of `(curent_scope, isolation_scope)` * the `current_scope` will always be forked (like on every span creation/context update in practice) * note that this is on `attach`, so not on all copy-on-write context object creation but only on apis such as [`trace.use_span`](https://github.com/open-telemetry/opentelemetry-python/blob/ba22b165471bde2037620f2c850ab648a849fbc0/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547) or [`tracer.start_as_current_span`](https://github.com/open-telemetry/opentelemetry-python/blob/ba22b165471bde2037620f2c850ab648a849fbc0/opentelemetry-api/src/opentelemetry/trace/__init__.py#L329) * basically every otel `context` fork corresponds to our `current_scope` fork * the `isolation_scope` currently will not be forked * these will later be updated, for instance when we update our top level scope apis that fork isolation scope, that will also have a corresponding change in this `attach` function * mypy fixes * working span processor * lint * Port over op/description/status extraction * defaultdict * naive impl * wip * fix args * wip * remove extra docs * Add simple scope management whenever a context is attached (#3159) Add simple scope management whenever a context is attached * create a new otel context `_SCOPES_KEY` that will hold a tuple of `(curent_scope, isolation_scope)` * the `current_scope` will always be forked (like on every span creation/context update in practice) * note that this is on `attach`, so not on all copy-on-write context object creation but only on apis such as [`trace.use_span`](https://github.com/open-telemetry/opentelemetry-python/blob/ba22b165471bde2037620f2c850ab648a849fbc0/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547) or [`tracer.start_as_current_span`](https://github.com/open-telemetry/opentelemetry-python/blob/ba22b165471bde2037620f2c850ab648a849fbc0/opentelemetry-api/src/opentelemetry/trace/__init__.py#L329) * basically every otel `context` fork corresponds to our `current_scope` fork * the `isolation_scope` currently will not be forked * these will later be updated, for instance when we update our top level scope apis that fork isolation scope, that will also have a corresponding change in this `attach` function * Implement new POTel span processor (#3223) * only acts on `on_end` instead of both `on_start/on_end` as before * store children spans in a dict mapping `span_id -> children` * new dict only stores otel span objects and no sentry transaction/span objects so we save a bit of useless memory allocation * I'm not using our current `Transaction/Span` classes at all to build the event because when we add our APIs later, we'll need to rip these out and we also avoid having to deal with the `instrumenter` problem * if we get a root span (without parent), we recursively walk the dict and find the children and package up the transaction event and send it * I didn't do it like JS because I think this way is better * they [group an array of `finished_spans`](https://github.com/getsentry/sentry-javascript/blob/7e298036a21a5658f3eb9ba184165178c48d7ef8/packages/opentelemetry/src/spanExporter.ts#L132) every time a root span ends and I think this uses more cpu than what I did * and the dict like I used it doesn't take more space than the array either * if we get a span with a parent we just update the dict to find the span later * moved the common `is_sentry_span` logic to utils * Basic test cases for potel (#3286) * Proxy POTelSpan.set_data to underlying otel span attributes (#3297) * ref(tracing): Simplify backwards-compat code (#3379) With this change, we aim to simplify the backwards-compatibility code for POTel tracing. We do this as follows: - Remove `start_*` functions from `tracing` - Remove unused parameters from `tracing.POTelSpan.__init__`. - Make all parameters to `tracing.POTelSpan.__init__` kwarg-only. - Allow `tracing.POTelSpan.__init__` to accept arbitrary kwargs, which are all ignored, for compatibility with old `Span` interface. - Completely remove `start_inactive_span`, since inactive spans can be created by setting `active=False` when constructing a `POTelSpan`. * New Scope implementation based on OTel Context (#3389) * New `PotelScope` inherits from scope and reads the scope from the otel context key `SENTRY_SCOPES_KEY` * New `isolation_scope` and `new_scope` context managers just use the context manager forking and yield with the scopes living on the above context key * isolation scope forking is done with the `SENTRY_FORK_ISOLATION_SCOPE_KEY` boolean context key * Fix circular imports (#3431) * Random tweaks (#3437) * Origin improvements (#3432) * Tweak OTel timestamp utils (#3436) * Create spans on scope (#3442) * Fill out more property/method stubs (#3441) * Cleanup origin handling and defaults (#3445) * add note to migration guide * Attribute namespace for tags, measurements (#3448) --------- Co-authored-by: Neel Shah <neel.shah@sentry.io> Co-authored-by: Neel Shah <neelshah.sa@gmail.com> Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com>
1 parent 17a8494 commit 2f540eb

File tree

21 files changed

+1052
-160
lines changed

21 files changed

+1052
-160
lines changed

MIGRATION_GUIDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,26 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
99

1010
### Changed
1111

12+
- The SDK now supports Python 3.7 and higher.
13+
- `sentry_sdk.start_span` now only takes keyword arguments.
1214
- The `Span()` constructor does not accept a `hub` parameter anymore.
1315
- `Span.finish()` does not accept a `hub` parameter anymore.
1416
- The `Profile()` constructor does not accept a `hub` parameter anymore.
1517
- A `Profile` object does not have a `.hub` property anymore.
1618

1719
### Removed
1820

21+
- When setting span status, the HTTP status code is no longer automatically added as a tag.
1922
- Class `Hub` has been removed.
2023
- Class `_ScopeManager` has been removed.
2124
- The context manager `auto_session_tracking()` has been removed. Use `track_session()` instead.
2225
- The context manager `auto_session_tracking_scope()` has been removed. Use `track_session()` instead.
23-
- Utility function `is_auto_session_tracking_enabled()` has been removed. There is no public replacement. There is a private `_is_auto_session_tracking_enabled()` (if you absolutely need this function) It accepts a `scope` parameter instead of the previously used `hub` parameter.
26+
- Utility function `is_auto_session_tracking_enabled()` has been removed. There is no public replacement. There is a private `_is_auto_session_tracking_enabled()` (if you absolutely need this function) It accepts a `scope` parameter instead of the previously used `hub` parameter.
2427
- Utility function `is_auto_session_tracking_enabled_scope()` has been removed. There is no public replacement. There is a private `_is_auto_session_tracking_enabled()` (if you absolutely need this function)
2528

2629
### Deprecated
2730

31+
- `sentry_sdk.start_transaction` is deprecated. Use `sentry_sdk.start_span` instead.
2832

2933
## Upgrading to 2.0
3034

sentry_sdk/api.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
from sentry_sdk import tracing_utils, Client
44
from sentry_sdk._init_implementation import init
5-
from sentry_sdk.scope import Scope, new_scope, isolation_scope
6-
from sentry_sdk.tracing import NoOpSpan, Transaction, trace
5+
from sentry_sdk.tracing import POTelSpan, Transaction, trace
76
from sentry_sdk.crons import monitor
87

8+
# TODO-neel-potel make 2 scope strategies/impls and switch
9+
from sentry_sdk.integrations.opentelemetry.scope import (
10+
PotelScope as Scope,
11+
new_scope,
12+
isolation_scope,
13+
)
914

1015
from sentry_sdk._types import TYPE_CHECKING
1116

@@ -227,22 +232,40 @@ def flush(
227232
return get_client().flush(timeout=timeout, callback=callback)
228233

229234

230-
@scopemethod
231235
def start_span(
236+
*,
237+
span=None,
238+
custom_sampling_context=None,
232239
**kwargs, # type: Any
233240
):
234-
# type: (...) -> Span
235-
return get_current_scope().start_span(**kwargs)
241+
# type: (...) -> POTelSpan
242+
"""
243+
Start and return a span.
244+
245+
This is the entry point to manual tracing instrumentation.
246+
247+
A tree structure can be built by adding child spans to the span.
248+
To start a new child span within the span, call the `start_child()` method.
249+
250+
When used as a context manager, spans are automatically finished at the end
251+
of the `with` block. If not using context managers, call the `finish()`
252+
method.
253+
"""
254+
# TODO: Consider adding type hints to the method signature.
255+
return get_current_scope().start_span(span, custom_sampling_context, **kwargs)
236256

237257

238-
@scopemethod
239258
def start_transaction(
240259
transaction=None, # type: Optional[Transaction]
241260
custom_sampling_context=None, # type: Optional[SamplingContext]
242261
**kwargs, # type: Unpack[TransactionKwargs]
243262
):
244-
# type: (...) -> Union[Transaction, NoOpSpan]
263+
# type: (...) -> POTelSpan
245264
"""
265+
.. deprecated:: 3.0.0
266+
This function is deprecated and will be removed in a future release.
267+
Use :py:meth:`sentry_sdk.start_span` instead.
268+
246269
Start and return a transaction on the current scope.
247270
248271
Start an existing transaction if given, otherwise create and start a new
@@ -271,8 +294,10 @@ def start_transaction(
271294
constructor. See :py:class:`sentry_sdk.tracing.Transaction` for
272295
available arguments.
273296
"""
274-
return get_current_scope().start_transaction(
275-
transaction, custom_sampling_context, **kwargs
297+
return start_span(
298+
span=transaction,
299+
custom_sampling_context=custom_sampling_context,
300+
**kwargs,
276301
)
277302

278303

@@ -311,10 +336,8 @@ def get_baggage():
311336
return None
312337

313338

314-
def continue_trace(
315-
environ_or_headers, op=None, name=None, source=None, origin="manual"
316-
):
317-
# type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Transaction
339+
def continue_trace(environ_or_headers, op=None, name=None, source=None, origin=None):
340+
# type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], Optional[str]) -> Transaction
318341
"""
319342
Sets the propagation context from environment or headers and returns a transaction.
320343
"""

sentry_sdk/integrations/asgi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ def __init__(
9696
unsafe_context_data=False,
9797
transaction_style="endpoint",
9898
mechanism_type="asgi",
99-
span_origin="manual",
99+
span_origin=None,
100100
):
101-
# type: (Any, bool, str, str, str) -> None
101+
# type: (Any, bool, str, str, Optional[str]) -> None
102102
"""
103103
Instrument an ASGI application with Sentry. Provides HTTP/websocket
104104
data to sent events and basic handling for exceptions bubbling up

sentry_sdk/integrations/boto3.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import sentry_sdk
44
from sentry_sdk.consts import OP, SPANDATA
55
from sentry_sdk.integrations import Integration, DidNotEnable
6-
from sentry_sdk.tracing import Span
76

87
from sentry_sdk._types import TYPE_CHECKING
98
from sentry_sdk.utils import (
@@ -19,6 +18,8 @@
1918
from typing import Optional
2019
from typing import Type
2120

21+
from sentry_sdk.tracing import Span
22+
2223
try:
2324
from botocore import __version__ as BOTOCORE_VERSION # type: ignore
2425
from botocore.client import BaseClient # type: ignore
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
from opentelemetry.context import create_key
22

33

4+
# propagation keys
45
SENTRY_TRACE_KEY = create_key("sentry-trace")
56
SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")
7+
8+
# scope management keys
9+
SENTRY_SCOPES_KEY = create_key("sentry_scopes")
10+
SENTRY_FORK_ISOLATION_SCOPE_KEY = create_key("sentry_fork_isolation_scope")
11+
612
OTEL_SENTRY_CONTEXT = "otel"
713
SPAN_ORIGIN = "auto.otel"
14+
15+
16+
class SentrySpanAttribute:
17+
# XXX not all of these need separate attributes, we might just use
18+
# existing otel attrs for some
19+
DESCRIPTION = "sentry.description"
20+
OP = "sentry.op"
21+
ORIGIN = "sentry.origin"
22+
MEASUREMENT = "sentry.measurement"
23+
TAG = "sentry.tag"
Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1-
from opentelemetry.context import Context, create_key, get_value, set_value
1+
from opentelemetry.context import Context, get_value, set_value
22
from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext
33

4-
from sentry_sdk.scope import Scope
5-
6-
7-
_SCOPES_KEY = create_key("sentry_scopes")
4+
import sentry_sdk
5+
from sentry_sdk.integrations.opentelemetry.consts import (
6+
SENTRY_SCOPES_KEY,
7+
SENTRY_FORK_ISOLATION_SCOPE_KEY,
8+
)
89

910

1011
class SentryContextVarsRuntimeContext(ContextVarsRuntimeContext):
1112
def attach(self, context):
1213
# type: (Context) -> object
13-
scopes = get_value(_SCOPES_KEY, context)
14+
scopes = get_value(SENTRY_SCOPES_KEY, context)
15+
should_fork_isolation_scope = context.pop(
16+
SENTRY_FORK_ISOLATION_SCOPE_KEY, False
17+
)
1418

1519
if scopes and isinstance(scopes, tuple):
1620
(current_scope, isolation_scope) = scopes
1721
else:
18-
current_scope = Scope.get_current_scope()
19-
isolation_scope = Scope.get_isolation_scope()
22+
current_scope = sentry_sdk.get_current_scope()
23+
isolation_scope = sentry_sdk.get_isolation_scope()
2024

21-
# TODO-neel-potel fork isolation_scope too like JS
22-
# once we setup our own apis to pass through to otel
23-
new_scopes = (current_scope.fork(), isolation_scope)
24-
new_context = set_value(_SCOPES_KEY, new_scopes, context)
25+
new_scope = current_scope.fork()
26+
new_isolation_scope = (
27+
isolation_scope.fork() if should_fork_isolation_scope else isolation_scope
28+
)
29+
new_scopes = (new_scope, new_isolation_scope)
2530

31+
new_context = set_value(SENTRY_SCOPES_KEY, new_scopes, context)
2632
return super().attach(new_context)

sentry_sdk/integrations/opentelemetry/integration.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
from sentry_sdk.integrations import DidNotEnable, Integration
88
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
9-
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
9+
from sentry_sdk.integrations.opentelemetry.potel_span_processor import (
10+
PotelSentrySpanProcessor,
11+
)
12+
from sentry_sdk.integrations.opentelemetry.contextvars_context import (
13+
SentryContextVarsRuntimeContext,
14+
)
1015
from sentry_sdk.utils import logger
1116

1217
try:
@@ -46,9 +51,14 @@ def setup_once():
4651

4752
def _setup_sentry_tracing():
4853
# type: () -> None
54+
import opentelemetry.context
55+
56+
opentelemetry.context._RUNTIME_CONTEXT = SentryContextVarsRuntimeContext()
57+
4958
provider = TracerProvider()
50-
provider.add_span_processor(SentrySpanProcessor())
59+
provider.add_span_processor(PotelSentrySpanProcessor())
5160
trace.set_tracer_provider(provider)
61+
5262
set_global_textmap(SentryPropagator())
5363

5464

sentry_sdk/integrations/opentelemetry/potel_span_processor.py

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
from opentelemetry.sdk.trace import Span, ReadableSpan, SpanProcessor
66

77
from sentry_sdk import capture_event
8+
from sentry_sdk.tracing import DEFAULT_SPAN_ORIGIN
89
from sentry_sdk.integrations.opentelemetry.utils import (
910
is_sentry_span,
10-
convert_otel_timestamp,
11+
convert_from_otel_timestamp,
12+
extract_span_attributes,
1113
extract_span_data,
1214
)
1315
from sentry_sdk.integrations.opentelemetry.consts import (
1416
OTEL_SENTRY_CONTEXT,
15-
SPAN_ORIGIN,
17+
SentrySpanAttribute,
1618
)
1719
from sentry_sdk._types import TYPE_CHECKING
1820

@@ -107,21 +109,21 @@ def _root_span_to_transaction_event(self, span):
107109
# type: (ReadableSpan) -> Optional[Event]
108110
if not span.context:
109111
return None
110-
if not span.start_time:
111-
return None
112-
if not span.end_time:
112+
113+
event = self._common_span_transaction_attributes_as_json(span)
114+
if event is None:
113115
return None
114116

115117
trace_id = format_trace_id(span.context.trace_id)
116118
span_id = format_span_id(span.context.span_id)
117119
parent_span_id = format_span_id(span.parent.span_id) if span.parent else None
118120

119-
(op, description, status, _) = extract_span_data(span)
121+
(op, description, status, _, origin) = extract_span_data(span)
120122

121123
trace_context = {
122124
"trace_id": trace_id,
123125
"span_id": span_id,
124-
"origin": SPAN_ORIGIN,
126+
"origin": origin or DEFAULT_SPAN_ORIGIN,
125127
"op": op,
126128
"status": status,
127129
} # type: dict[str, Any]
@@ -135,47 +137,68 @@ def _root_span_to_transaction_event(self, span):
135137
if span.resource.attributes:
136138
contexts[OTEL_SENTRY_CONTEXT] = {"resource": dict(span.resource.attributes)}
137139

138-
event = {
139-
"type": "transaction",
140-
"transaction": description,
141-
# TODO-neel-potel tx source based on integration
142-
"transaction_info": {"source": "custom"},
143-
"contexts": contexts,
144-
"start_timestamp": convert_otel_timestamp(span.start_time),
145-
"timestamp": convert_otel_timestamp(span.end_time),
146-
} # type: Event
140+
event.update(
141+
{
142+
"type": "transaction",
143+
"transaction": description,
144+
# TODO-neel-potel tx source based on integration
145+
"transaction_info": {"source": "custom"},
146+
"contexts": contexts,
147+
}
148+
) # type: Event
147149

148150
return event
149151

150152
def _span_to_json(self, span):
151153
# type: (ReadableSpan) -> Optional[dict[str, Any]]
152154
if not span.context:
153155
return None
154-
if not span.start_time:
155-
return None
156-
if not span.end_time:
156+
157+
span_json = self._common_span_transaction_attributes_as_json(span)
158+
if span_json is None:
157159
return None
158160

159161
trace_id = format_trace_id(span.context.trace_id)
160162
span_id = format_span_id(span.context.span_id)
161163
parent_span_id = format_span_id(span.parent.span_id) if span.parent else None
162164

163-
(op, description, status, _) = extract_span_data(span)
165+
(op, description, status, _, origin) = extract_span_data(span)
164166

165-
span_json = {
166-
"trace_id": trace_id,
167-
"span_id": span_id,
168-
"origin": SPAN_ORIGIN,
169-
"op": op,
170-
"description": description,
171-
"status": status,
172-
"start_timestamp": convert_otel_timestamp(span.start_time),
173-
"timestamp": convert_otel_timestamp(span.end_time),
174-
} # type: dict[str, Any]
167+
span_json.update(
168+
{
169+
"trace_id": trace_id,
170+
"span_id": span_id,
171+
"op": op,
172+
"description": description,
173+
"status": status,
174+
"origin": origin or DEFAULT_SPAN_ORIGIN,
175+
}
176+
)
175177

176178
if parent_span_id:
177179
span_json["parent_span_id"] = parent_span_id
180+
178181
if span.attributes:
179182
span_json["data"] = dict(span.attributes)
180183

181184
return span_json
185+
186+
def _common_span_transaction_attributes_as_json(self, span):
187+
# type: (ReadableSpan) -> Optional[dict[str, Any]]
188+
if not span.start_time or not span.end_time:
189+
return None
190+
191+
common_json = {
192+
"start_timestamp": convert_from_otel_timestamp(span.start_time),
193+
"timestamp": convert_from_otel_timestamp(span.end_time),
194+
} # type: dict[str, Any]
195+
196+
measurements = extract_span_attributes(span, SentrySpanAttribute.MEASUREMENT)
197+
if measurements:
198+
common_json["measurements"] = measurements
199+
200+
tags = extract_span_attributes(span, SentrySpanAttribute.TAG)
201+
if tags:
202+
common_json["tags"] = tags
203+
204+
return common_json

0 commit comments

Comments
 (0)