sentry-sdk 2.30.0__py2.py3-none-any.whl → 3.0.0a2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sentry-sdk might be problematic. Click here for more details.

Files changed (109) hide show
  1. sentry_sdk/__init__.py +3 -8
  2. sentry_sdk/_compat.py +0 -1
  3. sentry_sdk/_init_implementation.py +6 -44
  4. sentry_sdk/_types.py +2 -64
  5. sentry_sdk/ai/monitoring.py +14 -10
  6. sentry_sdk/ai/utils.py +1 -1
  7. sentry_sdk/api.py +56 -169
  8. sentry_sdk/client.py +27 -72
  9. sentry_sdk/consts.py +60 -23
  10. sentry_sdk/debug.py +0 -10
  11. sentry_sdk/envelope.py +1 -3
  12. sentry_sdk/feature_flags.py +1 -1
  13. sentry_sdk/integrations/__init__.py +4 -2
  14. sentry_sdk/integrations/_asgi_common.py +5 -6
  15. sentry_sdk/integrations/_wsgi_common.py +11 -40
  16. sentry_sdk/integrations/aiohttp.py +104 -57
  17. sentry_sdk/integrations/anthropic.py +10 -7
  18. sentry_sdk/integrations/arq.py +24 -13
  19. sentry_sdk/integrations/asgi.py +102 -83
  20. sentry_sdk/integrations/asyncio.py +1 -0
  21. sentry_sdk/integrations/asyncpg.py +45 -30
  22. sentry_sdk/integrations/aws_lambda.py +109 -92
  23. sentry_sdk/integrations/boto3.py +38 -9
  24. sentry_sdk/integrations/bottle.py +1 -1
  25. sentry_sdk/integrations/celery/__init__.py +51 -41
  26. sentry_sdk/integrations/clickhouse_driver.py +59 -28
  27. sentry_sdk/integrations/cohere.py +2 -0
  28. sentry_sdk/integrations/django/__init__.py +25 -46
  29. sentry_sdk/integrations/django/asgi.py +6 -2
  30. sentry_sdk/integrations/django/caching.py +13 -22
  31. sentry_sdk/integrations/django/middleware.py +1 -0
  32. sentry_sdk/integrations/django/signals_handlers.py +3 -1
  33. sentry_sdk/integrations/django/templates.py +8 -12
  34. sentry_sdk/integrations/django/transactions.py +1 -6
  35. sentry_sdk/integrations/django/views.py +5 -2
  36. sentry_sdk/integrations/falcon.py +7 -25
  37. sentry_sdk/integrations/fastapi.py +3 -3
  38. sentry_sdk/integrations/flask.py +1 -1
  39. sentry_sdk/integrations/gcp.py +63 -38
  40. sentry_sdk/integrations/graphene.py +6 -13
  41. sentry_sdk/integrations/grpc/aio/client.py +14 -8
  42. sentry_sdk/integrations/grpc/aio/server.py +19 -21
  43. sentry_sdk/integrations/grpc/client.py +8 -6
  44. sentry_sdk/integrations/grpc/server.py +12 -14
  45. sentry_sdk/integrations/httpx.py +47 -12
  46. sentry_sdk/integrations/huey.py +26 -22
  47. sentry_sdk/integrations/huggingface_hub.py +1 -0
  48. sentry_sdk/integrations/langchain.py +22 -15
  49. sentry_sdk/integrations/litestar.py +4 -2
  50. sentry_sdk/integrations/logging.py +7 -2
  51. sentry_sdk/integrations/openai.py +2 -0
  52. sentry_sdk/integrations/pymongo.py +18 -25
  53. sentry_sdk/integrations/pyramid.py +1 -1
  54. sentry_sdk/integrations/quart.py +3 -3
  55. sentry_sdk/integrations/ray.py +23 -17
  56. sentry_sdk/integrations/redis/_async_common.py +29 -18
  57. sentry_sdk/integrations/redis/_sync_common.py +28 -19
  58. sentry_sdk/integrations/redis/modules/caches.py +13 -10
  59. sentry_sdk/integrations/redis/modules/queries.py +14 -11
  60. sentry_sdk/integrations/redis/rb.py +4 -4
  61. sentry_sdk/integrations/redis/redis.py +6 -6
  62. sentry_sdk/integrations/redis/redis_cluster.py +18 -18
  63. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +4 -4
  64. sentry_sdk/integrations/redis/utils.py +64 -24
  65. sentry_sdk/integrations/rq.py +68 -23
  66. sentry_sdk/integrations/rust_tracing.py +28 -43
  67. sentry_sdk/integrations/sanic.py +23 -13
  68. sentry_sdk/integrations/socket.py +9 -5
  69. sentry_sdk/integrations/sqlalchemy.py +8 -8
  70. sentry_sdk/integrations/starlette.py +11 -31
  71. sentry_sdk/integrations/starlite.py +4 -2
  72. sentry_sdk/integrations/stdlib.py +56 -9
  73. sentry_sdk/integrations/strawberry.py +40 -59
  74. sentry_sdk/integrations/threading.py +10 -26
  75. sentry_sdk/integrations/tornado.py +57 -18
  76. sentry_sdk/integrations/trytond.py +4 -1
  77. sentry_sdk/integrations/wsgi.py +84 -38
  78. sentry_sdk/opentelemetry/__init__.py +9 -0
  79. sentry_sdk/opentelemetry/consts.py +33 -0
  80. sentry_sdk/opentelemetry/contextvars_context.py +81 -0
  81. sentry_sdk/{integrations/opentelemetry → opentelemetry}/propagator.py +19 -28
  82. sentry_sdk/opentelemetry/sampler.py +326 -0
  83. sentry_sdk/opentelemetry/scope.py +218 -0
  84. sentry_sdk/opentelemetry/span_processor.py +335 -0
  85. sentry_sdk/opentelemetry/tracing.py +59 -0
  86. sentry_sdk/opentelemetry/utils.py +484 -0
  87. sentry_sdk/profiler/__init__.py +0 -40
  88. sentry_sdk/profiler/continuous_profiler.py +1 -30
  89. sentry_sdk/profiler/transaction_profiler.py +5 -56
  90. sentry_sdk/scope.py +108 -361
  91. sentry_sdk/sessions.py +0 -87
  92. sentry_sdk/tracing.py +415 -1161
  93. sentry_sdk/tracing_utils.py +130 -166
  94. sentry_sdk/transport.py +4 -104
  95. sentry_sdk/utils.py +169 -152
  96. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/METADATA +3 -5
  97. sentry_sdk-3.0.0a2.dist-info/RECORD +154 -0
  98. sentry_sdk-3.0.0a2.dist-info/entry_points.txt +2 -0
  99. sentry_sdk/hub.py +0 -739
  100. sentry_sdk/integrations/opentelemetry/__init__.py +0 -7
  101. sentry_sdk/integrations/opentelemetry/consts.py +0 -5
  102. sentry_sdk/integrations/opentelemetry/integration.py +0 -58
  103. sentry_sdk/integrations/opentelemetry/span_processor.py +0 -391
  104. sentry_sdk/metrics.py +0 -965
  105. sentry_sdk-2.30.0.dist-info/RECORD +0 -152
  106. sentry_sdk-2.30.0.dist-info/entry_points.txt +0 -2
  107. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/WHEEL +0 -0
  108. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/licenses/LICENSE +0 -0
  109. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,218 @@
1
+ from typing import cast
2
+ from contextlib import contextmanager
3
+ import warnings
4
+
5
+ from opentelemetry.context import (
6
+ get_value,
7
+ set_value,
8
+ attach,
9
+ detach,
10
+ get_current,
11
+ )
12
+ from opentelemetry.trace import (
13
+ SpanContext,
14
+ NonRecordingSpan,
15
+ TraceFlags,
16
+ TraceState,
17
+ use_span,
18
+ )
19
+
20
+ from sentry_sdk.opentelemetry.consts import (
21
+ SENTRY_SCOPES_KEY,
22
+ SENTRY_FORK_ISOLATION_SCOPE_KEY,
23
+ SENTRY_USE_CURRENT_SCOPE_KEY,
24
+ SENTRY_USE_ISOLATION_SCOPE_KEY,
25
+ TRACESTATE_SAMPLED_KEY,
26
+ )
27
+ from sentry_sdk.opentelemetry.contextvars_context import (
28
+ SentryContextVarsRuntimeContext,
29
+ )
30
+ from sentry_sdk.opentelemetry.utils import trace_state_from_baggage
31
+ from sentry_sdk.scope import Scope, ScopeType
32
+ from sentry_sdk.tracing import Span
33
+ from sentry_sdk._types import TYPE_CHECKING
34
+
35
+ if TYPE_CHECKING:
36
+ from typing import Tuple, Optional, Generator, Dict, Any
37
+
38
+
39
+ class PotelScope(Scope):
40
+ @classmethod
41
+ def _get_scopes(cls):
42
+ # type: () -> Optional[Tuple[PotelScope, PotelScope]]
43
+ """
44
+ Returns the current scopes tuple on the otel context. Internal use only.
45
+ """
46
+ return cast(
47
+ "Optional[Tuple[PotelScope, PotelScope]]", get_value(SENTRY_SCOPES_KEY)
48
+ )
49
+
50
+ @classmethod
51
+ def get_current_scope(cls):
52
+ # type: () -> PotelScope
53
+ """
54
+ Returns the current scope.
55
+ """
56
+ return cls._get_current_scope() or _INITIAL_CURRENT_SCOPE
57
+
58
+ @classmethod
59
+ def _get_current_scope(cls):
60
+ # type: () -> Optional[PotelScope]
61
+ """
62
+ Returns the current scope without creating a new one. Internal use only.
63
+ """
64
+ scopes = cls._get_scopes()
65
+ return scopes[0] if scopes else None
66
+
67
+ @classmethod
68
+ def get_isolation_scope(cls):
69
+ # type: () -> PotelScope
70
+ """
71
+ Returns the isolation scope.
72
+ """
73
+ return cls._get_isolation_scope() or _INITIAL_ISOLATION_SCOPE
74
+
75
+ @classmethod
76
+ def _get_isolation_scope(cls):
77
+ # type: () -> Optional[PotelScope]
78
+ """
79
+ Returns the isolation scope without creating a new one. Internal use only.
80
+ """
81
+ scopes = cls._get_scopes()
82
+ return scopes[1] if scopes else None
83
+
84
+ @contextmanager
85
+ def continue_trace(self, environ_or_headers):
86
+ # type: (Dict[str, Any]) -> Generator[None, None, None]
87
+ """
88
+ Sets the propagation context from environment or headers to continue an incoming trace.
89
+ Any span started within this context manager will use the same trace_id, parent_span_id
90
+ and inherit the sampling decision from the incoming trace.
91
+ """
92
+ self.generate_propagation_context(environ_or_headers)
93
+
94
+ span_context = self._incoming_otel_span_context()
95
+ if span_context is None:
96
+ yield
97
+ else:
98
+ with use_span(NonRecordingSpan(span_context)):
99
+ yield
100
+
101
+ def _incoming_otel_span_context(self):
102
+ # type: () -> Optional[SpanContext]
103
+ if self._propagation_context is None:
104
+ return None
105
+ # If sentry-trace extraction didn't have a parent_span_id, we don't have an upstream header
106
+ if self._propagation_context.parent_span_id is None:
107
+ return None
108
+
109
+ trace_flags = TraceFlags(
110
+ TraceFlags.SAMPLED
111
+ if self._propagation_context.parent_sampled
112
+ else TraceFlags.DEFAULT
113
+ )
114
+
115
+ if self._propagation_context.baggage:
116
+ trace_state = trace_state_from_baggage(self._propagation_context.baggage)
117
+ else:
118
+ trace_state = TraceState()
119
+
120
+ # for twp to work, we also need to consider deferred sampling when the sampling
121
+ # flag is not present, so the above TraceFlags are not sufficient
122
+ if self._propagation_context.parent_sampled is None:
123
+ trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, "deferred")
124
+
125
+ span_context = SpanContext(
126
+ trace_id=int(self._propagation_context.trace_id, 16),
127
+ span_id=int(self._propagation_context.parent_span_id, 16),
128
+ is_remote=True,
129
+ trace_flags=trace_flags,
130
+ trace_state=trace_state,
131
+ )
132
+
133
+ return span_context
134
+
135
+ def start_transaction(self, **kwargs):
136
+ # type: (Any) -> Span
137
+ """
138
+ .. deprecated:: 3.0.0
139
+ This function is deprecated and will be removed in a future release.
140
+ Use :py:meth:`sentry_sdk.start_span` instead.
141
+ """
142
+ warnings.warn(
143
+ "The `start_transaction` method is deprecated, please use `sentry_sdk.start_span instead.`",
144
+ DeprecationWarning,
145
+ stacklevel=2,
146
+ )
147
+ return self.start_span(**kwargs)
148
+
149
+ def start_span(self, **kwargs):
150
+ # type: (Any) -> Span
151
+ return Span(**kwargs)
152
+
153
+
154
+ _INITIAL_CURRENT_SCOPE = PotelScope(ty=ScopeType.CURRENT)
155
+ _INITIAL_ISOLATION_SCOPE = PotelScope(ty=ScopeType.ISOLATION)
156
+
157
+
158
+ def setup_initial_scopes():
159
+ # type: () -> None
160
+ global _INITIAL_CURRENT_SCOPE, _INITIAL_ISOLATION_SCOPE
161
+ _INITIAL_CURRENT_SCOPE = PotelScope(ty=ScopeType.CURRENT)
162
+ _INITIAL_ISOLATION_SCOPE = PotelScope(ty=ScopeType.ISOLATION)
163
+
164
+ scopes = (_INITIAL_CURRENT_SCOPE, _INITIAL_ISOLATION_SCOPE)
165
+ attach(set_value(SENTRY_SCOPES_KEY, scopes))
166
+
167
+
168
+ def setup_scope_context_management():
169
+ # type: () -> None
170
+ import opentelemetry.context
171
+
172
+ opentelemetry.context._RUNTIME_CONTEXT = SentryContextVarsRuntimeContext()
173
+ setup_initial_scopes()
174
+
175
+
176
+ @contextmanager
177
+ def isolation_scope():
178
+ # type: () -> Generator[PotelScope, None, None]
179
+ context = set_value(SENTRY_FORK_ISOLATION_SCOPE_KEY, True)
180
+ token = attach(context)
181
+ try:
182
+ yield PotelScope.get_isolation_scope()
183
+ finally:
184
+ detach(token)
185
+
186
+
187
+ @contextmanager
188
+ def new_scope():
189
+ # type: () -> Generator[PotelScope, None, None]
190
+ token = attach(get_current())
191
+ try:
192
+ yield PotelScope.get_current_scope()
193
+ finally:
194
+ detach(token)
195
+
196
+
197
+ @contextmanager
198
+ def use_scope(scope):
199
+ # type: (PotelScope) -> Generator[PotelScope, None, None]
200
+ context = set_value(SENTRY_USE_CURRENT_SCOPE_KEY, scope)
201
+ token = attach(context)
202
+
203
+ try:
204
+ yield scope
205
+ finally:
206
+ detach(token)
207
+
208
+
209
+ @contextmanager
210
+ def use_isolation_scope(isolation_scope):
211
+ # type: (PotelScope) -> Generator[PotelScope, None, None]
212
+ context = set_value(SENTRY_USE_ISOLATION_SCOPE_KEY, isolation_scope)
213
+ token = attach(context)
214
+
215
+ try:
216
+ yield isolation_scope
217
+ finally:
218
+ detach(token)
@@ -0,0 +1,335 @@
1
+ from collections import deque, defaultdict
2
+ from typing import cast
3
+
4
+ from opentelemetry.trace import (
5
+ format_trace_id,
6
+ format_span_id,
7
+ get_current_span,
8
+ INVALID_SPAN,
9
+ Span as AbstractSpan,
10
+ )
11
+ from opentelemetry.context import Context
12
+ from opentelemetry.sdk.trace import Span, ReadableSpan, SpanProcessor
13
+
14
+ import sentry_sdk
15
+ from sentry_sdk.consts import SPANDATA, DEFAULT_SPAN_ORIGIN
16
+ from sentry_sdk.utils import get_current_thread_meta
17
+ from sentry_sdk.opentelemetry.consts import (
18
+ OTEL_SENTRY_CONTEXT,
19
+ SentrySpanAttribute,
20
+ )
21
+ from sentry_sdk.opentelemetry.sampler import create_sampling_context
22
+ from sentry_sdk.opentelemetry.utils import (
23
+ is_sentry_span,
24
+ convert_from_otel_timestamp,
25
+ extract_span_attributes,
26
+ extract_span_data,
27
+ extract_transaction_name_source,
28
+ get_trace_context,
29
+ get_profile_context,
30
+ get_sentry_meta,
31
+ set_sentry_meta,
32
+ delete_sentry_meta,
33
+ )
34
+ from sentry_sdk.profiler.continuous_profiler import (
35
+ try_autostart_continuous_profiler,
36
+ get_profiler_id,
37
+ try_profile_lifecycle_trace_start,
38
+ )
39
+ from sentry_sdk.profiler.transaction_profiler import Profile
40
+ from sentry_sdk._types import TYPE_CHECKING
41
+
42
+ if TYPE_CHECKING:
43
+ from typing import Optional, List, Any, Deque, DefaultDict
44
+ from sentry_sdk._types import Event
45
+
46
+
47
+ DEFAULT_MAX_SPANS = 1000
48
+
49
+
50
+ class SentrySpanProcessor(SpanProcessor):
51
+ """
52
+ Converts OTel spans into Sentry spans so they can be sent to the Sentry backend.
53
+ """
54
+
55
+ def __new__(cls):
56
+ # type: () -> SentrySpanProcessor
57
+ if not hasattr(cls, "instance"):
58
+ cls.instance = super().__new__(cls)
59
+
60
+ return cls.instance
61
+
62
+ def __init__(self):
63
+ # type: () -> None
64
+ self._children_spans = defaultdict(
65
+ list
66
+ ) # type: DefaultDict[int, List[ReadableSpan]]
67
+ self._dropped_spans = defaultdict(lambda: 0) # type: DefaultDict[int, int]
68
+
69
+ def on_start(self, span, parent_context=None):
70
+ # type: (Span, Optional[Context]) -> None
71
+ if is_sentry_span(span):
72
+ return
73
+
74
+ self._add_root_span(span, get_current_span(parent_context))
75
+ self._start_profile(span)
76
+
77
+ def on_end(self, span):
78
+ # type: (ReadableSpan) -> None
79
+ if is_sentry_span(span):
80
+ return
81
+
82
+ is_root_span = not span.parent or span.parent.is_remote
83
+ if is_root_span:
84
+ # if have a root span ending, stop the profiler, build a transaction and send it
85
+ self._stop_profile(span)
86
+ self._flush_root_span(span)
87
+ else:
88
+ self._append_child_span(span)
89
+
90
+ # TODO-neel-potel not sure we need a clear like JS
91
+ def shutdown(self):
92
+ # type: () -> None
93
+ pass
94
+
95
+ # TODO-neel-potel change default? this is 30 sec
96
+ # TODO-neel-potel call this in client.flush
97
+ def force_flush(self, timeout_millis=30000):
98
+ # type: (int) -> bool
99
+ return True
100
+
101
+ def _add_root_span(self, span, parent_span):
102
+ # type: (Span, AbstractSpan) -> None
103
+ """
104
+ This is required to make Span.root_span work
105
+ since we can't traverse back to the root purely with otel efficiently.
106
+ """
107
+ if parent_span != INVALID_SPAN and not parent_span.get_span_context().is_remote:
108
+ # child span points to parent's root or parent
109
+ parent_root_span = get_sentry_meta(parent_span, "root_span")
110
+ set_sentry_meta(span, "root_span", parent_root_span or parent_span)
111
+ else:
112
+ # root span points to itself
113
+ set_sentry_meta(span, "root_span", span)
114
+
115
+ def _start_profile(self, span):
116
+ # type: (Span) -> None
117
+ try_autostart_continuous_profiler()
118
+
119
+ profiler_id = get_profiler_id()
120
+ thread_id, thread_name = get_current_thread_meta()
121
+
122
+ if profiler_id:
123
+ span.set_attribute(SPANDATA.PROFILER_ID, profiler_id)
124
+ if thread_id:
125
+ span.set_attribute(SPANDATA.THREAD_ID, str(thread_id))
126
+ if thread_name:
127
+ span.set_attribute(SPANDATA.THREAD_NAME, thread_name)
128
+
129
+ is_root_span = not span.parent or span.parent.is_remote
130
+ sampled = span.context and span.context.trace_flags.sampled
131
+
132
+ if is_root_span and sampled:
133
+ # profiler uses time.perf_counter_ns() so we cannot use the
134
+ # unix timestamp that is on span.start_time
135
+ # setting it to 0 means the profiler will internally measure time on start
136
+ profile = Profile(sampled, 0)
137
+
138
+ sampling_context = create_sampling_context(
139
+ span.name, span.attributes, span.parent, span.context.trace_id
140
+ )
141
+ profile._set_initial_sampling_decision(sampling_context)
142
+ profile.__enter__()
143
+ set_sentry_meta(span, "profile", profile)
144
+
145
+ continuous_profile = try_profile_lifecycle_trace_start()
146
+ profiler_id = get_profiler_id()
147
+ if profiler_id:
148
+ span.set_attribute(SPANDATA.PROFILER_ID, profiler_id)
149
+ set_sentry_meta(span, "continuous_profile", continuous_profile)
150
+
151
+ def _stop_profile(self, span):
152
+ # type: (ReadableSpan) -> None
153
+ continuous_profiler = get_sentry_meta(span, "continuous_profile")
154
+ if continuous_profiler:
155
+ continuous_profiler.stop()
156
+
157
+ def _flush_root_span(self, span):
158
+ # type: (ReadableSpan) -> None
159
+ transaction_event = self._root_span_to_transaction_event(span)
160
+ if not transaction_event:
161
+ return
162
+
163
+ collected_spans, dropped_spans = self._collect_children(span)
164
+ spans = []
165
+ for child in collected_spans:
166
+ span_json = self._span_to_json(child)
167
+ if span_json:
168
+ spans.append(span_json)
169
+
170
+ transaction_event["spans"] = spans
171
+ if dropped_spans > 0:
172
+ transaction_event["_dropped_spans"] = dropped_spans
173
+
174
+ # TODO-neel-potel sort and cutoff max spans
175
+
176
+ sentry_sdk.capture_event(transaction_event)
177
+ self._cleanup_references([span] + collected_spans)
178
+
179
+ def _append_child_span(self, span):
180
+ # type: (ReadableSpan) -> None
181
+ if not span.parent:
182
+ return
183
+
184
+ max_spans = (
185
+ sentry_sdk.get_client().options["_experiments"].get("max_spans")
186
+ or DEFAULT_MAX_SPANS
187
+ )
188
+
189
+ children_spans = self._children_spans[span.parent.span_id]
190
+ if len(children_spans) < max_spans:
191
+ children_spans.append(span)
192
+ else:
193
+ self._dropped_spans[span.parent.span_id] += 1
194
+
195
+ def _collect_children(self, span):
196
+ # type: (ReadableSpan) -> tuple[List[ReadableSpan], int]
197
+ if not span.context:
198
+ return [], 0
199
+
200
+ children = []
201
+ dropped_spans = 0
202
+ bfs_queue = deque() # type: Deque[int]
203
+ bfs_queue.append(span.context.span_id)
204
+
205
+ while bfs_queue:
206
+ parent_span_id = bfs_queue.popleft()
207
+ node_children = self._children_spans.pop(parent_span_id, [])
208
+ dropped_spans += self._dropped_spans.pop(parent_span_id, 0)
209
+ children.extend(node_children)
210
+ bfs_queue.extend(
211
+ [child.context.span_id for child in node_children if child.context]
212
+ )
213
+
214
+ return children, dropped_spans
215
+
216
+ # we construct the event from scratch here
217
+ # and not use the current Transaction class for easier refactoring
218
+ def _root_span_to_transaction_event(self, span):
219
+ # type: (ReadableSpan) -> Optional[Event]
220
+ if not span.context:
221
+ return None
222
+
223
+ event = self._common_span_transaction_attributes_as_json(span)
224
+ if event is None:
225
+ return None
226
+
227
+ transaction_name, transaction_source = extract_transaction_name_source(span)
228
+ span_data = extract_span_data(span)
229
+ trace_context = get_trace_context(span, span_data=span_data)
230
+ contexts = {"trace": trace_context}
231
+
232
+ profile_context = get_profile_context(span)
233
+ if profile_context:
234
+ contexts["profile"] = profile_context
235
+
236
+ (_, description, _, http_status, _) = span_data
237
+
238
+ if http_status:
239
+ contexts["response"] = {"status_code": http_status}
240
+
241
+ if span.resource.attributes:
242
+ contexts[OTEL_SENTRY_CONTEXT] = {"resource": dict(span.resource.attributes)}
243
+
244
+ event.update(
245
+ {
246
+ "type": "transaction",
247
+ "transaction": transaction_name or description,
248
+ "transaction_info": {"source": transaction_source or "custom"},
249
+ "contexts": contexts,
250
+ }
251
+ )
252
+
253
+ profile = cast("Optional[Profile]", get_sentry_meta(span, "profile"))
254
+ if profile:
255
+ profile.__exit__(None, None, None)
256
+ if profile.valid():
257
+ event["profile"] = profile
258
+
259
+ return event
260
+
261
+ def _span_to_json(self, span):
262
+ # type: (ReadableSpan) -> Optional[dict[str, Any]]
263
+ if not span.context:
264
+ return None
265
+
266
+ # This is a safe cast because dict[str, Any] is a superset of Event
267
+ span_json = cast(
268
+ "dict[str, Any]", self._common_span_transaction_attributes_as_json(span)
269
+ )
270
+ if span_json is None:
271
+ return None
272
+
273
+ trace_id = format_trace_id(span.context.trace_id)
274
+ span_id = format_span_id(span.context.span_id)
275
+ parent_span_id = format_span_id(span.parent.span_id) if span.parent else None
276
+
277
+ (op, description, status, _, origin) = extract_span_data(span)
278
+
279
+ span_json.update(
280
+ {
281
+ "trace_id": trace_id,
282
+ "span_id": span_id,
283
+ "op": op,
284
+ "description": description,
285
+ "status": status,
286
+ "origin": origin or DEFAULT_SPAN_ORIGIN,
287
+ }
288
+ )
289
+
290
+ if parent_span_id:
291
+ span_json["parent_span_id"] = parent_span_id
292
+
293
+ attributes = getattr(span, "attributes", {}) or {}
294
+ if attributes:
295
+ span_json["data"] = {}
296
+ for key, value in attributes.items():
297
+ if not key.startswith("_"):
298
+ span_json["data"][key] = value
299
+
300
+ return span_json
301
+
302
+ def _common_span_transaction_attributes_as_json(self, span):
303
+ # type: (ReadableSpan) -> Optional[Event]
304
+ if not span.start_time or not span.end_time:
305
+ return None
306
+
307
+ common_json = {
308
+ "start_timestamp": convert_from_otel_timestamp(span.start_time),
309
+ "timestamp": convert_from_otel_timestamp(span.end_time),
310
+ } # type: Event
311
+
312
+ tags = extract_span_attributes(span, SentrySpanAttribute.TAG)
313
+ if tags:
314
+ common_json["tags"] = tags
315
+
316
+ return common_json
317
+
318
+ def _cleanup_references(self, spans):
319
+ # type: (List[ReadableSpan]) -> None
320
+ for span in spans:
321
+ delete_sentry_meta(span)
322
+
323
+ def _log_debug_info(self):
324
+ # type: () -> None
325
+ import pprint
326
+
327
+ pprint.pprint(
328
+ {
329
+ format_span_id(span_id): [
330
+ (format_span_id(child.context.span_id), child.name)
331
+ for child in children
332
+ ]
333
+ for span_id, children in self._children_spans.items()
334
+ }
335
+ )
@@ -0,0 +1,59 @@
1
+ from opentelemetry import trace
2
+ from opentelemetry.propagate import set_global_textmap
3
+ from opentelemetry.sdk.trace import TracerProvider, Span, ReadableSpan
4
+
5
+ from sentry_sdk.opentelemetry import (
6
+ SentryPropagator,
7
+ SentrySampler,
8
+ SentrySpanProcessor,
9
+ )
10
+ from sentry_sdk.utils import logger
11
+
12
+
13
+ def patch_readable_span():
14
+ # type: () -> None
15
+ """
16
+ We need to pass through sentry specific metadata/objects from Span to ReadableSpan
17
+ to work with them consistently in the SpanProcessor.
18
+ """
19
+ old_readable_span = Span._readable_span
20
+
21
+ def sentry_patched_readable_span(self):
22
+ # type: (Span) -> ReadableSpan
23
+ readable_span = old_readable_span(self)
24
+ readable_span._sentry_meta = getattr(self, "_sentry_meta", {}) # type: ignore[attr-defined]
25
+ return readable_span
26
+
27
+ Span._readable_span = sentry_patched_readable_span # type: ignore[method-assign]
28
+
29
+
30
+ def setup_sentry_tracing():
31
+ # type: () -> None
32
+ # TracerProvider can only be set once. If we're the first ones setting it,
33
+ # there's no issue. If it already exists, we need to patch it.
34
+ from opentelemetry.trace import _TRACER_PROVIDER
35
+
36
+ if _TRACER_PROVIDER is not None:
37
+ logger.debug("[Tracing] Detected an existing TracerProvider, patching")
38
+ tracer_provider = _TRACER_PROVIDER
39
+ tracer_provider.sampler = SentrySampler() # type: ignore[attr-defined]
40
+
41
+ else:
42
+ logger.debug("[Tracing] No TracerProvider set, creating a new one")
43
+ tracer_provider = TracerProvider(sampler=SentrySampler())
44
+ trace.set_tracer_provider(tracer_provider)
45
+
46
+ try:
47
+ existing_span_processors = (
48
+ tracer_provider._active_span_processor._span_processors # type: ignore[attr-defined]
49
+ )
50
+ except Exception:
51
+ existing_span_processors = []
52
+
53
+ for span_processor in existing_span_processors:
54
+ if isinstance(span_processor, SentrySpanProcessor):
55
+ break
56
+ else:
57
+ tracer_provider.add_span_processor(SentrySpanProcessor()) # type: ignore[attr-defined]
58
+
59
+ set_global_textmap(SentryPropagator())