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.
- sentry_sdk/__init__.py +3 -8
- sentry_sdk/_compat.py +0 -1
- sentry_sdk/_init_implementation.py +6 -44
- sentry_sdk/_types.py +2 -64
- sentry_sdk/ai/monitoring.py +14 -10
- sentry_sdk/ai/utils.py +1 -1
- sentry_sdk/api.py +56 -169
- sentry_sdk/client.py +27 -72
- sentry_sdk/consts.py +60 -23
- sentry_sdk/debug.py +0 -10
- sentry_sdk/envelope.py +1 -3
- sentry_sdk/feature_flags.py +1 -1
- sentry_sdk/integrations/__init__.py +4 -2
- sentry_sdk/integrations/_asgi_common.py +5 -6
- 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 +102 -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 +51 -41
- 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/litestar.py +4 -2
- sentry_sdk/integrations/logging.py +7 -2
- sentry_sdk/integrations/openai.py +2 -0
- 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 +29 -18
- sentry_sdk/integrations/redis/_sync_common.py +28 -19
- 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 -18
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +4 -4
- sentry_sdk/integrations/redis/utils.py +64 -24
- 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/wsgi.py +84 -38
- sentry_sdk/opentelemetry/__init__.py +9 -0
- sentry_sdk/opentelemetry/consts.py +33 -0
- sentry_sdk/opentelemetry/contextvars_context.py +81 -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 +335 -0
- sentry_sdk/opentelemetry/tracing.py +59 -0
- sentry_sdk/opentelemetry/utils.py +484 -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 +108 -361
- sentry_sdk/sessions.py +0 -87
- sentry_sdk/tracing.py +415 -1161
- sentry_sdk/tracing_utils.py +130 -166
- sentry_sdk/transport.py +4 -104
- sentry_sdk/utils.py +169 -152
- {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/METADATA +3 -5
- sentry_sdk-3.0.0a2.dist-info/RECORD +154 -0
- sentry_sdk-3.0.0a2.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.30.0.dist-info/RECORD +0 -152
- sentry_sdk-2.30.0.dist-info/entry_points.txt +0 -2
- {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/WHEEL +0 -0
- {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/licenses/LICENSE +0 -0
- {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import cast, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from opentelemetry.trace import get_current_span, set_span_in_context
|
|
4
|
+
from opentelemetry.trace.span import INVALID_SPAN
|
|
5
|
+
from opentelemetry.context import Context, get_value, set_value
|
|
6
|
+
from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext
|
|
7
|
+
|
|
8
|
+
import sentry_sdk
|
|
9
|
+
from sentry_sdk.tracing import Span
|
|
10
|
+
from sentry_sdk.opentelemetry.consts import (
|
|
11
|
+
SENTRY_SCOPES_KEY,
|
|
12
|
+
SENTRY_FORK_ISOLATION_SCOPE_KEY,
|
|
13
|
+
SENTRY_USE_CURRENT_SCOPE_KEY,
|
|
14
|
+
SENTRY_USE_ISOLATION_SCOPE_KEY,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from typing import Optional
|
|
19
|
+
from contextvars import Token
|
|
20
|
+
import sentry_sdk.opentelemetry.scope as scope
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SentryContextVarsRuntimeContext(ContextVarsRuntimeContext):
|
|
24
|
+
def attach(self, context):
|
|
25
|
+
# type: (Context) -> Token[Context]
|
|
26
|
+
scopes = get_value(SENTRY_SCOPES_KEY, context)
|
|
27
|
+
|
|
28
|
+
should_fork_isolation_scope = context.pop(
|
|
29
|
+
SENTRY_FORK_ISOLATION_SCOPE_KEY, False
|
|
30
|
+
)
|
|
31
|
+
should_fork_isolation_scope = cast("bool", should_fork_isolation_scope)
|
|
32
|
+
|
|
33
|
+
should_use_isolation_scope = context.pop(SENTRY_USE_ISOLATION_SCOPE_KEY, None)
|
|
34
|
+
should_use_isolation_scope = cast(
|
|
35
|
+
"Optional[scope.PotelScope]", should_use_isolation_scope
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
should_use_current_scope = context.pop(SENTRY_USE_CURRENT_SCOPE_KEY, None)
|
|
39
|
+
should_use_current_scope = cast(
|
|
40
|
+
"Optional[scope.PotelScope]", should_use_current_scope
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if scopes:
|
|
44
|
+
scopes = cast("tuple[scope.PotelScope, scope.PotelScope]", scopes)
|
|
45
|
+
(current_scope, isolation_scope) = scopes
|
|
46
|
+
else:
|
|
47
|
+
current_scope = sentry_sdk.get_current_scope()
|
|
48
|
+
isolation_scope = sentry_sdk.get_isolation_scope()
|
|
49
|
+
|
|
50
|
+
new_context = context
|
|
51
|
+
|
|
52
|
+
if should_use_current_scope:
|
|
53
|
+
new_scope = should_use_current_scope
|
|
54
|
+
|
|
55
|
+
# the main case where we use use_scope is for
|
|
56
|
+
# scope propagation in the ThreadingIntegration
|
|
57
|
+
# so we need to carry forward the span reference explicitly too
|
|
58
|
+
span = should_use_current_scope.span
|
|
59
|
+
if span:
|
|
60
|
+
new_context = set_span_in_context(span._otel_span, new_context)
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
new_scope = current_scope.fork()
|
|
64
|
+
|
|
65
|
+
# carry forward a wrapped span reference since the otel context is always the
|
|
66
|
+
# source of truth for the active span
|
|
67
|
+
current_span = get_current_span(context)
|
|
68
|
+
if current_span != INVALID_SPAN:
|
|
69
|
+
new_scope._span = Span(otel_span=get_current_span(context))
|
|
70
|
+
|
|
71
|
+
if should_use_isolation_scope:
|
|
72
|
+
new_isolation_scope = should_use_isolation_scope
|
|
73
|
+
elif should_fork_isolation_scope:
|
|
74
|
+
new_isolation_scope = isolation_scope.fork()
|
|
75
|
+
else:
|
|
76
|
+
new_isolation_scope = isolation_scope
|
|
77
|
+
|
|
78
|
+
new_scopes = (new_scope, new_isolation_scope)
|
|
79
|
+
|
|
80
|
+
new_context = set_value(SENTRY_SCOPES_KEY, new_scopes, new_context)
|
|
81
|
+
return super().attach(new_context)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
1
3
|
from opentelemetry import trace
|
|
2
4
|
from opentelemetry.context import (
|
|
3
5
|
Context,
|
|
4
6
|
get_current,
|
|
7
|
+
get_value,
|
|
5
8
|
set_value,
|
|
6
9
|
)
|
|
7
10
|
from opentelemetry.propagators.textmap import (
|
|
@@ -18,23 +21,22 @@ from opentelemetry.trace import (
|
|
|
18
21
|
TraceFlags,
|
|
19
22
|
)
|
|
20
23
|
|
|
21
|
-
from sentry_sdk.
|
|
22
|
-
SENTRY_BAGGAGE_KEY,
|
|
23
|
-
SENTRY_TRACE_KEY,
|
|
24
|
-
)
|
|
25
|
-
from sentry_sdk.integrations.opentelemetry.span_processor import (
|
|
26
|
-
SentrySpanProcessor,
|
|
27
|
-
)
|
|
28
|
-
from sentry_sdk.tracing import (
|
|
24
|
+
from sentry_sdk.consts import (
|
|
29
25
|
BAGGAGE_HEADER_NAME,
|
|
30
26
|
SENTRY_TRACE_HEADER_NAME,
|
|
31
27
|
)
|
|
28
|
+
from sentry_sdk.opentelemetry.consts import (
|
|
29
|
+
SENTRY_BAGGAGE_KEY,
|
|
30
|
+
SENTRY_TRACE_KEY,
|
|
31
|
+
SENTRY_SCOPES_KEY,
|
|
32
|
+
)
|
|
32
33
|
from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data
|
|
33
34
|
|
|
34
35
|
from typing import TYPE_CHECKING
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
37
38
|
from typing import Optional, Set
|
|
39
|
+
import sentry_sdk.opentelemetry.scope as scope
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
class SentryPropagator(TextMapPropagator):
|
|
@@ -47,6 +49,7 @@ class SentryPropagator(TextMapPropagator):
|
|
|
47
49
|
if context is None:
|
|
48
50
|
context = get_current()
|
|
49
51
|
|
|
52
|
+
# TODO-neel-potel cleanup with continue_trace / isolation_scope
|
|
50
53
|
sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME)
|
|
51
54
|
if not sentry_trace:
|
|
52
55
|
return context
|
|
@@ -89,27 +92,15 @@ class SentryPropagator(TextMapPropagator):
|
|
|
89
92
|
if context is None:
|
|
90
93
|
context = get_current()
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
span_id = trace.format_span_id(current_span_context.span_id)
|
|
99
|
-
|
|
100
|
-
span_map = SentrySpanProcessor().otel_span_map
|
|
101
|
-
sentry_span = span_map.get(span_id, None)
|
|
102
|
-
if not sentry_span:
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent())
|
|
95
|
+
scopes = get_value(SENTRY_SCOPES_KEY, context)
|
|
96
|
+
if scopes:
|
|
97
|
+
scopes = cast("tuple[scope.PotelScope, scope.PotelScope]", scopes)
|
|
98
|
+
(current_scope, _) = scopes
|
|
106
99
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if baggage_data:
|
|
112
|
-
setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data)
|
|
100
|
+
# TODO-neel-potel check trace_propagation_targets
|
|
101
|
+
# TODO-neel-potel test propagator works with twp
|
|
102
|
+
for key, value in current_scope.iter_trace_propagation_headers():
|
|
103
|
+
setter.set(carrier, key, value)
|
|
113
104
|
|
|
114
105
|
@property
|
|
115
106
|
def fields(self):
|
|
@@ -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
|