sentry-sdk 2.26.1__py2.py3-none-any.whl → 3.0.0a1__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.
- sentry_sdk/__init__.py +4 -8
- sentry_sdk/_compat.py +0 -1
- sentry_sdk/_init_implementation.py +6 -44
- sentry_sdk/_log_batcher.py +47 -28
- sentry_sdk/_types.py +8 -64
- sentry_sdk/ai/monitoring.py +14 -10
- sentry_sdk/ai/utils.py +1 -1
- sentry_sdk/api.py +69 -163
- sentry_sdk/client.py +25 -72
- sentry_sdk/consts.py +42 -23
- sentry_sdk/debug.py +0 -10
- sentry_sdk/envelope.py +2 -10
- sentry_sdk/feature_flags.py +5 -1
- sentry_sdk/integrations/__init__.py +5 -2
- sentry_sdk/integrations/_asgi_common.py +3 -3
- sentry_sdk/integrations/_wsgi_common.py +11 -40
- sentry_sdk/integrations/aiohttp.py +104 -57
- sentry_sdk/integrations/anthropic.py +10 -7
- sentry_sdk/integrations/arq.py +24 -13
- sentry_sdk/integrations/asgi.py +103 -83
- sentry_sdk/integrations/asyncio.py +1 -0
- sentry_sdk/integrations/asyncpg.py +45 -30
- sentry_sdk/integrations/aws_lambda.py +109 -92
- sentry_sdk/integrations/boto3.py +38 -9
- sentry_sdk/integrations/bottle.py +1 -1
- sentry_sdk/integrations/celery/__init__.py +48 -38
- sentry_sdk/integrations/clickhouse_driver.py +59 -28
- sentry_sdk/integrations/cohere.py +2 -0
- sentry_sdk/integrations/django/__init__.py +25 -46
- sentry_sdk/integrations/django/asgi.py +6 -2
- sentry_sdk/integrations/django/caching.py +13 -22
- sentry_sdk/integrations/django/middleware.py +1 -0
- sentry_sdk/integrations/django/signals_handlers.py +3 -1
- sentry_sdk/integrations/django/templates.py +8 -12
- sentry_sdk/integrations/django/transactions.py +1 -6
- sentry_sdk/integrations/django/views.py +5 -2
- sentry_sdk/integrations/falcon.py +7 -25
- sentry_sdk/integrations/fastapi.py +3 -3
- sentry_sdk/integrations/flask.py +1 -1
- sentry_sdk/integrations/gcp.py +63 -38
- sentry_sdk/integrations/graphene.py +6 -13
- sentry_sdk/integrations/grpc/aio/client.py +14 -8
- sentry_sdk/integrations/grpc/aio/server.py +19 -21
- sentry_sdk/integrations/grpc/client.py +8 -6
- sentry_sdk/integrations/grpc/server.py +12 -14
- sentry_sdk/integrations/httpx.py +47 -12
- sentry_sdk/integrations/huey.py +26 -22
- sentry_sdk/integrations/huggingface_hub.py +1 -0
- sentry_sdk/integrations/langchain.py +22 -15
- sentry_sdk/integrations/launchdarkly.py +3 -3
- sentry_sdk/integrations/litestar.py +4 -2
- sentry_sdk/integrations/logging.py +12 -3
- sentry_sdk/integrations/openai.py +2 -0
- sentry_sdk/integrations/openfeature.py +3 -5
- sentry_sdk/integrations/pymongo.py +18 -25
- sentry_sdk/integrations/pyramid.py +1 -1
- sentry_sdk/integrations/quart.py +3 -3
- sentry_sdk/integrations/ray.py +23 -17
- sentry_sdk/integrations/redis/_async_common.py +30 -18
- sentry_sdk/integrations/redis/_sync_common.py +28 -18
- sentry_sdk/integrations/redis/modules/caches.py +13 -10
- sentry_sdk/integrations/redis/modules/queries.py +14 -11
- sentry_sdk/integrations/redis/rb.py +4 -4
- sentry_sdk/integrations/redis/redis.py +6 -6
- sentry_sdk/integrations/redis/redis_cluster.py +18 -16
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +4 -4
- sentry_sdk/integrations/redis/utils.py +63 -19
- sentry_sdk/integrations/rq.py +68 -23
- sentry_sdk/integrations/rust_tracing.py +28 -43
- sentry_sdk/integrations/sanic.py +23 -13
- sentry_sdk/integrations/socket.py +9 -5
- sentry_sdk/integrations/sqlalchemy.py +8 -8
- sentry_sdk/integrations/starlette.py +11 -31
- sentry_sdk/integrations/starlite.py +4 -2
- sentry_sdk/integrations/stdlib.py +56 -9
- sentry_sdk/integrations/strawberry.py +40 -59
- sentry_sdk/integrations/threading.py +10 -26
- sentry_sdk/integrations/tornado.py +57 -18
- sentry_sdk/integrations/trytond.py +4 -1
- sentry_sdk/integrations/unleash.py +2 -3
- sentry_sdk/integrations/wsgi.py +84 -38
- sentry_sdk/opentelemetry/__init__.py +9 -0
- sentry_sdk/opentelemetry/consts.py +33 -0
- sentry_sdk/opentelemetry/contextvars_context.py +73 -0
- sentry_sdk/{integrations/opentelemetry → opentelemetry}/propagator.py +19 -28
- sentry_sdk/opentelemetry/sampler.py +326 -0
- sentry_sdk/opentelemetry/scope.py +218 -0
- sentry_sdk/opentelemetry/span_processor.py +329 -0
- sentry_sdk/opentelemetry/tracing.py +35 -0
- sentry_sdk/opentelemetry/utils.py +476 -0
- sentry_sdk/profiler/__init__.py +0 -40
- sentry_sdk/profiler/continuous_profiler.py +1 -30
- sentry_sdk/profiler/transaction_profiler.py +5 -56
- sentry_sdk/scope.py +107 -351
- sentry_sdk/sessions.py +0 -87
- sentry_sdk/tracing.py +418 -1134
- sentry_sdk/tracing_utils.py +134 -169
- sentry_sdk/transport.py +4 -104
- sentry_sdk/types.py +26 -2
- sentry_sdk/utils.py +169 -152
- {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/METADATA +3 -5
- sentry_sdk-3.0.0a1.dist-info/RECORD +154 -0
- {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/WHEEL +1 -1
- sentry_sdk-3.0.0a1.dist-info/entry_points.txt +2 -0
- sentry_sdk/hub.py +0 -739
- sentry_sdk/integrations/opentelemetry/__init__.py +0 -7
- sentry_sdk/integrations/opentelemetry/consts.py +0 -5
- sentry_sdk/integrations/opentelemetry/integration.py +0 -58
- sentry_sdk/integrations/opentelemetry/span_processor.py +0 -391
- sentry_sdk/metrics.py +0 -965
- sentry_sdk-2.26.1.dist-info/RECORD +0 -152
- sentry_sdk-2.26.1.dist-info/entry_points.txt +0 -2
- {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/licenses/LICENSE +0 -0
- {sentry_sdk-2.26.1.dist-info → sentry_sdk-3.0.0a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
from opentelemetry.sdk.trace.sampling import Sampler, SamplingResult, Decision
|
|
6
|
+
from opentelemetry.trace.span import TraceState
|
|
7
|
+
|
|
8
|
+
import sentry_sdk
|
|
9
|
+
from sentry_sdk.opentelemetry.consts import (
|
|
10
|
+
TRACESTATE_SAMPLED_KEY,
|
|
11
|
+
TRACESTATE_SAMPLE_RAND_KEY,
|
|
12
|
+
TRACESTATE_SAMPLE_RATE_KEY,
|
|
13
|
+
SentrySpanAttribute,
|
|
14
|
+
)
|
|
15
|
+
from sentry_sdk.tracing_utils import (
|
|
16
|
+
_generate_sample_rand,
|
|
17
|
+
has_tracing_enabled,
|
|
18
|
+
)
|
|
19
|
+
from sentry_sdk.utils import is_valid_sample_rate, logger
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from typing import Any, Optional, Sequence, Union
|
|
25
|
+
from opentelemetry.context import Context
|
|
26
|
+
from opentelemetry.trace import Link, SpanKind
|
|
27
|
+
from opentelemetry.trace.span import SpanContext
|
|
28
|
+
from opentelemetry.util.types import Attributes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_parent_sampled(parent_context, trace_id):
|
|
32
|
+
# type: (Optional[SpanContext], int) -> Optional[bool]
|
|
33
|
+
if parent_context is None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
is_span_context_valid = parent_context is not None and parent_context.is_valid
|
|
37
|
+
|
|
38
|
+
# Only inherit sample rate if `traceId` is the same
|
|
39
|
+
if is_span_context_valid and parent_context.trace_id == trace_id:
|
|
40
|
+
# this is getSamplingDecision in JS
|
|
41
|
+
# if there was no sampling flag, defer the decision
|
|
42
|
+
dsc_sampled = parent_context.trace_state.get(TRACESTATE_SAMPLED_KEY)
|
|
43
|
+
if dsc_sampled == "deferred":
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if parent_context.trace_flags.sampled is not None:
|
|
47
|
+
return parent_context.trace_flags.sampled
|
|
48
|
+
|
|
49
|
+
if dsc_sampled == "true":
|
|
50
|
+
return True
|
|
51
|
+
elif dsc_sampled == "false":
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_parent_sample_rate(parent_context, trace_id):
|
|
58
|
+
# type: (Optional[SpanContext], int) -> Optional[float]
|
|
59
|
+
if parent_context is None:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
is_span_context_valid = parent_context is not None and parent_context.is_valid
|
|
63
|
+
|
|
64
|
+
if is_span_context_valid and parent_context.trace_id == trace_id:
|
|
65
|
+
parent_sample_rate = parent_context.trace_state.get(TRACESTATE_SAMPLE_RATE_KEY)
|
|
66
|
+
if parent_sample_rate is None:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
return float(parent_sample_rate)
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_parent_sample_rand(parent_context, trace_id):
|
|
78
|
+
# type: (Optional[SpanContext], int) -> Optional[Decimal]
|
|
79
|
+
if parent_context is None:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
is_span_context_valid = parent_context is not None and parent_context.is_valid
|
|
83
|
+
|
|
84
|
+
if is_span_context_valid and parent_context.trace_id == trace_id:
|
|
85
|
+
parent_sample_rand = parent_context.trace_state.get(TRACESTATE_SAMPLE_RAND_KEY)
|
|
86
|
+
if parent_sample_rand is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
return Decimal(parent_sample_rand)
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def dropped_result(span_context, attributes, sample_rate=None, sample_rand=None):
|
|
95
|
+
# type: (SpanContext, Attributes, Optional[float], Optional[Decimal]) -> SamplingResult
|
|
96
|
+
"""
|
|
97
|
+
React to a span getting unsampled and return a DROP SamplingResult.
|
|
98
|
+
|
|
99
|
+
Update the trace_state with the effective sampled, sample_rate and sample_rand,
|
|
100
|
+
record that we dropped the event for client report purposes, and return
|
|
101
|
+
an OTel SamplingResult with Decision.DROP.
|
|
102
|
+
|
|
103
|
+
See for more info about OTel sampling:
|
|
104
|
+
https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.sampling.html
|
|
105
|
+
"""
|
|
106
|
+
trace_state = _update_trace_state(
|
|
107
|
+
span_context, sampled=False, sample_rate=sample_rate, sample_rand=sample_rand
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
is_root_span = not (span_context.is_valid and not span_context.is_remote)
|
|
111
|
+
if is_root_span:
|
|
112
|
+
# Tell Sentry why we dropped the transaction/root-span
|
|
113
|
+
client = sentry_sdk.get_client()
|
|
114
|
+
if client.monitor and client.monitor.downsample_factor > 0:
|
|
115
|
+
reason = "backpressure"
|
|
116
|
+
else:
|
|
117
|
+
reason = "sample_rate"
|
|
118
|
+
|
|
119
|
+
if client.transport and has_tracing_enabled(client.options):
|
|
120
|
+
client.transport.record_lost_event(reason, data_category="transaction")
|
|
121
|
+
|
|
122
|
+
# Only one span (the transaction itself) is discarded, since we did not record any spans here.
|
|
123
|
+
client.transport.record_lost_event(reason, data_category="span")
|
|
124
|
+
|
|
125
|
+
return SamplingResult(
|
|
126
|
+
Decision.DROP,
|
|
127
|
+
attributes=attributes,
|
|
128
|
+
trace_state=trace_state,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def sampled_result(span_context, attributes, sample_rate=None, sample_rand=None):
|
|
133
|
+
# type: (SpanContext, Attributes, Optional[float], Optional[Decimal]) -> SamplingResult
|
|
134
|
+
"""
|
|
135
|
+
React to a span being sampled and return a sampled SamplingResult.
|
|
136
|
+
|
|
137
|
+
Update the trace_state with the effective sampled, sample_rate and sample_rand,
|
|
138
|
+
and return an OTel SamplingResult with Decision.RECORD_AND_SAMPLE.
|
|
139
|
+
|
|
140
|
+
See for more info about OTel sampling:
|
|
141
|
+
https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.sampling.html
|
|
142
|
+
"""
|
|
143
|
+
trace_state = _update_trace_state(
|
|
144
|
+
span_context, sampled=True, sample_rate=sample_rate, sample_rand=sample_rand
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return SamplingResult(
|
|
148
|
+
Decision.RECORD_AND_SAMPLE,
|
|
149
|
+
attributes=attributes,
|
|
150
|
+
trace_state=trace_state,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _update_trace_state(span_context, sampled, sample_rate=None, sample_rand=None):
|
|
155
|
+
# type: (SpanContext, bool, Optional[float], Optional[Decimal]) -> TraceState
|
|
156
|
+
trace_state = span_context.trace_state
|
|
157
|
+
|
|
158
|
+
sampled = "true" if sampled else "false"
|
|
159
|
+
if TRACESTATE_SAMPLED_KEY not in trace_state:
|
|
160
|
+
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, sampled)
|
|
161
|
+
elif trace_state.get(TRACESTATE_SAMPLED_KEY) == "deferred":
|
|
162
|
+
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, sampled)
|
|
163
|
+
|
|
164
|
+
if sample_rate is not None:
|
|
165
|
+
trace_state = trace_state.update(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))
|
|
166
|
+
|
|
167
|
+
if sample_rand is not None:
|
|
168
|
+
trace_state = trace_state.update(
|
|
169
|
+
TRACESTATE_SAMPLE_RAND_KEY, f"{sample_rand:.6f}" # noqa: E231
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return trace_state
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SentrySampler(Sampler):
|
|
176
|
+
def should_sample(
|
|
177
|
+
self,
|
|
178
|
+
parent_context, # type: Optional[Context]
|
|
179
|
+
trace_id, # type: int
|
|
180
|
+
name, # type: str
|
|
181
|
+
kind=None, # type: Optional[SpanKind]
|
|
182
|
+
attributes=None, # type: Attributes
|
|
183
|
+
links=None, # type: Optional[Sequence[Link]]
|
|
184
|
+
trace_state=None, # type: Optional[TraceState]
|
|
185
|
+
):
|
|
186
|
+
# type: (...) -> SamplingResult
|
|
187
|
+
client = sentry_sdk.get_client()
|
|
188
|
+
|
|
189
|
+
parent_span_context = trace.get_current_span(parent_context).get_span_context()
|
|
190
|
+
|
|
191
|
+
attributes = attributes or {}
|
|
192
|
+
|
|
193
|
+
# No tracing enabled, thus no sampling
|
|
194
|
+
if not has_tracing_enabled(client.options):
|
|
195
|
+
return dropped_result(parent_span_context, attributes)
|
|
196
|
+
|
|
197
|
+
# parent_span_context.is_valid means this span has a parent, remote or local
|
|
198
|
+
is_root_span = not parent_span_context.is_valid or parent_span_context.is_remote
|
|
199
|
+
|
|
200
|
+
sample_rate = None
|
|
201
|
+
|
|
202
|
+
parent_sampled = get_parent_sampled(parent_span_context, trace_id)
|
|
203
|
+
parent_sample_rate = get_parent_sample_rate(parent_span_context, trace_id)
|
|
204
|
+
parent_sample_rand = get_parent_sample_rand(parent_span_context, trace_id)
|
|
205
|
+
|
|
206
|
+
if parent_sample_rand is not None:
|
|
207
|
+
# We have a sample_rand on the incoming trace or we already backfilled
|
|
208
|
+
# it in PropagationContext
|
|
209
|
+
sample_rand = parent_sample_rand
|
|
210
|
+
else:
|
|
211
|
+
# We are the head SDK and we need to generate a new sample_rand
|
|
212
|
+
sample_rand = cast(Decimal, _generate_sample_rand(str(trace_id), (0, 1)))
|
|
213
|
+
|
|
214
|
+
# Explicit sampled value provided at start_span
|
|
215
|
+
custom_sampled = cast(
|
|
216
|
+
"Optional[bool]", attributes.get(SentrySpanAttribute.CUSTOM_SAMPLED)
|
|
217
|
+
)
|
|
218
|
+
if custom_sampled is not None:
|
|
219
|
+
if is_root_span:
|
|
220
|
+
sample_rate = float(custom_sampled)
|
|
221
|
+
if sample_rate > 0:
|
|
222
|
+
return sampled_result(
|
|
223
|
+
parent_span_context,
|
|
224
|
+
attributes,
|
|
225
|
+
sample_rate=sample_rate,
|
|
226
|
+
sample_rand=sample_rand,
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
return dropped_result(
|
|
230
|
+
parent_span_context,
|
|
231
|
+
attributes,
|
|
232
|
+
sample_rate=sample_rate,
|
|
233
|
+
sample_rand=sample_rand,
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
logger.debug(
|
|
237
|
+
f"[Tracing.Sampler] Ignoring sampled param for non-root span {name}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Check if there is a traces_sampler
|
|
241
|
+
# Traces_sampler is responsible to check parent sampled to have full transactions.
|
|
242
|
+
has_traces_sampler = callable(client.options.get("traces_sampler"))
|
|
243
|
+
|
|
244
|
+
sample_rate_to_propagate = None
|
|
245
|
+
|
|
246
|
+
if is_root_span and has_traces_sampler:
|
|
247
|
+
sampling_context = create_sampling_context(
|
|
248
|
+
name, attributes, parent_span_context, trace_id
|
|
249
|
+
)
|
|
250
|
+
sample_rate = client.options["traces_sampler"](sampling_context)
|
|
251
|
+
sample_rate_to_propagate = sample_rate
|
|
252
|
+
else:
|
|
253
|
+
# Check if there is a parent with a sampling decision
|
|
254
|
+
if parent_sampled is not None:
|
|
255
|
+
sample_rate = bool(parent_sampled)
|
|
256
|
+
sample_rate_to_propagate = (
|
|
257
|
+
parent_sample_rate if parent_sample_rate else sample_rate
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
# Check if there is a traces_sample_rate
|
|
261
|
+
sample_rate = client.options.get("traces_sample_rate")
|
|
262
|
+
sample_rate_to_propagate = sample_rate
|
|
263
|
+
|
|
264
|
+
# If the sample rate is invalid, drop the span
|
|
265
|
+
if not is_valid_sample_rate(sample_rate, source=self.__class__.__name__):
|
|
266
|
+
logger.warning(
|
|
267
|
+
f"[Tracing.Sampler] Discarding {name} because of invalid sample rate."
|
|
268
|
+
)
|
|
269
|
+
return dropped_result(parent_span_context, attributes)
|
|
270
|
+
|
|
271
|
+
# Down-sample in case of back pressure monitor says so
|
|
272
|
+
if is_root_span and client.monitor:
|
|
273
|
+
sample_rate /= 2**client.monitor.downsample_factor
|
|
274
|
+
if client.monitor.downsample_factor > 0:
|
|
275
|
+
sample_rate_to_propagate = sample_rate
|
|
276
|
+
|
|
277
|
+
# Compare sample_rand to sample_rate to make the final sampling decision
|
|
278
|
+
sample_rate = float(cast("Union[bool, float, int]", sample_rate))
|
|
279
|
+
sampled = sample_rand < Decimal.from_float(sample_rate)
|
|
280
|
+
|
|
281
|
+
if sampled:
|
|
282
|
+
if is_root_span:
|
|
283
|
+
logger.debug(
|
|
284
|
+
f"[Tracing.Sampler] Sampled #{name} with sample_rate: {sample_rate} and sample_rand: {sample_rand}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return sampled_result(
|
|
288
|
+
parent_span_context,
|
|
289
|
+
attributes,
|
|
290
|
+
sample_rate=sample_rate_to_propagate,
|
|
291
|
+
sample_rand=None if sample_rand == parent_sample_rand else sample_rand,
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
if is_root_span:
|
|
295
|
+
logger.debug(
|
|
296
|
+
f"[Tracing.Sampler] Dropped #{name} with sample_rate: {sample_rate} and sample_rand: {sample_rand}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return dropped_result(
|
|
300
|
+
parent_span_context,
|
|
301
|
+
attributes,
|
|
302
|
+
sample_rate=sample_rate_to_propagate,
|
|
303
|
+
sample_rand=None if sample_rand == parent_sample_rand else sample_rand,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def get_description(self) -> str:
|
|
307
|
+
return self.__class__.__name__
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def create_sampling_context(name, attributes, parent_span_context, trace_id):
|
|
311
|
+
# type: (str, Attributes, Optional[SpanContext], int) -> dict[str, Any]
|
|
312
|
+
sampling_context = {
|
|
313
|
+
"transaction_context": {
|
|
314
|
+
"name": name,
|
|
315
|
+
"op": attributes.get(SentrySpanAttribute.OP) if attributes else None,
|
|
316
|
+
"source": (
|
|
317
|
+
attributes.get(SentrySpanAttribute.SOURCE) if attributes else None
|
|
318
|
+
),
|
|
319
|
+
},
|
|
320
|
+
"parent_sampled": get_parent_sampled(parent_span_context, trace_id),
|
|
321
|
+
} # type: dict[str, Any]
|
|
322
|
+
|
|
323
|
+
if attributes is not None:
|
|
324
|
+
sampling_context.update(attributes)
|
|
325
|
+
|
|
326
|
+
return sampling_context
|
|
@@ -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)
|