sentry-sdk 2.26.1__py2.py3-none-any.whl → 3.0.0a1__py2.py3-none-any.whl

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

Potentially problematic release.


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

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