sentry-sdk 3.0.0a1__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 (34) hide show
  1. sentry_sdk/_types.py +2 -2
  2. sentry_sdk/ai/monitoring.py +7 -6
  3. sentry_sdk/client.py +31 -6
  4. sentry_sdk/consts.py +83 -5
  5. sentry_sdk/integrations/_asgi_common.py +2 -3
  6. sentry_sdk/integrations/arq.py +2 -1
  7. sentry_sdk/integrations/celery/__init__.py +5 -5
  8. sentry_sdk/integrations/celery/beat.py +2 -2
  9. sentry_sdk/integrations/cohere.py +10 -10
  10. sentry_sdk/integrations/django/asgi.py +2 -2
  11. sentry_sdk/integrations/grpc/__init__.py +18 -1
  12. sentry_sdk/integrations/huggingface_hub.py +2 -2
  13. sentry_sdk/integrations/logging.py +26 -32
  14. sentry_sdk/integrations/loguru.py +124 -50
  15. sentry_sdk/integrations/openai.py +4 -4
  16. sentry_sdk/integrations/redis/_async_common.py +10 -3
  17. sentry_sdk/integrations/redis/_sync_common.py +6 -1
  18. sentry_sdk/integrations/redis/redis_cluster.py +11 -5
  19. sentry_sdk/integrations/redis/utils.py +3 -3
  20. sentry_sdk/integrations/tornado.py +1 -1
  21. sentry_sdk/logger.py +32 -5
  22. sentry_sdk/opentelemetry/contextvars_context.py +9 -1
  23. sentry_sdk/opentelemetry/span_processor.py +7 -1
  24. sentry_sdk/opentelemetry/tracing.py +27 -3
  25. sentry_sdk/opentelemetry/utils.py +8 -0
  26. sentry_sdk/scope.py +2 -11
  27. sentry_sdk/tracing.py +12 -12
  28. sentry_sdk/tracing_utils.py +4 -2
  29. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a2.dist-info}/METADATA +3 -3
  30. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a2.dist-info}/RECORD +34 -34
  31. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a2.dist-info}/WHEEL +1 -1
  32. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a2.dist-info}/entry_points.txt +0 -0
  33. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a2.dist-info}/licenses/LICENSE +0 -0
  34. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a2.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,27 @@
1
1
  import enum
2
2
 
3
+ import sentry_sdk
3
4
  from sentry_sdk.integrations import Integration, DidNotEnable
4
5
  from sentry_sdk.integrations.logging import (
5
6
  BreadcrumbHandler,
6
7
  EventHandler,
7
8
  _BaseHandler,
8
9
  )
10
+ from sentry_sdk.logger import _log_level_to_otel
9
11
 
10
12
  from typing import TYPE_CHECKING
11
13
 
12
14
  if TYPE_CHECKING:
13
15
  from logging import LogRecord
14
- from typing import Optional, Tuple, Any
16
+ from typing import Any, Optional
15
17
 
16
18
  try:
17
19
  import loguru
18
20
  from loguru import logger
19
21
  from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT
22
+
23
+ if TYPE_CHECKING:
24
+ from loguru import Message
20
25
  except ImportError:
21
26
  raise DidNotEnable("LOGURU is not installed")
22
27
 
@@ -31,6 +36,10 @@ class LoggingLevels(enum.IntEnum):
31
36
  CRITICAL = 50
32
37
 
33
38
 
39
+ DEFAULT_LEVEL = LoggingLevels.INFO.value
40
+ DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
41
+
42
+
34
43
  SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
35
44
  "TRACE": "DEBUG",
36
45
  "DEBUG": "DEBUG",
@@ -41,59 +50,76 @@ SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
41
50
  "CRITICAL": "CRITICAL",
42
51
  }
43
52
 
44
- DEFAULT_LEVEL = LoggingLevels.INFO.value
45
- DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
46
- # We need to save the handlers to be able to remove them later
47
- # in tests (they call `LoguruIntegration.__init__` multiple times,
48
- # and we can't use `setup_once` because it's called before
49
- # than we get configuration).
50
- _ADDED_HANDLERS = (None, None) # type: Tuple[Optional[int], Optional[int]]
53
+ # Map Loguru level numbers to corresponding OTel level numbers
54
+ SEVERITY_TO_OTEL_SEVERITY = {
55
+ LoggingLevels.CRITICAL: 21, # fatal
56
+ LoggingLevels.ERROR: 17, # error
57
+ LoggingLevels.WARNING: 13, # warn
58
+ LoggingLevels.SUCCESS: 11, # info
59
+ LoggingLevels.INFO: 9, # info
60
+ LoggingLevels.DEBUG: 5, # debug
61
+ LoggingLevels.TRACE: 1, # trace
62
+ }
51
63
 
52
64
 
53
65
  class LoguruIntegration(Integration):
54
66
  identifier = "loguru"
55
67
 
68
+ level = DEFAULT_LEVEL # type: Optional[int]
69
+ event_level = DEFAULT_EVENT_LEVEL # type: Optional[int]
70
+ breadcrumb_format = DEFAULT_FORMAT
71
+ event_format = DEFAULT_FORMAT
72
+ sentry_logs_level = DEFAULT_LEVEL # type: Optional[int]
73
+
56
74
  def __init__(
57
75
  self,
58
76
  level=DEFAULT_LEVEL,
59
77
  event_level=DEFAULT_EVENT_LEVEL,
60
78
  breadcrumb_format=DEFAULT_FORMAT,
61
79
  event_format=DEFAULT_FORMAT,
80
+ sentry_logs_level=DEFAULT_LEVEL,
62
81
  ):
63
- # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction) -> None
64
- global _ADDED_HANDLERS
65
- breadcrumb_handler, event_handler = _ADDED_HANDLERS
66
-
67
- if breadcrumb_handler is not None:
68
- logger.remove(breadcrumb_handler)
69
- breadcrumb_handler = None
70
- if event_handler is not None:
71
- logger.remove(event_handler)
72
- event_handler = None
73
-
74
- if level is not None:
75
- breadcrumb_handler = logger.add(
76
- LoguruBreadcrumbHandler(level=level),
77
- level=level,
78
- format=breadcrumb_format,
79
- )
80
-
81
- if event_level is not None:
82
- event_handler = logger.add(
83
- LoguruEventHandler(level=event_level),
84
- level=event_level,
85
- format=event_format,
86
- )
87
-
88
- _ADDED_HANDLERS = (breadcrumb_handler, event_handler)
82
+ # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[int]) -> None
83
+ LoguruIntegration.level = level
84
+ LoguruIntegration.event_level = event_level
85
+ LoguruIntegration.breadcrumb_format = breadcrumb_format
86
+ LoguruIntegration.event_format = event_format
87
+ LoguruIntegration.sentry_logs_level = sentry_logs_level
89
88
 
90
89
  @staticmethod
91
90
  def setup_once():
92
91
  # type: () -> None
93
- pass # we do everything in __init__
92
+ if LoguruIntegration.level is not None:
93
+ logger.add(
94
+ LoguruBreadcrumbHandler(level=LoguruIntegration.level),
95
+ level=LoguruIntegration.level,
96
+ format=LoguruIntegration.breadcrumb_format,
97
+ )
98
+
99
+ if LoguruIntegration.event_level is not None:
100
+ logger.add(
101
+ LoguruEventHandler(level=LoguruIntegration.event_level),
102
+ level=LoguruIntegration.event_level,
103
+ format=LoguruIntegration.event_format,
104
+ )
105
+
106
+ if LoguruIntegration.sentry_logs_level is not None:
107
+ logger.add(
108
+ loguru_sentry_logs_handler,
109
+ level=LoguruIntegration.sentry_logs_level,
110
+ )
94
111
 
95
112
 
96
113
  class _LoguruBaseHandler(_BaseHandler):
114
+ def __init__(self, *args, **kwargs):
115
+ # type: (*Any, **Any) -> None
116
+ if kwargs.get("level"):
117
+ kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
118
+ kwargs.get("level", ""), DEFAULT_LEVEL
119
+ )
120
+
121
+ super().__init__(*args, **kwargs)
122
+
97
123
  def _logging_to_event_level(self, record):
98
124
  # type: (LogRecord) -> str
99
125
  try:
@@ -107,24 +133,72 @@ class _LoguruBaseHandler(_BaseHandler):
107
133
  class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
108
134
  """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
109
135
 
110
- def __init__(self, *args, **kwargs):
111
- # type: (*Any, **Any) -> None
112
- if kwargs.get("level"):
113
- kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
114
- kwargs.get("level", ""), DEFAULT_LEVEL
115
- )
116
-
117
- super().__init__(*args, **kwargs)
136
+ pass
118
137
 
119
138
 
120
139
  class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
121
140
  """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
122
141
 
123
- def __init__(self, *args, **kwargs):
124
- # type: (*Any, **Any) -> None
125
- if kwargs.get("level"):
126
- kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
127
- kwargs.get("level", ""), DEFAULT_LEVEL
128
- )
142
+ pass
129
143
 
130
- super().__init__(*args, **kwargs)
144
+
145
+ def loguru_sentry_logs_handler(message):
146
+ # type: (Message) -> None
147
+ # This is intentionally a callable sink instead of a standard logging handler
148
+ # since otherwise we wouldn't get direct access to message.record
149
+ client = sentry_sdk.get_client()
150
+
151
+ if not client.is_active():
152
+ return
153
+
154
+ if not client.options["_experiments"].get("enable_logs", False):
155
+ return
156
+
157
+ record = message.record
158
+
159
+ if (
160
+ LoguruIntegration.sentry_logs_level is None
161
+ or record["level"].no < LoguruIntegration.sentry_logs_level
162
+ ):
163
+ return
164
+
165
+ otel_severity_number, otel_severity_text = _log_level_to_otel(
166
+ record["level"].no, SEVERITY_TO_OTEL_SEVERITY
167
+ )
168
+
169
+ attrs = {"sentry.origin": "auto.logger.loguru"} # type: dict[str, Any]
170
+
171
+ project_root = client.options["project_root"]
172
+ if record.get("file"):
173
+ if project_root is not None and record["file"].path.startswith(project_root):
174
+ attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :]
175
+ else:
176
+ attrs["code.file.path"] = record["file"].path
177
+
178
+ if record.get("line") is not None:
179
+ attrs["code.line.number"] = record["line"]
180
+
181
+ if record.get("function"):
182
+ attrs["code.function.name"] = record["function"]
183
+
184
+ if record.get("thread"):
185
+ attrs["thread.name"] = record["thread"].name
186
+ attrs["thread.id"] = record["thread"].id
187
+
188
+ if record.get("process"):
189
+ attrs["process.pid"] = record["process"].id
190
+ attrs["process.executable.name"] = record["process"].name
191
+
192
+ if record.get("name"):
193
+ attrs["logger.name"] = record["name"]
194
+
195
+ client._capture_experimental_log(
196
+ {
197
+ "severity_text": otel_severity_text,
198
+ "severity_number": otel_severity_number,
199
+ "body": record["message"],
200
+ "attributes": attrs,
201
+ "time_unix_nano": int(record["time"].timestamp() * 1e9),
202
+ "trace_id": None,
203
+ }
204
+ )
@@ -156,7 +156,7 @@ def _new_chat_completion_common(f, *args, **kwargs):
156
156
  if should_send_default_pii() and integration.include_prompts:
157
157
  set_data_normalized(
158
158
  span,
159
- "ai.responses",
159
+ SPANDATA.AI_RESPONSES,
160
160
  list(map(lambda x: x.message, res.choices)),
161
161
  )
162
162
  _calculate_chat_completion_usage(
@@ -331,15 +331,15 @@ def _new_embeddings_create_common(f, *args, **kwargs):
331
331
  should_send_default_pii() and integration.include_prompts
332
332
  ):
333
333
  if isinstance(kwargs["input"], str):
334
- set_data_normalized(span, "ai.input_messages", [kwargs["input"]])
334
+ set_data_normalized(span, SPANDATA.AI_INPUT_MESSAGES, [kwargs["input"]])
335
335
  elif (
336
336
  isinstance(kwargs["input"], list)
337
337
  and len(kwargs["input"]) > 0
338
338
  and isinstance(kwargs["input"][0], str)
339
339
  ):
340
- set_data_normalized(span, "ai.input_messages", kwargs["input"])
340
+ set_data_normalized(span, SPANDATA.AI_INPUT_MESSAGES, kwargs["input"])
341
341
  if "model" in kwargs:
342
- set_data_normalized(span, "ai.model_id", kwargs["model"])
342
+ set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"])
343
343
 
344
344
  response = yield f, args, kwargs
345
345
 
@@ -44,13 +44,20 @@ def patch_redis_async_pipeline(
44
44
  ) as span:
45
45
  with capture_internal_exceptions():
46
46
  span_data = get_db_data_fn(self)
47
+
48
+ try:
49
+ command_seq = self._execution_strategy._command_queue
50
+ except AttributeError:
51
+ if is_cluster:
52
+ command_seq = self._command_stack
53
+ else:
54
+ command_seq = self.command_stack
55
+
47
56
  pipeline_data = _get_pipeline_data(
48
57
  is_cluster=is_cluster,
49
58
  get_command_args_fn=get_command_args_fn,
50
59
  is_transaction=False if is_cluster else self.is_transaction,
51
- command_stack=(
52
- self._command_stack if is_cluster else self.command_stack
53
- ),
60
+ command_seq=command_seq,
54
61
  )
55
62
  _update_span(span, span_data, pipeline_data)
56
63
  _create_breadcrumb("redis.pipeline.execute", span_data, pipeline_data)
@@ -44,12 +44,17 @@ def patch_redis_pipeline(
44
44
  only_if_parent=True,
45
45
  ) as span:
46
46
  with capture_internal_exceptions():
47
+ try:
48
+ command_seq = self._execution_strategy.command_queue
49
+ except AttributeError:
50
+ command_seq = self.command_stack
51
+
47
52
  span_data = get_db_data_fn(self)
48
53
  pipeline_data = _get_pipeline_data(
49
54
  is_cluster=is_cluster,
50
55
  get_command_args_fn=get_command_args_fn,
51
56
  is_transaction=False if is_cluster else self.transaction,
52
- command_stack=self.command_stack,
57
+ command_seq=command_seq,
53
58
  )
54
59
  _update_span(span, span_data, pipeline_data)
55
60
  _create_breadcrumb("redis.pipeline.execute", span_data, pipeline_data)
@@ -37,11 +37,17 @@ def _get_async_cluster_db_data(async_redis_cluster_instance):
37
37
  def _get_async_cluster_pipeline_db_data(async_redis_cluster_pipeline_instance):
38
38
  # type: (AsyncClusterPipeline[Any]) -> dict[str, Any]
39
39
  with capture_internal_exceptions():
40
- return _get_async_cluster_db_data(
41
- # the AsyncClusterPipeline has always had a `_client` attr but it is private so potentially problematic and mypy
42
- # does not recognize it - see https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386
43
- async_redis_cluster_pipeline_instance._client, # type: ignore[attr-defined]
44
- )
40
+ client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None)
41
+ if client is None:
42
+ # In older redis-py versions, the AsyncClusterPipeline had a `_client`
43
+ # attr but it is private so potentially problematic and mypy does not
44
+ # recognize it - see
45
+ # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386
46
+ client = (
47
+ async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined]
48
+ )
49
+
50
+ return _get_async_cluster_db_data(client)
45
51
 
46
52
 
47
53
  def _get_cluster_db_data(redis_cluster_instance):
@@ -147,7 +147,7 @@ def _parse_rediscluster_command(command):
147
147
  return command.args
148
148
 
149
149
 
150
- def _get_pipeline_data(is_cluster, get_command_args_fn, is_transaction, command_stack):
150
+ def _get_pipeline_data(is_cluster, get_command_args_fn, is_transaction, command_seq):
151
151
  # type: (bool, Any, bool, Sequence[Any]) -> dict[str, Any]
152
152
  data = {
153
153
  "redis.is_cluster": is_cluster,
@@ -155,14 +155,14 @@ def _get_pipeline_data(is_cluster, get_command_args_fn, is_transaction, command_
155
155
  } # type: dict[str, Any]
156
156
 
157
157
  commands = []
158
- for i, arg in enumerate(command_stack):
158
+ for i, arg in enumerate(command_seq):
159
159
  if i >= _MAX_NUM_COMMANDS:
160
160
  break
161
161
 
162
162
  command = get_command_args_fn(arg)
163
163
  commands.append(_get_safe_command(command[0], command[1:]))
164
164
 
165
- data["redis.commands.count"] = len(command_stack)
165
+ data["redis.commands.count"] = len(command_seq)
166
166
  data["redis.commands.first_ten"] = commands
167
167
 
168
168
  return data
@@ -85,7 +85,7 @@ class TornadoIntegration(Integration):
85
85
  else:
86
86
 
87
87
  @coroutine # type: ignore
88
- def sentry_execute_request_handler(self, *args, **kwargs): # type: ignore
88
+ def sentry_execute_request_handler(self, *args, **kwargs):
89
89
  # type: (RequestHandler, *Any, **Any) -> Any
90
90
  with _handle_request_impl(self):
91
91
  result = yield from old_execute(self, *args, **kwargs)
sentry_sdk/logger.py CHANGED
@@ -3,14 +3,24 @@ import functools
3
3
  import time
4
4
  from typing import Any
5
5
 
6
- from sentry_sdk import get_client, get_current_scope
6
+ from sentry_sdk import get_client
7
7
  from sentry_sdk.utils import safe_repr
8
8
 
9
+ OTEL_RANGES = [
10
+ # ((severity level range), severity text)
11
+ # https://opentelemetry.io/docs/specs/otel/logs/data-model
12
+ ((1, 4), "trace"),
13
+ ((5, 8), "debug"),
14
+ ((9, 12), "info"),
15
+ ((13, 16), "warn"),
16
+ ((17, 20), "error"),
17
+ ((21, 24), "fatal"),
18
+ ]
19
+
9
20
 
10
21
  def _capture_log(severity_text, severity_number, template, **kwargs):
11
22
  # type: (str, int, str, **Any) -> None
12
23
  client = get_client()
13
- scope = get_current_scope()
14
24
 
15
25
  attrs = {
16
26
  "sentry.message.template": template,
@@ -18,7 +28,7 @@ def _capture_log(severity_text, severity_number, template, **kwargs):
18
28
  if "attributes" in kwargs:
19
29
  attrs.update(kwargs.pop("attributes"))
20
30
  for k, v in kwargs.items():
21
- attrs[f"sentry.message.parameters.{k}"] = v
31
+ attrs[f"sentry.message.parameter.{k}"] = v
22
32
 
23
33
  attrs = {
24
34
  k: (
@@ -36,7 +46,6 @@ def _capture_log(severity_text, severity_number, template, **kwargs):
36
46
 
37
47
  # noinspection PyProtectedMember
38
48
  client._capture_experimental_log(
39
- scope,
40
49
  {
41
50
  "severity_text": severity_text,
42
51
  "severity_number": severity_number,
@@ -51,6 +60,24 @@ def _capture_log(severity_text, severity_number, template, **kwargs):
51
60
  trace = functools.partial(_capture_log, "trace", 1)
52
61
  debug = functools.partial(_capture_log, "debug", 5)
53
62
  info = functools.partial(_capture_log, "info", 9)
54
- warning = functools.partial(_capture_log, "warning", 13)
63
+ warning = functools.partial(_capture_log, "warn", 13)
55
64
  error = functools.partial(_capture_log, "error", 17)
56
65
  fatal = functools.partial(_capture_log, "fatal", 21)
66
+
67
+
68
+ def _otel_severity_text(otel_severity_number):
69
+ # type: (int) -> str
70
+ for (lower, upper), severity in OTEL_RANGES:
71
+ if lower <= otel_severity_number <= upper:
72
+ return severity
73
+
74
+ return "default"
75
+
76
+
77
+ def _log_level_to_otel(level, mapping):
78
+ # type: (int, dict[Any, int]) -> tuple[int, str]
79
+ for py_level, otel_severity_number in sorted(mapping.items(), reverse=True):
80
+ if level >= py_level:
81
+ return otel_severity_number, _otel_severity_text(otel_severity_number)
82
+
83
+ return 0, "default"
@@ -1,10 +1,12 @@
1
1
  from typing import cast, TYPE_CHECKING
2
2
 
3
- from opentelemetry.trace import set_span_in_context
3
+ from opentelemetry.trace import get_current_span, set_span_in_context
4
+ from opentelemetry.trace.span import INVALID_SPAN
4
5
  from opentelemetry.context import Context, get_value, set_value
5
6
  from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext
6
7
 
7
8
  import sentry_sdk
9
+ from sentry_sdk.tracing import Span
8
10
  from sentry_sdk.opentelemetry.consts import (
9
11
  SENTRY_SCOPES_KEY,
10
12
  SENTRY_FORK_ISOLATION_SCOPE_KEY,
@@ -60,6 +62,12 @@ class SentryContextVarsRuntimeContext(ContextVarsRuntimeContext):
60
62
  else:
61
63
  new_scope = current_scope.fork()
62
64
 
65
+ # carry forward a wrapped span reference since the otel context is always the
66
+ # source of truth for the active span
67
+ current_span = get_current_span(context)
68
+ if current_span != INVALID_SPAN:
69
+ new_scope._span = Span(otel_span=get_current_span(context))
70
+
63
71
  if should_use_isolation_scope:
64
72
  new_isolation_scope = should_use_isolation_scope
65
73
  elif should_fork_isolation_scope:
@@ -29,6 +29,7 @@ from sentry_sdk.opentelemetry.utils import (
29
29
  get_profile_context,
30
30
  get_sentry_meta,
31
31
  set_sentry_meta,
32
+ delete_sentry_meta,
32
33
  )
33
34
  from sentry_sdk.profiler.continuous_profiler import (
34
35
  try_autostart_continuous_profiler,
@@ -173,6 +174,7 @@ class SentrySpanProcessor(SpanProcessor):
173
174
  # TODO-neel-potel sort and cutoff max spans
174
175
 
175
176
  sentry_sdk.capture_event(transaction_event)
177
+ self._cleanup_references([span] + collected_spans)
176
178
 
177
179
  def _append_child_span(self, span):
178
180
  # type: (ReadableSpan) -> None
@@ -253,7 +255,6 @@ class SentrySpanProcessor(SpanProcessor):
253
255
  profile.__exit__(None, None, None)
254
256
  if profile.valid():
255
257
  event["profile"] = profile
256
- set_sentry_meta(span, "profile", None)
257
258
 
258
259
  return event
259
260
 
@@ -314,6 +315,11 @@ class SentrySpanProcessor(SpanProcessor):
314
315
 
315
316
  return common_json
316
317
 
318
+ def _cleanup_references(self, spans):
319
+ # type: (List[ReadableSpan]) -> None
320
+ for span in spans:
321
+ delete_sentry_meta(span)
322
+
317
323
  def _log_debug_info(self):
318
324
  # type: () -> None
319
325
  import pprint
@@ -7,6 +7,7 @@ from sentry_sdk.opentelemetry import (
7
7
  SentrySampler,
8
8
  SentrySpanProcessor,
9
9
  )
10
+ from sentry_sdk.utils import logger
10
11
 
11
12
 
12
13
  def patch_readable_span():
@@ -28,8 +29,31 @@ def patch_readable_span():
28
29
 
29
30
  def setup_sentry_tracing():
30
31
  # type: () -> None
31
- provider = TracerProvider(sampler=SentrySampler())
32
- provider.add_span_processor(SentrySpanProcessor())
33
- trace.set_tracer_provider(provider)
32
+ # TracerProvider can only be set once. If we're the first ones setting it,
33
+ # there's no issue. If it already exists, we need to patch it.
34
+ from opentelemetry.trace import _TRACER_PROVIDER
35
+
36
+ if _TRACER_PROVIDER is not None:
37
+ logger.debug("[Tracing] Detected an existing TracerProvider, patching")
38
+ tracer_provider = _TRACER_PROVIDER
39
+ tracer_provider.sampler = SentrySampler() # type: ignore[attr-defined]
40
+
41
+ else:
42
+ logger.debug("[Tracing] No TracerProvider set, creating a new one")
43
+ tracer_provider = TracerProvider(sampler=SentrySampler())
44
+ trace.set_tracer_provider(tracer_provider)
45
+
46
+ try:
47
+ existing_span_processors = (
48
+ tracer_provider._active_span_processor._span_processors # type: ignore[attr-defined]
49
+ )
50
+ except Exception:
51
+ existing_span_processors = []
52
+
53
+ for span_processor in existing_span_processors:
54
+ if isinstance(span_processor, SentrySpanProcessor):
55
+ break
56
+ else:
57
+ tracer_provider.add_span_processor(SentrySpanProcessor()) # type: ignore[attr-defined]
34
58
 
35
59
  set_global_textmap(SentryPropagator())
@@ -464,6 +464,14 @@ def set_sentry_meta(span, key, value):
464
464
  span._sentry_meta = sentry_meta # type: ignore[union-attr]
465
465
 
466
466
 
467
+ def delete_sentry_meta(span):
468
+ # type: (Union[AbstractSpan, ReadableSpan]) -> None
469
+ try:
470
+ del span._sentry_meta # type: ignore[union-attr]
471
+ except AttributeError:
472
+ pass
473
+
474
+
467
475
  def get_profile_context(span):
468
476
  # type: (ReadableSpan) -> Optional[dict[str, str]]
469
477
  if not span.attributes:
sentry_sdk/scope.py CHANGED
@@ -172,8 +172,8 @@ class Scope:
172
172
  "_flags",
173
173
  )
174
174
 
175
- def __init__(self, ty=None, client=None):
176
- # type: (Optional[ScopeType], Optional[sentry_sdk.Client]) -> None
175
+ def __init__(self, ty=None):
176
+ # type: (Optional[ScopeType]) -> None
177
177
  self._type = ty
178
178
 
179
179
  self._event_processors = [] # type: List[EventProcessor]
@@ -185,9 +185,6 @@ class Scope:
185
185
 
186
186
  self.client = NonRecordingClient() # type: sentry_sdk.client.BaseClient
187
187
 
188
- if client is not None:
189
- self.set_client(client)
190
-
191
188
  self.clear()
192
189
 
193
190
  incoming_trace_information = self._load_trace_data_from_env()
@@ -732,12 +729,6 @@ class Scope:
732
729
  """Get current tracing span."""
733
730
  return self._span
734
731
 
735
- @span.setter
736
- def span(self, span):
737
- # type: (Optional[Span]) -> None
738
- """Set current tracing span."""
739
- self._span = span
740
-
741
732
  @property
742
733
  def profile(self):
743
734
  # type: () -> Optional[Profile]
sentry_sdk/tracing.py CHANGED
@@ -15,7 +15,6 @@ from opentelemetry.trace.status import Status, StatusCode
15
15
  from opentelemetry.sdk.trace import ReadableSpan
16
16
  from opentelemetry.version import __version__ as otel_version
17
17
 
18
- import sentry_sdk
19
18
  from sentry_sdk.consts import (
20
19
  DEFAULT_SPAN_NAME,
21
20
  DEFAULT_SPAN_ORIGIN,
@@ -189,7 +188,7 @@ class Span:
189
188
  If otel_span is passed explicitly, just acts as a proxy.
190
189
 
191
190
  If span is passed explicitly, use it. The only purpose of this param
192
- if backwards compatibility with start_transaction(transaction=...).
191
+ is backwards compatibility with start_transaction(transaction=...).
193
192
 
194
193
  If only_if_parent is True, just return an INVALID_SPAN
195
194
  and avoid instrumentation if there's no active parent span.
@@ -271,19 +270,21 @@ class Span:
271
270
  )
272
271
  )
273
272
 
274
- def __enter__(self):
275
- # type: () -> 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
273
+ def activate(self):
274
+ # type: () -> None
279
275
  ctx = otel_trace.set_span_in_context(self._otel_span)
280
276
  # set as the implicit current context
281
277
  self._ctx_token = context.attach(ctx)
282
278
 
283
- # get the new scope that was forked on context.attach
284
- self.scope = sentry_sdk.get_current_scope()
285
- self.scope.span = self
279
+ def deactivate(self):
280
+ # type: () -> None
281
+ if self._ctx_token:
282
+ context.detach(self._ctx_token)
283
+ del self._ctx_token
286
284
 
285
+ def __enter__(self):
286
+ # type: () -> Span
287
+ self.activate()
287
288
  return self
288
289
 
289
290
  def __exit__(self, ty, value, tb):
@@ -299,8 +300,7 @@ class Span:
299
300
  self.set_status(SPANSTATUS.OK)
300
301
 
301
302
  self.finish()
302
- context.detach(self._ctx_token)
303
- del self._ctx_token
303
+ self.deactivate()
304
304
 
305
305
  @property
306
306
  def description(self):
@@ -729,9 +729,10 @@ def start_child_span_decorator(func):
729
729
  )
730
730
  return await func(*args, **kwargs)
731
731
 
732
- with span.start_child(
732
+ with sentry_sdk.start_span(
733
733
  op=OP.FUNCTION,
734
734
  name=qualname_from_function(func),
735
+ only_if_parent=True,
735
736
  ):
736
737
  return await func(*args, **kwargs)
737
738
 
@@ -757,9 +758,10 @@ def start_child_span_decorator(func):
757
758
  )
758
759
  return func(*args, **kwargs)
759
760
 
760
- with span.start_child(
761
+ with sentry_sdk.start_span(
761
762
  op=OP.FUNCTION,
762
763
  name=qualname_from_function(func),
764
+ only_if_parent=True,
763
765
  ):
764
766
  return func(*args, **kwargs)
765
767