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
sentry_sdk/tracing.py CHANGED
@@ -1,29 +1,58 @@
1
- from decimal import Decimal
2
- import uuid
1
+ from datetime import datetime
2
+ import json
3
3
  import warnings
4
- from datetime import datetime, timedelta, timezone
5
- from enum import Enum
6
4
 
7
- import sentry_sdk
8
- from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA
9
- from sentry_sdk.profiler.continuous_profiler import get_profiler_id
5
+ from opentelemetry import trace as otel_trace, context
6
+ from opentelemetry.trace import (
7
+ format_trace_id,
8
+ format_span_id,
9
+ Span as OtelSpan,
10
+ TraceState,
11
+ get_current_span,
12
+ INVALID_SPAN,
13
+ )
14
+ from opentelemetry.trace.status import Status, StatusCode
15
+ from opentelemetry.sdk.trace import ReadableSpan
16
+ from opentelemetry.version import __version__ as otel_version
17
+
18
+ from sentry_sdk.consts import (
19
+ DEFAULT_SPAN_NAME,
20
+ DEFAULT_SPAN_ORIGIN,
21
+ BAGGAGE_HEADER_NAME,
22
+ SENTRY_TRACE_HEADER_NAME,
23
+ SPANSTATUS,
24
+ SPANDATA,
25
+ TransactionSource,
26
+ )
27
+ from sentry_sdk.opentelemetry.consts import (
28
+ TRACESTATE_SAMPLE_RATE_KEY,
29
+ SentrySpanAttribute,
30
+ )
31
+ from sentry_sdk.opentelemetry.utils import (
32
+ baggage_from_trace_state,
33
+ convert_from_otel_timestamp,
34
+ convert_to_otel_timestamp,
35
+ get_trace_context,
36
+ get_trace_state,
37
+ get_sentry_meta,
38
+ serialize_trace_state,
39
+ )
40
+ from sentry_sdk.tracing_utils import get_span_status_from_http_code
10
41
  from sentry_sdk.utils import (
42
+ _serialize_span_attribute,
11
43
  get_current_thread_meta,
12
- is_valid_sample_rate,
13
- logger,
14
- nanosecond_time,
44
+ parse_version,
15
45
  should_be_treated_as_error,
16
46
  )
17
47
 
18
- from typing import TYPE_CHECKING
48
+ from typing import TYPE_CHECKING, cast
19
49
 
20
50
 
21
51
  if TYPE_CHECKING:
22
- from collections.abc import Callable, Mapping, MutableMapping
52
+ from collections.abc import Callable
23
53
  from typing import Any
24
54
  from typing import Dict
25
55
  from typing import Iterator
26
- from typing import List
27
56
  from typing import Optional
28
57
  from typing import overload
29
58
  from typing import ParamSpec
@@ -31,346 +60,208 @@ if TYPE_CHECKING:
31
60
  from typing import Union
32
61
  from typing import TypeVar
33
62
 
34
- from typing_extensions import TypedDict, Unpack
35
-
36
63
  P = ParamSpec("P")
37
64
  R = TypeVar("R")
38
65
 
39
- from sentry_sdk.profiler.continuous_profiler import ContinuousProfile
40
- from sentry_sdk.profiler.transaction_profiler import Profile
41
66
  from sentry_sdk._types import (
42
- Event,
43
- MeasurementUnit,
44
67
  SamplingContext,
45
- MeasurementValue,
46
68
  )
47
69
 
48
- class SpanKwargs(TypedDict, total=False):
49
- trace_id: str
50
- """
51
- The trace ID of the root span. If this new span is to be the root span,
52
- omit this parameter, and a new trace ID will be generated.
53
- """
54
-
55
- span_id: str
56
- """The span ID of this span. If omitted, a new span ID will be generated."""
57
-
58
- parent_span_id: str
59
- """The span ID of the parent span, if applicable."""
60
-
61
- same_process_as_parent: bool
62
- """Whether this span is in the same process as the parent span."""
63
-
64
- sampled: bool
65
- """
66
- Whether the span should be sampled. Overrides the default sampling decision
67
- for this span when provided.
68
- """
69
-
70
- op: str
71
- """
72
- The span's operation. A list of recommended values is available here:
73
- https://develop.sentry.dev/sdk/performance/span-operations/
74
- """
75
-
76
- description: str
77
- """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead."""
78
-
79
- hub: Optional["sentry_sdk.Hub"]
80
- """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead."""
81
-
82
- status: str
83
- """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/"""
70
+ from sentry_sdk.tracing_utils import Baggage
84
71
 
85
- containing_transaction: Optional["Transaction"]
86
- """The transaction that this span belongs to."""
72
+ _FLAGS_CAPACITY = 10
73
+ _OTEL_VERSION = parse_version(otel_version)
87
74
 
88
- start_timestamp: Optional[Union[datetime, float]]
89
- """
90
- The timestamp when the span started. If omitted, the current time
91
- will be used.
92
- """
75
+ tracer = otel_trace.get_tracer(__name__)
93
76
 
94
- scope: "sentry_sdk.Scope"
95
- """The scope to use for this span. If not provided, we use the current scope."""
96
77
 
97
- origin: str
98
- """
99
- The origin of the span.
100
- See https://develop.sentry.dev/sdk/performance/trace-origin/
101
- Default "manual".
102
- """
103
-
104
- name: str
105
- """A string describing what operation is being performed within the span/transaction."""
106
-
107
- class TransactionKwargs(SpanKwargs, total=False):
108
- source: str
109
- """
110
- A string describing the source of the transaction name. This will be used to determine the transaction's type.
111
- See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information.
112
- Default "custom".
113
- """
78
+ class NoOpSpan:
79
+ def __init__(self, **kwargs):
80
+ # type: (Any) -> None
81
+ pass
114
82
 
115
- parent_sampled: bool
116
- """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded."""
83
+ def __repr__(self):
84
+ # type: () -> str
85
+ return "<%s>" % self.__class__.__name__
117
86
 
118
- baggage: "Baggage"
119
- """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)"""
87
+ @property
88
+ def root_span(self):
89
+ # type: () -> Optional[Span]
90
+ return None
120
91
 
121
- ProfileContext = TypedDict(
122
- "ProfileContext",
123
- {
124
- "profiler_id": str,
125
- },
126
- )
92
+ def start_child(self, **kwargs):
93
+ # type: (**Any) -> NoOpSpan
94
+ return NoOpSpan()
127
95
 
128
- BAGGAGE_HEADER_NAME = "baggage"
129
- SENTRY_TRACE_HEADER_NAME = "sentry-trace"
96
+ def to_traceparent(self):
97
+ # type: () -> str
98
+ return ""
130
99
 
100
+ def to_baggage(self):
101
+ # type: () -> Optional[Baggage]
102
+ return None
131
103
 
132
- # Transaction source
133
- # see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
134
- class TransactionSource(str, Enum):
135
- COMPONENT = "component"
136
- CUSTOM = "custom"
137
- ROUTE = "route"
138
- TASK = "task"
139
- URL = "url"
140
- VIEW = "view"
104
+ def get_baggage(self):
105
+ # type: () -> Optional[Baggage]
106
+ return None
141
107
 
142
- def __str__(self):
143
- # type: () -> str
144
- return self.value
108
+ def iter_headers(self):
109
+ # type: () -> Iterator[Tuple[str, str]]
110
+ return iter(())
145
111
 
112
+ def set_tag(self, key, value):
113
+ # type: (str, Any) -> None
114
+ pass
146
115
 
147
- # These are typically high cardinality and the server hates them
148
- LOW_QUALITY_TRANSACTION_SOURCES = [
149
- TransactionSource.URL,
150
- ]
116
+ def set_data(self, key, value):
117
+ # type: (str, Any) -> None
118
+ pass
151
119
 
152
- SOURCE_FOR_STYLE = {
153
- "endpoint": TransactionSource.COMPONENT,
154
- "function_name": TransactionSource.COMPONENT,
155
- "handler_name": TransactionSource.COMPONENT,
156
- "method_and_path_pattern": TransactionSource.ROUTE,
157
- "path": TransactionSource.URL,
158
- "route_name": TransactionSource.COMPONENT,
159
- "route_pattern": TransactionSource.ROUTE,
160
- "uri_template": TransactionSource.ROUTE,
161
- "url": TransactionSource.ROUTE,
162
- }
120
+ def set_status(self, value):
121
+ # type: (str) -> None
122
+ pass
163
123
 
124
+ def set_http_status(self, http_status):
125
+ # type: (int) -> None
126
+ pass
164
127
 
165
- def get_span_status_from_http_code(http_status_code):
166
- # type: (int) -> str
167
- """
168
- Returns the Sentry status corresponding to the given HTTP status code.
128
+ def is_success(self):
129
+ # type: () -> bool
130
+ return True
169
131
 
170
- See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context
171
- """
172
- if http_status_code < 400:
173
- return SPANSTATUS.OK
174
-
175
- elif 400 <= http_status_code < 500:
176
- if http_status_code == 403:
177
- return SPANSTATUS.PERMISSION_DENIED
178
- elif http_status_code == 404:
179
- return SPANSTATUS.NOT_FOUND
180
- elif http_status_code == 429:
181
- return SPANSTATUS.RESOURCE_EXHAUSTED
182
- elif http_status_code == 413:
183
- return SPANSTATUS.FAILED_PRECONDITION
184
- elif http_status_code == 401:
185
- return SPANSTATUS.UNAUTHENTICATED
186
- elif http_status_code == 409:
187
- return SPANSTATUS.ALREADY_EXISTS
188
- else:
189
- return SPANSTATUS.INVALID_ARGUMENT
190
-
191
- elif 500 <= http_status_code < 600:
192
- if http_status_code == 504:
193
- return SPANSTATUS.DEADLINE_EXCEEDED
194
- elif http_status_code == 501:
195
- return SPANSTATUS.UNIMPLEMENTED
196
- elif http_status_code == 503:
197
- return SPANSTATUS.UNAVAILABLE
198
- else:
199
- return SPANSTATUS.INTERNAL_ERROR
132
+ def to_json(self):
133
+ # type: () -> Dict[str, Any]
134
+ return {}
200
135
 
201
- return SPANSTATUS.UNKNOWN_ERROR
136
+ def get_trace_context(self):
137
+ # type: () -> Any
138
+ return {}
202
139
 
140
+ def get_profile_context(self):
141
+ # type: () -> Any
142
+ return {}
203
143
 
204
- class _SpanRecorder:
205
- """Limits the number of spans recorded in a transaction."""
144
+ def finish(
145
+ self,
146
+ end_timestamp=None, # type: Optional[Union[float, datetime]]
147
+ ):
148
+ # type: (...) -> None
149
+ pass
206
150
 
207
- __slots__ = ("maxlen", "spans", "dropped_spans")
151
+ def set_context(self, key, value):
152
+ # type: (str, dict[str, Any]) -> None
153
+ pass
208
154
 
209
- def __init__(self, maxlen):
155
+ def init_span_recorder(self, maxlen):
210
156
  # type: (int) -> None
211
- # FIXME: this is `maxlen - 1` only to preserve historical behavior
212
- # enforced by tests.
213
- # Either this should be changed to `maxlen` or the JS SDK implementation
214
- # should be changed to match a consistent interpretation of what maxlen
215
- # limits: either transaction+spans or only child spans.
216
- self.maxlen = maxlen - 1
217
- self.spans = [] # type: List[Span]
218
- self.dropped_spans = 0 # type: int
219
-
220
- def add(self, span):
221
- # type: (Span) -> None
222
- if len(self.spans) > self.maxlen:
223
- span._span_recorder = None
224
- self.dropped_spans += 1
225
- else:
226
- self.spans.append(span)
157
+ pass
158
+
159
+ def _set_initial_sampling_decision(self, sampling_context):
160
+ # type: (SamplingContext) -> None
161
+ pass
227
162
 
228
163
 
229
164
  class Span:
230
- """A span holds timing information of a block of code.
231
- Spans can have multiple child spans thus forming a span tree.
232
-
233
- :param trace_id: The trace ID of the root span. If this new span is to be the root span,
234
- omit this parameter, and a new trace ID will be generated.
235
- :param span_id: The span ID of this span. If omitted, a new span ID will be generated.
236
- :param parent_span_id: The span ID of the parent span, if applicable.
237
- :param same_process_as_parent: Whether this span is in the same process as the parent span.
238
- :param sampled: Whether the span should be sampled. Overrides the default sampling decision
239
- for this span when provided.
240
- :param op: The span's operation. A list of recommended values is available here:
241
- https://develop.sentry.dev/sdk/performance/span-operations/
242
- :param description: A description of what operation is being performed within the span.
243
-
244
- .. deprecated:: 2.15.0
245
- Please use the `name` parameter, instead.
246
- :param name: A string describing what operation is being performed within the span.
247
- :param hub: The hub to use for this span.
248
-
249
- .. deprecated:: 2.0.0
250
- Please use the `scope` parameter, instead.
251
- :param status: The span's status. Possible values are listed at
252
- https://develop.sentry.dev/sdk/event-payloads/span/
253
- :param containing_transaction: The transaction that this span belongs to.
254
- :param start_timestamp: The timestamp when the span started. If omitted, the current time
255
- will be used.
256
- :param scope: The scope to use for this span. If not provided, we use the current scope.
257
165
  """
258
-
259
- __slots__ = (
260
- "trace_id",
261
- "span_id",
262
- "parent_span_id",
263
- "same_process_as_parent",
264
- "sampled",
265
- "op",
266
- "description",
267
- "_measurements",
268
- "start_timestamp",
269
- "_start_timestamp_monotonic_ns",
270
- "status",
271
- "timestamp",
272
- "_tags",
273
- "_data",
274
- "_span_recorder",
275
- "hub",
276
- "_context_manager_state",
277
- "_containing_transaction",
278
- "_local_aggregator",
279
- "scope",
280
- "origin",
281
- "name",
282
- "_flags",
283
- "_flags_capacity",
284
- )
166
+ OTel span wrapper providing compatibility with the old span interface.
167
+ """
285
168
 
286
169
  def __init__(
287
170
  self,
288
- trace_id=None, # type: Optional[str]
289
- span_id=None, # type: Optional[str]
290
- parent_span_id=None, # type: Optional[str]
291
- same_process_as_parent=True, # type: bool
292
- sampled=None, # type: Optional[bool]
171
+ *,
293
172
  op=None, # type: Optional[str]
294
173
  description=None, # type: Optional[str]
295
- hub=None, # type: Optional[sentry_sdk.Hub] # deprecated
296
174
  status=None, # type: Optional[str]
297
- containing_transaction=None, # type: Optional[Transaction]
175
+ sampled=None, # type: Optional[bool]
298
176
  start_timestamp=None, # type: Optional[Union[datetime, float]]
299
- scope=None, # type: Optional[sentry_sdk.Scope]
300
- origin="manual", # type: str
177
+ origin=None, # type: Optional[str]
301
178
  name=None, # type: Optional[str]
179
+ source=TransactionSource.CUSTOM, # type: str
180
+ attributes=None, # type: Optional[dict[str, Any]]
181
+ only_if_parent=False, # type: bool
182
+ parent_span=None, # type: Optional[Span]
183
+ otel_span=None, # type: Optional[OtelSpan]
184
+ span=None, # type: Optional[Span]
302
185
  ):
303
186
  # type: (...) -> None
304
- self.trace_id = trace_id or uuid.uuid4().hex
305
- self.span_id = span_id or uuid.uuid4().hex[16:]
306
- self.parent_span_id = parent_span_id
307
- self.same_process_as_parent = same_process_as_parent
308
- self.sampled = sampled
309
- self.op = op
310
- self.description = name or description
311
- self.status = status
312
- self.hub = hub # backwards compatibility
313
- self.scope = scope
314
- self.origin = origin
315
- self._measurements = {} # type: Dict[str, MeasurementValue]
316
- self._tags = {} # type: MutableMapping[str, str]
317
- self._data = {} # type: Dict[str, Any]
318
- self._containing_transaction = containing_transaction
319
- self._flags = {} # type: Dict[str, bool]
320
- self._flags_capacity = 10
321
-
322
- if hub is not None:
323
- warnings.warn(
324
- "The `hub` parameter is deprecated. Please use `scope` instead.",
325
- DeprecationWarning,
326
- stacklevel=2,
327
- )
187
+ """
188
+ If otel_span is passed explicitly, just acts as a proxy.
328
189
 
329
- self.scope = self.scope or hub.scope
190
+ If span is passed explicitly, use it. The only purpose of this param
191
+ is backwards compatibility with start_transaction(transaction=...).
330
192
 
331
- if start_timestamp is None:
332
- start_timestamp = datetime.now(timezone.utc)
333
- elif isinstance(start_timestamp, float):
334
- start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc)
335
- self.start_timestamp = start_timestamp
336
- try:
337
- # profiling depends on this value and requires that
338
- # it is measured in nanoseconds
339
- self._start_timestamp_monotonic_ns = nanosecond_time()
340
- except AttributeError:
341
- pass
193
+ If only_if_parent is True, just return an INVALID_SPAN
194
+ and avoid instrumentation if there's no active parent span.
195
+ """
196
+ if otel_span is not None:
197
+ self._otel_span = otel_span
198
+ elif span is not None:
199
+ self._otel_span = span._otel_span
200
+ else:
201
+ skip_span = False
202
+ if only_if_parent and parent_span is None:
203
+ parent_span_context = get_current_span().get_span_context()
204
+ skip_span = (
205
+ not parent_span_context.is_valid or parent_span_context.is_remote
206
+ )
342
207
 
343
- #: End timestamp of span
344
- self.timestamp = None # type: Optional[datetime]
208
+ if skip_span:
209
+ self._otel_span = INVALID_SPAN
210
+ else:
345
211
 
346
- self._span_recorder = None # type: Optional[_SpanRecorder]
347
- self._local_aggregator = None # type: Optional[LocalAggregator]
212
+ if start_timestamp is not None:
213
+ # OTel timestamps have nanosecond precision
214
+ start_timestamp = convert_to_otel_timestamp(start_timestamp)
215
+
216
+ span_name = name or description or op or DEFAULT_SPAN_NAME
217
+
218
+ # Prepopulate some attrs so that they're accessible in traces_sampler
219
+ attributes = attributes or {}
220
+ if op is not None:
221
+ attributes[SentrySpanAttribute.OP] = op
222
+ if source is not None:
223
+ attributes[SentrySpanAttribute.SOURCE] = source
224
+ if description is not None:
225
+ attributes[SentrySpanAttribute.DESCRIPTION] = description
226
+ if sampled is not None:
227
+ attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled
228
+
229
+ parent_context = None
230
+ if parent_span is not None:
231
+ parent_context = otel_trace.set_span_in_context(
232
+ parent_span._otel_span
233
+ )
234
+
235
+ self._otel_span = tracer.start_span(
236
+ span_name,
237
+ context=parent_context,
238
+ start_time=start_timestamp,
239
+ attributes=attributes,
240
+ )
348
241
 
349
- self.update_active_thread()
350
- self.set_profiler_id(get_profiler_id())
242
+ self.origin = origin or DEFAULT_SPAN_ORIGIN
243
+ self.description = description
244
+ self.name = span_name
351
245
 
352
- # TODO this should really live on the Transaction class rather than the Span
353
- # class
354
- def init_span_recorder(self, maxlen):
355
- # type: (int) -> None
356
- if self._span_recorder is None:
357
- self._span_recorder = _SpanRecorder(maxlen)
246
+ if status is not None:
247
+ self.set_status(status)
248
+
249
+ self.update_active_thread()
358
250
 
359
- def _get_local_aggregator(self):
360
- # type: (...) -> LocalAggregator
361
- rv = self._local_aggregator
362
- if rv is None:
363
- rv = self._local_aggregator = LocalAggregator()
364
- return rv
251
+ def __eq__(self, other):
252
+ # type: (object) -> bool
253
+ if not isinstance(other, Span):
254
+ return False
255
+ return self._otel_span == other._otel_span
365
256
 
366
257
  def __repr__(self):
367
258
  # type: () -> str
368
259
  return (
369
- "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>"
260
+ "<%s(op=%r, name:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>"
370
261
  % (
371
262
  self.__class__.__name__,
372
263
  self.op,
373
- self.description,
264
+ self.name,
374
265
  self.trace_id,
375
266
  self.span_id,
376
267
  self.parent_span_id,
@@ -379,196 +270,175 @@ class Span:
379
270
  )
380
271
  )
381
272
 
273
+ def activate(self):
274
+ # type: () -> None
275
+ ctx = otel_trace.set_span_in_context(self._otel_span)
276
+ # set as the implicit current context
277
+ self._ctx_token = context.attach(ctx)
278
+
279
+ def deactivate(self):
280
+ # type: () -> None
281
+ if self._ctx_token:
282
+ context.detach(self._ctx_token)
283
+ del self._ctx_token
284
+
382
285
  def __enter__(self):
383
286
  # type: () -> Span
384
- scope = self.scope or sentry_sdk.get_current_scope()
385
- old_span = scope.span
386
- scope.span = self
387
- self._context_manager_state = (scope, old_span)
287
+ self.activate()
388
288
  return self
389
289
 
390
290
  def __exit__(self, ty, value, tb):
391
291
  # type: (Optional[Any], Optional[Any], Optional[Any]) -> None
392
292
  if value is not None and should_be_treated_as_error(ty, value):
393
293
  self.set_status(SPANSTATUS.INTERNAL_ERROR)
294
+ else:
295
+ status_unset = (
296
+ hasattr(self._otel_span, "status")
297
+ and self._otel_span.status.status_code == StatusCode.UNSET
298
+ )
299
+ if status_unset:
300
+ self.set_status(SPANSTATUS.OK)
394
301
 
395
- scope, old_span = self._context_manager_state
396
- del self._context_manager_state
397
- self.finish(scope)
398
- scope.span = old_span
302
+ self.finish()
303
+ self.deactivate()
399
304
 
400
305
  @property
401
- def containing_transaction(self):
402
- # type: () -> Optional[Transaction]
403
- """The ``Transaction`` that this span belongs to.
404
- The ``Transaction`` is the root of the span tree,
405
- so one could also think of this ``Transaction`` as the "root span"."""
406
-
407
- # this is a getter rather than a regular attribute so that transactions
408
- # can return `self` here instead (as a way to prevent them circularly
409
- # referencing themselves)
410
- return self._containing_transaction
411
-
412
- def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
413
- # type: (str, **Any) -> Span
414
- """
415
- Start a sub-span from the current span or transaction.
416
-
417
- Takes the same arguments as the initializer of :py:class:`Span`. The
418
- trace id, sampling decision, transaction pointer, and span recorder are
419
- inherited from the current span/transaction.
420
-
421
- The instrumenter parameter is deprecated for user code, and it will
422
- be removed in the next major version. Going forward, it should only
423
- be used by the SDK itself.
424
- """
425
- if kwargs.get("description") is not None:
426
- warnings.warn(
427
- "The `description` parameter is deprecated. Please use `name` instead.",
428
- DeprecationWarning,
429
- stacklevel=2,
430
- )
306
+ def description(self):
307
+ # type: () -> Optional[str]
308
+ return self.get_attribute(SentrySpanAttribute.DESCRIPTION)
431
309
 
432
- configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"]
433
-
434
- if instrumenter != configuration_instrumenter:
435
- return NoOpSpan()
310
+ @description.setter
311
+ def description(self, value):
312
+ # type: (Optional[str]) -> None
313
+ self.set_attribute(SentrySpanAttribute.DESCRIPTION, value)
436
314
 
437
- kwargs.setdefault("sampled", self.sampled)
315
+ @property
316
+ def origin(self):
317
+ # type: () -> Optional[str]
318
+ return self.get_attribute(SentrySpanAttribute.ORIGIN)
438
319
 
439
- child = Span(
440
- trace_id=self.trace_id,
441
- parent_span_id=self.span_id,
442
- containing_transaction=self.containing_transaction,
443
- **kwargs,
444
- )
320
+ @origin.setter
321
+ def origin(self, value):
322
+ # type: (Optional[str]) -> None
323
+ self.set_attribute(SentrySpanAttribute.ORIGIN, value)
445
324
 
446
- span_recorder = (
447
- self.containing_transaction and self.containing_transaction._span_recorder
325
+ @property
326
+ def root_span(self):
327
+ # type: () -> Optional[Span]
328
+ root_otel_span = cast(
329
+ "Optional[OtelSpan]", get_sentry_meta(self._otel_span, "root_span")
448
330
  )
449
- if span_recorder:
450
- span_recorder.add(child)
331
+ return Span(otel_span=root_otel_span) if root_otel_span else None
451
332
 
452
- return child
333
+ @property
334
+ def is_root_span(self):
335
+ # type: () -> bool
336
+ return self.root_span == self
453
337
 
454
- @classmethod
455
- def continue_from_environ(
456
- cls,
457
- environ, # type: Mapping[str, str]
458
- **kwargs, # type: Any
459
- ):
460
- # type: (...) -> Transaction
461
- """
462
- Create a Transaction with the given params, then add in data pulled from
463
- the ``sentry-trace`` and ``baggage`` headers from the environ (if any)
464
- before returning the Transaction.
338
+ @property
339
+ def parent_span_id(self):
340
+ # type: () -> Optional[str]
341
+ if (
342
+ not isinstance(self._otel_span, ReadableSpan)
343
+ or self._otel_span.parent is None
344
+ ):
345
+ return None
346
+ return format_span_id(self._otel_span.parent.span_id)
465
347
 
466
- This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers`
467
- in that it assumes header names in the form ``HTTP_HEADER_NAME`` -
468
- such as you would get from a WSGI/ASGI environ -
469
- rather than the form ``header-name``.
348
+ @property
349
+ def trace_id(self):
350
+ # type: () -> str
351
+ return format_trace_id(self._otel_span.get_span_context().trace_id)
470
352
 
471
- :param environ: The ASGI/WSGI environ to pull information from.
472
- """
473
- if cls is Span:
474
- logger.warning(
475
- "Deprecated: use Transaction.continue_from_environ "
476
- "instead of Span.continue_from_environ."
477
- )
478
- return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs)
353
+ @property
354
+ def span_id(self):
355
+ # type: () -> str
356
+ return format_span_id(self._otel_span.get_span_context().span_id)
479
357
 
480
- @classmethod
481
- def continue_from_headers(
482
- cls,
483
- headers, # type: Mapping[str, str]
484
- *,
485
- _sample_rand=None, # type: Optional[str]
486
- **kwargs, # type: Any
487
- ):
488
- # type: (...) -> Transaction
489
- """
490
- Create a transaction with the given params (including any data pulled from
491
- the ``sentry-trace`` and ``baggage`` headers).
358
+ @property
359
+ def is_valid(self):
360
+ # type: () -> bool
361
+ return self._otel_span.get_span_context().is_valid and isinstance(
362
+ self._otel_span, ReadableSpan
363
+ )
492
364
 
493
- :param headers: The dictionary with the HTTP headers to pull information from.
494
- :param _sample_rand: If provided, we override the sample_rand value from the
495
- incoming headers with this value. (internal use only)
496
- """
497
- # TODO move this to the Transaction class
498
- if cls is Span:
499
- logger.warning(
500
- "Deprecated: use Transaction.continue_from_headers "
501
- "instead of Span.continue_from_headers."
502
- )
365
+ @property
366
+ def sampled(self):
367
+ # type: () -> Optional[bool]
368
+ return self._otel_span.get_span_context().trace_flags.sampled
503
369
 
504
- # TODO-neel move away from this kwargs stuff, it's confusing and opaque
505
- # make more explicit
506
- baggage = Baggage.from_incoming_header(
507
- headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand
370
+ @property
371
+ def sample_rate(self):
372
+ # type: () -> Optional[float]
373
+ sample_rate = self._otel_span.get_span_context().trace_state.get(
374
+ TRACESTATE_SAMPLE_RATE_KEY
508
375
  )
509
- kwargs.update({BAGGAGE_HEADER_NAME: baggage})
376
+ return float(sample_rate) if sample_rate is not None else None
510
377
 
511
- sentrytrace_kwargs = extract_sentrytrace_data(
512
- headers.get(SENTRY_TRACE_HEADER_NAME)
513
- )
378
+ @property
379
+ def op(self):
380
+ # type: () -> Optional[str]
381
+ return self.get_attribute(SentrySpanAttribute.OP)
514
382
 
515
- if sentrytrace_kwargs is not None:
516
- kwargs.update(sentrytrace_kwargs)
383
+ @op.setter
384
+ def op(self, value):
385
+ # type: (Optional[str]) -> None
386
+ self.set_attribute(SentrySpanAttribute.OP, value)
517
387
 
518
- # If there's an incoming sentry-trace but no incoming baggage header,
519
- # for instance in traces coming from older SDKs,
520
- # baggage will be empty and immutable and won't be populated as head SDK.
521
- baggage.freeze()
388
+ @property
389
+ def name(self):
390
+ # type: () -> Optional[str]
391
+ return self.get_attribute(SentrySpanAttribute.NAME)
522
392
 
523
- transaction = Transaction(**kwargs)
524
- transaction.same_process_as_parent = False
393
+ @name.setter
394
+ def name(self, value):
395
+ # type: (Optional[str]) -> None
396
+ self.set_attribute(SentrySpanAttribute.NAME, value)
525
397
 
526
- return transaction
398
+ @property
399
+ def source(self):
400
+ # type: () -> str
401
+ return (
402
+ self.get_attribute(SentrySpanAttribute.SOURCE) or TransactionSource.CUSTOM
403
+ )
527
404
 
528
- def iter_headers(self):
529
- # type: () -> Iterator[Tuple[str, str]]
530
- """
531
- Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers.
532
- If the span's containing transaction doesn't yet have a ``baggage`` value,
533
- this will cause one to be generated and stored.
534
- """
535
- if not self.containing_transaction:
536
- # Do not propagate headers if there is no containing transaction. Otherwise, this
537
- # span ends up being the root span of a new trace, and since it does not get sent
538
- # to Sentry, the trace will be missing a root transaction. The dynamic sampling
539
- # context will also be missing, breaking dynamic sampling & traces.
540
- return
405
+ @source.setter
406
+ def source(self, value):
407
+ # type: (str) -> None
408
+ self.set_attribute(SentrySpanAttribute.SOURCE, value)
541
409
 
542
- yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()
410
+ @property
411
+ def start_timestamp(self):
412
+ # type: () -> Optional[datetime]
413
+ if not isinstance(self._otel_span, ReadableSpan):
414
+ return None
543
415
 
544
- baggage = self.containing_transaction.get_baggage().serialize()
545
- if baggage:
546
- yield BAGGAGE_HEADER_NAME, baggage
416
+ start_time = self._otel_span.start_time
417
+ if start_time is None:
418
+ return None
547
419
 
548
- @classmethod
549
- def from_traceparent(
550
- cls,
551
- traceparent, # type: Optional[str]
552
- **kwargs, # type: Any
553
- ):
554
- # type: (...) -> Optional[Transaction]
555
- """
556
- DEPRECATED: Use :py:meth:`sentry_sdk.tracing.Span.continue_from_headers`.
420
+ return convert_from_otel_timestamp(start_time)
557
421
 
558
- Create a ``Transaction`` with the given params, then add in data pulled from
559
- the given ``sentry-trace`` header value before returning the ``Transaction``.
560
- """
561
- logger.warning(
562
- "Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) "
563
- "instead of from_traceparent(traceparent, **kwargs)"
564
- )
422
+ @property
423
+ def timestamp(self):
424
+ # type: () -> Optional[datetime]
425
+ if not isinstance(self._otel_span, ReadableSpan):
426
+ return None
565
427
 
566
- if not traceparent:
428
+ end_time = self._otel_span.end_time
429
+ if end_time is None:
567
430
  return None
568
431
 
569
- return cls.continue_from_headers(
570
- {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs
571
- )
432
+ return convert_from_otel_timestamp(end_time)
433
+
434
+ def start_child(self, **kwargs):
435
+ # type: (**Any) -> Span
436
+ return Span(parent_span=self, **kwargs)
437
+
438
+ def iter_headers(self):
439
+ # type: () -> Iterator[Tuple[str, str]]
440
+ yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()
441
+ yield BAGGAGE_HEADER_NAME, serialize_trace_state(self.trace_state)
572
442
 
573
443
  def to_traceparent(self):
574
444
  # type: () -> str
@@ -585,748 +455,149 @@ class Span:
585
455
 
586
456
  return traceparent
587
457
 
458
+ @property
459
+ def trace_state(self):
460
+ # type: () -> TraceState
461
+ return get_trace_state(self._otel_span)
462
+
588
463
  def to_baggage(self):
589
- # type: () -> Optional[Baggage]
590
- """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage`
591
- associated with this ``Span``, if any. (Taken from the root of the span tree.)
592
- """
593
- if self.containing_transaction:
594
- return self.containing_transaction.get_baggage()
595
- return None
464
+ # type: () -> Baggage
465
+ return self.get_baggage()
466
+
467
+ def get_baggage(self):
468
+ # type: () -> Baggage
469
+ return baggage_from_trace_state(self.trace_state)
596
470
 
597
471
  def set_tag(self, key, value):
598
472
  # type: (str, Any) -> None
599
- self._tags[key] = value
473
+ self.set_attribute(f"{SentrySpanAttribute.TAG}.{key}", value)
600
474
 
601
475
  def set_data(self, key, value):
602
476
  # type: (str, Any) -> None
603
- self._data[key] = value
604
-
605
- def set_flag(self, flag, result):
606
- # type: (str, bool) -> None
607
- if len(self._flags) < self._flags_capacity:
608
- self._flags[flag] = result
609
-
610
- def set_status(self, value):
611
- # type: (str) -> None
612
- self.status = value
613
-
614
- def set_measurement(self, name, value, unit=""):
615
- # type: (str, float, MeasurementUnit) -> None
616
- """
617
- .. deprecated:: 2.28.0
618
- This function is deprecated and will be removed in the next major release.
619
- """
620
-
621
477
  warnings.warn(
622
- "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.",
478
+ "`Span.set_data` is deprecated. Please use `Span.set_attribute` instead.",
623
479
  DeprecationWarning,
624
480
  stacklevel=2,
625
481
  )
626
- self._measurements[name] = {"value": value, "unit": unit}
627
482
 
628
- def set_thread(self, thread_id, thread_name):
629
- # type: (Optional[int], Optional[str]) -> None
630
-
631
- if thread_id is not None:
632
- self.set_data(SPANDATA.THREAD_ID, str(thread_id))
483
+ # TODO-neel-potel we cannot add dicts here
484
+ self.set_attribute(key, value)
633
485
 
634
- if thread_name is not None:
635
- self.set_data(SPANDATA.THREAD_NAME, thread_name)
636
-
637
- def set_profiler_id(self, profiler_id):
638
- # type: (Optional[str]) -> None
639
- if profiler_id is not None:
640
- self.set_data(SPANDATA.PROFILER_ID, profiler_id)
486
+ def get_attribute(self, name):
487
+ # type: (str) -> Optional[Any]
488
+ if (
489
+ not isinstance(self._otel_span, ReadableSpan)
490
+ or not self._otel_span.attributes
491
+ ):
492
+ return None
493
+ return self._otel_span.attributes.get(name)
641
494
 
642
- def set_http_status(self, http_status):
643
- # type: (int) -> None
644
- self.set_tag(
645
- "http.status_code", str(http_status)
646
- ) # we keep this for backwards compatibility
647
- self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status)
648
- self.set_status(get_span_status_from_http_code(http_status))
495
+ def set_attribute(self, key, value):
496
+ # type: (str, Any) -> None
497
+ # otel doesn't support None as values, preferring to not set the key
498
+ # at all instead
499
+ if value is None:
500
+ return
501
+ serialized_value = _serialize_span_attribute(value)
502
+ if serialized_value is None:
503
+ return
649
504
 
650
- def is_success(self):
651
- # type: () -> bool
652
- return self.status == "ok"
505
+ self._otel_span.set_attribute(key, serialized_value)
653
506
 
654
- def finish(self, scope=None, end_timestamp=None):
655
- # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str]
507
+ @property
508
+ def status(self):
509
+ # type: () -> Optional[str]
656
510
  """
657
- Sets the end timestamp of the span.
658
-
659
- Additionally it also creates a breadcrumb from the span,
660
- if the span represents a database or HTTP request.
661
-
662
- :param scope: The scope to use for this transaction.
663
- If not provided, the current scope will be used.
664
- :param end_timestamp: Optional timestamp that should
665
- be used as timestamp instead of the current time.
666
-
667
- :return: Always ``None``. The type is ``Optional[str]`` to match
668
- the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`.
511
+ Return the Sentry `SPANSTATUS` corresponding to the underlying OTel status.
512
+ Because differences in possible values in OTel `StatusCode` and
513
+ Sentry `SPANSTATUS` it can not be guaranteed that the status
514
+ set in `set_status()` will be the same as the one returned here.
669
515
  """
670
- if self.timestamp is not None:
671
- # This span is already finished, ignore.
516
+ if not isinstance(self._otel_span, ReadableSpan):
672
517
  return None
673
518
 
674
- try:
675
- if end_timestamp:
676
- if isinstance(end_timestamp, float):
677
- end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc)
678
- self.timestamp = end_timestamp
679
- else:
680
- elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns
681
- self.timestamp = self.start_timestamp + timedelta(
682
- microseconds=elapsed / 1000
683
- )
684
- except AttributeError:
685
- self.timestamp = datetime.now(timezone.utc)
686
-
687
- scope = scope or sentry_sdk.get_current_scope()
688
- maybe_create_breadcrumbs_from_span(scope, self)
689
-
690
- return None
691
-
692
- def to_json(self):
693
- # type: () -> Dict[str, Any]
694
- """Returns a JSON-compatible representation of the span."""
695
-
696
- rv = {
697
- "trace_id": self.trace_id,
698
- "span_id": self.span_id,
699
- "parent_span_id": self.parent_span_id,
700
- "same_process_as_parent": self.same_process_as_parent,
701
- "op": self.op,
702
- "description": self.description,
703
- "start_timestamp": self.start_timestamp,
704
- "timestamp": self.timestamp,
705
- "origin": self.origin,
706
- } # type: Dict[str, Any]
707
-
708
- if self.status:
709
- self._tags["status"] = self.status
710
-
711
- if self._local_aggregator is not None:
712
- metrics_summary = self._local_aggregator.to_json()
713
- if metrics_summary:
714
- rv["_metrics_summary"] = metrics_summary
715
-
716
- if len(self._measurements) > 0:
717
- rv["measurements"] = self._measurements
718
-
719
- tags = self._tags
720
- if tags:
721
- rv["tags"] = tags
722
-
723
- data = {}
724
- data.update(self._flags)
725
- data.update(self._data)
726
- if data:
727
- rv["data"] = data
728
-
729
- return rv
519
+ if self._otel_span.status.status_code == StatusCode.UNSET:
520
+ return None
521
+ elif self._otel_span.status.status_code == StatusCode.OK:
522
+ return SPANSTATUS.OK
523
+ else:
524
+ return SPANSTATUS.UNKNOWN_ERROR
730
525
 
731
- def get_trace_context(self):
732
- # type: () -> Any
733
- rv = {
734
- "trace_id": self.trace_id,
735
- "span_id": self.span_id,
736
- "parent_span_id": self.parent_span_id,
737
- "op": self.op,
738
- "description": self.description,
739
- "origin": self.origin,
740
- } # type: Dict[str, Any]
741
- if self.status:
742
- rv["status"] = self.status
743
-
744
- if self.containing_transaction:
745
- rv["dynamic_sampling_context"] = (
746
- self.containing_transaction.get_baggage().dynamic_sampling_context()
747
- )
526
+ def set_status(self, status):
527
+ # type: (str) -> None
528
+ if status == SPANSTATUS.OK:
529
+ otel_status = StatusCode.OK
530
+ otel_description = None
531
+ else:
532
+ otel_status = StatusCode.ERROR
533
+ otel_description = status
748
534
 
749
- data = {}
535
+ if _OTEL_VERSION is None or _OTEL_VERSION >= (1, 12, 0):
536
+ self._otel_span.set_status(otel_status, otel_description)
537
+ else:
538
+ self._otel_span.set_status(Status(otel_status, otel_description))
750
539
 
751
- thread_id = self._data.get(SPANDATA.THREAD_ID)
540
+ def set_thread(self, thread_id, thread_name):
541
+ # type: (Optional[int], Optional[str]) -> None
752
542
  if thread_id is not None:
753
- data["thread.id"] = thread_id
754
-
755
- thread_name = self._data.get(SPANDATA.THREAD_NAME)
756
- if thread_name is not None:
757
- data["thread.name"] = thread_name
758
-
759
- if data:
760
- rv["data"] = data
543
+ self.set_attribute(SPANDATA.THREAD_ID, str(thread_id))
761
544
 
762
- return rv
763
-
764
- def get_profile_context(self):
765
- # type: () -> Optional[ProfileContext]
766
- profiler_id = self._data.get(SPANDATA.PROFILER_ID)
767
- if profiler_id is None:
768
- return None
769
-
770
- return {
771
- "profiler_id": profiler_id,
772
- }
545
+ if thread_name is not None:
546
+ self.set_attribute(SPANDATA.THREAD_NAME, thread_name)
773
547
 
774
548
  def update_active_thread(self):
775
549
  # type: () -> None
776
550
  thread_id, thread_name = get_current_thread_meta()
777
551
  self.set_thread(thread_id, thread_name)
778
552
 
779
-
780
- class Transaction(Span):
781
- """The Transaction is the root element that holds all the spans
782
- for Sentry performance instrumentation.
783
-
784
- :param name: Identifier of the transaction.
785
- Will show up in the Sentry UI.
786
- :param parent_sampled: Whether the parent transaction was sampled.
787
- If True this transaction will be kept, if False it will be discarded.
788
- :param baggage: The W3C baggage header value.
789
- (see https://www.w3.org/TR/baggage/)
790
- :param source: A string describing the source of the transaction name.
791
- This will be used to determine the transaction's type.
792
- See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
793
- for more information. Default "custom".
794
- :param kwargs: Additional arguments to be passed to the Span constructor.
795
- See :py:class:`sentry_sdk.tracing.Span` for available arguments.
796
- """
797
-
798
- __slots__ = (
799
- "name",
800
- "source",
801
- "parent_sampled",
802
- # used to create baggage value for head SDKs in dynamic sampling
803
- "sample_rate",
804
- "_measurements",
805
- "_contexts",
806
- "_profile",
807
- "_continuous_profile",
808
- "_baggage",
809
- "_sample_rand",
810
- )
811
-
812
- def __init__( # type: ignore[misc]
813
- self,
814
- name="", # type: str
815
- parent_sampled=None, # type: Optional[bool]
816
- baggage=None, # type: Optional[Baggage]
817
- source=TransactionSource.CUSTOM, # type: str
818
- **kwargs, # type: Unpack[SpanKwargs]
819
- ):
820
- # type: (...) -> None
821
-
822
- super().__init__(**kwargs)
823
-
824
- self.name = name
825
- self.source = source
826
- self.sample_rate = None # type: Optional[float]
827
- self.parent_sampled = parent_sampled
828
- self._measurements = {} # type: Dict[str, MeasurementValue]
829
- self._contexts = {} # type: Dict[str, Any]
830
- self._profile = None # type: Optional[Profile]
831
- self._continuous_profile = None # type: Optional[ContinuousProfile]
832
- self._baggage = baggage
833
-
834
- baggage_sample_rand = (
835
- None if self._baggage is None else self._baggage._sample_rand()
836
- )
837
- if baggage_sample_rand is not None:
838
- self._sample_rand = baggage_sample_rand
839
- else:
840
- self._sample_rand = _generate_sample_rand(self.trace_id)
841
-
842
- def __repr__(self):
843
- # type: () -> str
844
- return (
845
- "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>"
846
- % (
847
- self.__class__.__name__,
848
- self.name,
849
- self.op,
850
- self.trace_id,
851
- self.span_id,
852
- self.parent_span_id,
853
- self.sampled,
854
- self.source,
855
- self.origin,
856
- )
857
- )
858
-
859
- def _possibly_started(self):
860
- # type: () -> bool
861
- """Returns whether the transaction might have been started.
862
-
863
- If this returns False, we know that the transaction was not started
864
- with sentry_sdk.start_transaction, and therefore the transaction will
865
- be discarded.
866
- """
867
-
868
- # We must explicitly check self.sampled is False since self.sampled can be None
869
- return self._span_recorder is not None or self.sampled is False
870
-
871
- def __enter__(self):
872
- # type: () -> Transaction
873
- if not self._possibly_started():
874
- logger.debug(
875
- "Transaction was entered without being started with sentry_sdk.start_transaction."
876
- "The transaction will not be sent to Sentry. To fix, start the transaction by"
877
- "passing it to sentry_sdk.start_transaction."
878
- )
879
-
880
- super().__enter__()
881
-
882
- if self._profile is not None:
883
- self._profile.__enter__()
884
-
885
- return self
886
-
887
- def __exit__(self, ty, value, tb):
888
- # type: (Optional[Any], Optional[Any], Optional[Any]) -> None
889
- if self._profile is not None:
890
- self._profile.__exit__(ty, value, tb)
891
-
892
- if self._continuous_profile is not None:
893
- self._continuous_profile.stop()
894
-
895
- super().__exit__(ty, value, tb)
896
-
897
- @property
898
- def containing_transaction(self):
899
- # type: () -> Transaction
900
- """The root element of the span tree.
901
- In the case of a transaction it is the transaction itself.
902
- """
903
-
904
- # Transactions (as spans) belong to themselves (as transactions). This
905
- # is a getter rather than a regular attribute to avoid having a circular
906
- # reference.
907
- return self
908
-
909
- def _get_scope_from_finish_args(
910
- self,
911
- scope_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]
912
- hub_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]
913
- ):
914
- # type: (...) -> Optional[sentry_sdk.Scope]
915
- """
916
- Logic to get the scope from the arguments passed to finish. This
917
- function exists for backwards compatibility with the old finish.
918
-
919
- TODO: Remove this function in the next major version.
920
- """
921
- scope_or_hub = scope_arg
922
- if hub_arg is not None:
923
- warnings.warn(
924
- "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.",
925
- DeprecationWarning,
926
- stacklevel=3,
927
- )
928
-
929
- scope_or_hub = hub_arg
930
-
931
- if isinstance(scope_or_hub, sentry_sdk.Hub):
932
- warnings.warn(
933
- "Passing a Hub to finish is deprecated. Please pass a Scope, instead.",
934
- DeprecationWarning,
935
- stacklevel=3,
936
- )
937
-
938
- return scope_or_hub.scope
939
-
940
- return scope_or_hub
941
-
942
- def finish(
943
- self,
944
- scope=None, # type: Optional[sentry_sdk.Scope]
945
- end_timestamp=None, # type: Optional[Union[float, datetime]]
946
- *,
947
- hub=None, # type: Optional[sentry_sdk.Hub]
948
- ):
949
- # type: (...) -> Optional[str]
950
- """Finishes the transaction and sends it to Sentry.
951
- All finished spans in the transaction will also be sent to Sentry.
952
-
953
- :param scope: The Scope to use for this transaction.
954
- If not provided, the current Scope will be used.
955
- :param end_timestamp: Optional timestamp that should
956
- be used as timestamp instead of the current time.
957
- :param hub: The hub to use for this transaction.
958
- This argument is DEPRECATED. Please use the `scope`
959
- parameter, instead.
960
-
961
- :return: The event ID if the transaction was sent to Sentry,
962
- otherwise None.
963
- """
964
- if self.timestamp is not None:
965
- # This transaction is already finished, ignore.
966
- return None
967
-
968
- # For backwards compatibility, we must handle the case where `scope`
969
- # or `hub` could both either be a `Scope` or a `Hub`.
970
- scope = self._get_scope_from_finish_args(
971
- scope, hub
972
- ) # type: Optional[sentry_sdk.Scope]
973
-
974
- scope = scope or self.scope or sentry_sdk.get_current_scope()
975
- client = sentry_sdk.get_client()
976
-
977
- if not client.is_active():
978
- # We have no active client and therefore nowhere to send this transaction.
979
- return None
980
-
981
- if self._span_recorder is None:
982
- # Explicit check against False needed because self.sampled might be None
983
- if self.sampled is False:
984
- logger.debug("Discarding transaction because sampled = False")
985
- else:
986
- logger.debug(
987
- "Discarding transaction because it was not started with sentry_sdk.start_transaction"
988
- )
989
-
990
- # This is not entirely accurate because discards here are not
991
- # exclusively based on sample rate but also traces sampler, but
992
- # we handle this the same here.
993
- if client.transport and has_tracing_enabled(client.options):
994
- if client.monitor and client.monitor.downsample_factor > 0:
995
- reason = "backpressure"
996
- else:
997
- reason = "sample_rate"
998
-
999
- client.transport.record_lost_event(reason, data_category="transaction")
1000
-
1001
- # Only one span (the transaction itself) is discarded, since we did not record any spans here.
1002
- client.transport.record_lost_event(reason, data_category="span")
1003
- return None
1004
-
1005
- if not self.name:
1006
- logger.warning(
1007
- "Transaction has no name, falling back to `<unlabeled transaction>`."
1008
- )
1009
- self.name = "<unlabeled transaction>"
1010
-
1011
- super().finish(scope, end_timestamp)
1012
-
1013
- if not self.sampled:
1014
- # At this point a `sampled = None` should have already been resolved
1015
- # to a concrete decision.
1016
- if self.sampled is None:
1017
- logger.warning("Discarding transaction without sampling decision.")
1018
-
1019
- return None
1020
-
1021
- finished_spans = [
1022
- span.to_json()
1023
- for span in self._span_recorder.spans
1024
- if span.timestamp is not None
1025
- ]
1026
-
1027
- len_diff = len(self._span_recorder.spans) - len(finished_spans)
1028
- dropped_spans = len_diff + self._span_recorder.dropped_spans
1029
-
1030
- # we do this to break the circular reference of transaction -> span
1031
- # recorder -> span -> containing transaction (which is where we started)
1032
- # before either the spans or the transaction goes out of scope and has
1033
- # to be garbage collected
1034
- self._span_recorder = None
1035
-
1036
- contexts = {}
1037
- contexts.update(self._contexts)
1038
- contexts.update({"trace": self.get_trace_context()})
1039
- profile_context = self.get_profile_context()
1040
- if profile_context is not None:
1041
- contexts.update({"profile": profile_context})
1042
-
1043
- event = {
1044
- "type": "transaction",
1045
- "transaction": self.name,
1046
- "transaction_info": {"source": self.source},
1047
- "contexts": contexts,
1048
- "tags": self._tags,
1049
- "timestamp": self.timestamp,
1050
- "start_timestamp": self.start_timestamp,
1051
- "spans": finished_spans,
1052
- } # type: Event
1053
-
1054
- if dropped_spans > 0:
1055
- event["_dropped_spans"] = dropped_spans
1056
-
1057
- if self._profile is not None and self._profile.valid():
1058
- event["profile"] = self._profile
1059
- self._profile = None
1060
-
1061
- event["measurements"] = self._measurements
1062
-
1063
- # This is here since `to_json` is not invoked. This really should
1064
- # be gone when we switch to onlyspans.
1065
- if self._local_aggregator is not None:
1066
- metrics_summary = self._local_aggregator.to_json()
1067
- if metrics_summary:
1068
- event["_metrics_summary"] = metrics_summary
1069
-
1070
- return scope.capture_event(event)
1071
-
1072
- def set_measurement(self, name, value, unit=""):
1073
- # type: (str, float, MeasurementUnit) -> None
1074
- """
1075
- .. deprecated:: 2.28.0
1076
- This function is deprecated and will be removed in the next major release.
1077
- """
1078
-
1079
- warnings.warn(
1080
- "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.",
1081
- DeprecationWarning,
1082
- stacklevel=2,
1083
- )
1084
- self._measurements[name] = {"value": value, "unit": unit}
1085
-
1086
- def set_context(self, key, value):
1087
- # type: (str, dict[str, Any]) -> None
1088
- """Sets a context. Transactions can have multiple contexts
1089
- and they should follow the format described in the "Contexts Interface"
1090
- documentation.
1091
-
1092
- :param key: The name of the context.
1093
- :param value: The information about the context.
1094
- """
1095
- self._contexts[key] = value
1096
-
1097
553
  def set_http_status(self, http_status):
1098
554
  # type: (int) -> None
1099
- """Sets the status of the Transaction according to the given HTTP status.
1100
-
1101
- :param http_status: The HTTP status code."""
1102
- super().set_http_status(http_status)
1103
- self.set_context("response", {"status_code": http_status})
1104
-
1105
- def to_json(self):
1106
- # type: () -> Dict[str, Any]
1107
- """Returns a JSON-compatible representation of the transaction."""
1108
- rv = super().to_json()
1109
-
1110
- rv["name"] = self.name
1111
- rv["source"] = self.source
1112
- rv["sampled"] = self.sampled
1113
-
1114
- return rv
1115
-
1116
- def get_trace_context(self):
1117
- # type: () -> Any
1118
- trace_context = super().get_trace_context()
1119
-
1120
- if self._data:
1121
- trace_context["data"] = self._data
1122
-
1123
- return trace_context
1124
-
1125
- def get_baggage(self):
1126
- # type: () -> Baggage
1127
- """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage`
1128
- associated with the Transaction.
1129
-
1130
- The first time a new baggage with Sentry items is made,
1131
- it will be frozen."""
1132
- if not self._baggage or self._baggage.mutable:
1133
- self._baggage = Baggage.populate_from_transaction(self)
1134
-
1135
- return self._baggage
1136
-
1137
- def _set_initial_sampling_decision(self, sampling_context):
1138
- # type: (SamplingContext) -> None
1139
- """
1140
- Sets the transaction's sampling decision, according to the following
1141
- precedence rules:
1142
-
1143
- 1. If a sampling decision is passed to `start_transaction`
1144
- (`start_transaction(name: "my transaction", sampled: True)`), that
1145
- decision will be used, regardless of anything else
1146
-
1147
- 2. If `traces_sampler` is defined, its decision will be used. It can
1148
- choose to keep or ignore any parent sampling decision, or use the
1149
- sampling context data to make its own decision or to choose a sample
1150
- rate for the transaction.
1151
-
1152
- 3. If `traces_sampler` is not defined, but there's a parent sampling
1153
- decision, the parent sampling decision will be used.
1154
-
1155
- 4. If `traces_sampler` is not defined and there's no parent sampling
1156
- decision, `traces_sample_rate` will be used.
1157
- """
1158
- client = sentry_sdk.get_client()
1159
-
1160
- transaction_description = "{op}transaction <{name}>".format(
1161
- op=("<" + self.op + "> " if self.op else ""), name=self.name
1162
- )
1163
-
1164
- # nothing to do if tracing is disabled
1165
- if not has_tracing_enabled(client.options):
1166
- self.sampled = False
1167
- return
1168
-
1169
- # if the user has forced a sampling decision by passing a `sampled`
1170
- # value when starting the transaction, go with that
1171
- if self.sampled is not None:
1172
- self.sample_rate = float(self.sampled)
1173
- return
1174
-
1175
- # we would have bailed already if neither `traces_sampler` nor
1176
- # `traces_sample_rate` were defined, so one of these should work; prefer
1177
- # the hook if so
1178
- sample_rate = (
1179
- client.options["traces_sampler"](sampling_context)
1180
- if callable(client.options.get("traces_sampler"))
1181
- else (
1182
- # default inheritance behavior
1183
- sampling_context["parent_sampled"]
1184
- if sampling_context["parent_sampled"] is not None
1185
- else client.options["traces_sample_rate"]
1186
- )
1187
- )
1188
-
1189
- # Since this is coming from the user (or from a function provided by the
1190
- # user), who knows what we might get. (The only valid values are
1191
- # booleans or numbers between 0 and 1.)
1192
- if not is_valid_sample_rate(sample_rate, source="Tracing"):
1193
- logger.warning(
1194
- "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format(
1195
- transaction_description=transaction_description,
1196
- )
1197
- )
1198
- self.sampled = False
1199
- return
1200
-
1201
- self.sample_rate = float(sample_rate)
1202
-
1203
- if client.monitor:
1204
- self.sample_rate /= 2**client.monitor.downsample_factor
1205
-
1206
- # if the function returned 0 (or false), or if `traces_sample_rate` is
1207
- # 0, it's a sign the transaction should be dropped
1208
- if not self.sample_rate:
1209
- logger.debug(
1210
- "[Tracing] Discarding {transaction_description} because {reason}".format(
1211
- transaction_description=transaction_description,
1212
- reason=(
1213
- "traces_sampler returned 0 or False"
1214
- if callable(client.options.get("traces_sampler"))
1215
- else "traces_sample_rate is set to 0"
1216
- ),
1217
- )
1218
- )
1219
- self.sampled = False
1220
- return
1221
-
1222
- # Now we roll the dice.
1223
- self.sampled = self._sample_rand < Decimal.from_float(self.sample_rate)
1224
-
1225
- if self.sampled:
1226
- logger.debug(
1227
- "[Tracing] Starting {transaction_description}".format(
1228
- transaction_description=transaction_description,
1229
- )
1230
- )
1231
- else:
1232
- logger.debug(
1233
- "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format(
1234
- transaction_description=transaction_description,
1235
- sample_rate=self.sample_rate,
1236
- )
1237
- )
1238
-
1239
-
1240
- class NoOpSpan(Span):
1241
- def __repr__(self):
1242
- # type: () -> str
1243
- return "<%s>" % self.__class__.__name__
1244
-
1245
- @property
1246
- def containing_transaction(self):
1247
- # type: () -> Optional[Transaction]
1248
- return None
1249
-
1250
- def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
1251
- # type: (str, **Any) -> NoOpSpan
1252
- return NoOpSpan()
1253
-
1254
- def to_traceparent(self):
1255
- # type: () -> str
1256
- return ""
1257
-
1258
- def to_baggage(self):
1259
- # type: () -> Optional[Baggage]
1260
- return None
1261
-
1262
- def get_baggage(self):
1263
- # type: () -> Optional[Baggage]
1264
- return None
1265
-
1266
- def iter_headers(self):
1267
- # type: () -> Iterator[Tuple[str, str]]
1268
- return iter(())
1269
-
1270
- def set_tag(self, key, value):
1271
- # type: (str, Any) -> None
1272
- pass
1273
-
1274
- def set_data(self, key, value):
1275
- # type: (str, Any) -> None
1276
- pass
1277
-
1278
- def set_status(self, value):
1279
- # type: (str) -> None
1280
- pass
1281
-
1282
- def set_http_status(self, http_status):
1283
- # type: (int) -> None
1284
- pass
555
+ self.set_attribute(SPANDATA.HTTP_STATUS_CODE, http_status)
556
+ self.set_status(get_span_status_from_http_code(http_status))
1285
557
 
1286
558
  def is_success(self):
1287
559
  # type: () -> bool
1288
- return True
560
+ return self.status == SPANSTATUS.OK
561
+
562
+ def finish(self, end_timestamp=None):
563
+ # type: (Optional[Union[float, datetime]]) -> None
564
+ if end_timestamp is not None:
565
+ self._otel_span.end(convert_to_otel_timestamp(end_timestamp))
566
+ else:
567
+ self._otel_span.end()
1289
568
 
1290
569
  def to_json(self):
1291
- # type: () -> Dict[str, Any]
1292
- return {}
570
+ # type: () -> dict[str, Any]
571
+ """
572
+ Only meant for testing. Not used internally anymore.
573
+ """
574
+ if not isinstance(self._otel_span, ReadableSpan):
575
+ return {}
576
+ return json.loads(self._otel_span.to_json())
1293
577
 
1294
578
  def get_trace_context(self):
1295
- # type: () -> Any
1296
- return {}
579
+ # type: () -> dict[str, Any]
580
+ if not isinstance(self._otel_span, ReadableSpan):
581
+ return {}
1297
582
 
1298
- def get_profile_context(self):
1299
- # type: () -> Any
1300
- return {}
583
+ return get_trace_context(self._otel_span)
1301
584
 
1302
- def finish(
1303
- self,
1304
- scope=None, # type: Optional[sentry_sdk.Scope]
1305
- end_timestamp=None, # type: Optional[Union[float, datetime]]
1306
- *,
1307
- hub=None, # type: Optional[sentry_sdk.Hub]
1308
- ):
1309
- # type: (...) -> Optional[str]
1310
- """
1311
- The `hub` parameter is deprecated. Please use the `scope` parameter, instead.
1312
- """
1313
- pass
585
+ def set_context(self, key, value):
586
+ # type: (str, Any) -> None
587
+ # TODO-neel-potel we cannot add dicts here
1314
588
 
1315
- def set_measurement(self, name, value, unit=""):
1316
- # type: (str, float, MeasurementUnit) -> None
1317
- pass
589
+ self.set_attribute(f"{SentrySpanAttribute.CONTEXT}.{key}", value)
1318
590
 
1319
- def set_context(self, key, value):
1320
- # type: (str, dict[str, Any]) -> None
1321
- pass
591
+ def set_flag(self, flag, value):
592
+ # type: (str, bool) -> None
593
+ flag_count = self.get_attribute("_flag.count") or 0
594
+ if flag_count < _FLAGS_CAPACITY:
595
+ self.set_attribute(f"flag.evaluation.{flag}", value)
596
+ self.set_attribute("_flag.count", flag_count + 1)
1322
597
 
1323
- def init_span_recorder(self, maxlen):
1324
- # type: (int) -> None
1325
- pass
1326
598
 
1327
- def _set_initial_sampling_decision(self, sampling_context):
1328
- # type: (SamplingContext) -> None
1329
- pass
599
+ # TODO-neel-potel add deprecation
600
+ Transaction = Span
1330
601
 
1331
602
 
1332
603
  if TYPE_CHECKING:
@@ -1369,20 +640,3 @@ def trace(func=None):
1369
640
  return start_child_span_decorator(func)
1370
641
  else:
1371
642
  return start_child_span_decorator
1372
-
1373
-
1374
- # Circular imports
1375
-
1376
- from sentry_sdk.tracing_utils import (
1377
- Baggage,
1378
- EnvironHeaders,
1379
- extract_sentrytrace_data,
1380
- _generate_sample_rand,
1381
- has_tracing_enabled,
1382
- maybe_create_breadcrumbs_from_span,
1383
- )
1384
-
1385
- with warnings.catch_warnings():
1386
- # The code in this file which uses `LocalAggregator` is only called from the deprecated `metrics` module.
1387
- warnings.simplefilter("ignore", DeprecationWarning)
1388
- from sentry_sdk.metrics import LocalAggregator