sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +81 -19
  8. sentry_sdk/_types.py +311 -11
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +35 -28
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +20 -11
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +71 -60
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,529 @@
1
+ import sys
2
+ from collections.abc import Mapping
3
+ from functools import wraps
4
+
5
+ import sentry_sdk
6
+ from sentry_sdk import isolation_scope
7
+ from sentry_sdk.api import continue_trace
8
+ from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA
9
+ from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
10
+ from sentry_sdk.integrations.celery.beat import (
11
+ _patch_beat_apply_entry,
12
+ _patch_redbeat_apply_async,
13
+ _setup_celery_beat_signals,
14
+ )
15
+ from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch
16
+ from sentry_sdk.integrations.logging import ignore_logger
17
+ from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, TransactionSource
18
+ from sentry_sdk.tracing_utils import Baggage
19
+ from sentry_sdk.utils import (
20
+ capture_internal_exceptions,
21
+ ensure_integration_enabled,
22
+ event_from_exception,
23
+ reraise,
24
+ )
25
+
26
+ from typing import TYPE_CHECKING
27
+
28
+ if TYPE_CHECKING:
29
+ from typing import Any
30
+ from typing import Callable
31
+ from typing import List
32
+ from typing import Optional
33
+ from typing import TypeVar
34
+ from typing import Union
35
+
36
+ from sentry_sdk._types import EventProcessor, Event, Hint, ExcInfo
37
+ from sentry_sdk.tracing import Span
38
+
39
+ F = TypeVar("F", bound=Callable[..., Any])
40
+
41
+
42
+ try:
43
+ from celery import VERSION as CELERY_VERSION # type: ignore
44
+ from celery.app.task import Task # type: ignore
45
+ from celery.app.trace import task_has_custom
46
+ from celery.exceptions import ( # type: ignore
47
+ Ignore,
48
+ Reject,
49
+ Retry,
50
+ SoftTimeLimitExceeded,
51
+ )
52
+ from kombu import Producer # type: ignore
53
+ except ImportError:
54
+ raise DidNotEnable("Celery not installed")
55
+
56
+
57
+ CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject)
58
+
59
+
60
+ class CeleryIntegration(Integration):
61
+ identifier = "celery"
62
+ origin = f"auto.queue.{identifier}"
63
+
64
+ def __init__(
65
+ self,
66
+ propagate_traces=True,
67
+ monitor_beat_tasks=False,
68
+ exclude_beat_tasks=None,
69
+ ):
70
+ # type: (bool, bool, Optional[List[str]]) -> None
71
+ self.propagate_traces = propagate_traces
72
+ self.monitor_beat_tasks = monitor_beat_tasks
73
+ self.exclude_beat_tasks = exclude_beat_tasks
74
+
75
+ _patch_beat_apply_entry()
76
+ _patch_redbeat_apply_async()
77
+ _setup_celery_beat_signals(monitor_beat_tasks)
78
+
79
+ @staticmethod
80
+ def setup_once():
81
+ # type: () -> None
82
+ _check_minimum_version(CeleryIntegration, CELERY_VERSION)
83
+
84
+ _patch_build_tracer()
85
+ _patch_task_apply_async()
86
+ _patch_celery_send_task()
87
+ _patch_worker_exit()
88
+ _patch_producer_publish()
89
+
90
+ # This logger logs every status of every task that ran on the worker.
91
+ # Meaning that every task's breadcrumbs are full of stuff like "Task
92
+ # <foo> raised unexpected <bar>".
93
+ ignore_logger("celery.worker.job")
94
+ ignore_logger("celery.app.trace")
95
+
96
+ # This is stdout/err redirected to a logger, can't deal with this
97
+ # (need event_level=logging.WARN to reproduce)
98
+ ignore_logger("celery.redirected")
99
+
100
+
101
+ def _set_status(status):
102
+ # type: (str) -> None
103
+ with capture_internal_exceptions():
104
+ scope = sentry_sdk.get_current_scope()
105
+ if scope.span is not None:
106
+ scope.span.set_status(status)
107
+
108
+
109
+ def _capture_exception(task, exc_info):
110
+ # type: (Any, ExcInfo) -> None
111
+ client = sentry_sdk.get_client()
112
+ if client.get_integration(CeleryIntegration) is None:
113
+ return
114
+
115
+ if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS):
116
+ # ??? Doesn't map to anything
117
+ _set_status("aborted")
118
+ return
119
+
120
+ _set_status("internal_error")
121
+
122
+ if hasattr(task, "throws") and isinstance(exc_info[1], task.throws):
123
+ return
124
+
125
+ event, hint = event_from_exception(
126
+ exc_info,
127
+ client_options=client.options,
128
+ mechanism={"type": "celery", "handled": False},
129
+ )
130
+
131
+ sentry_sdk.capture_event(event, hint=hint)
132
+
133
+
134
+ def _make_event_processor(task, uuid, args, kwargs, request=None):
135
+ # type: (Any, Any, Any, Any, Optional[Any]) -> EventProcessor
136
+ def event_processor(event, hint):
137
+ # type: (Event, Hint) -> Optional[Event]
138
+
139
+ with capture_internal_exceptions():
140
+ tags = event.setdefault("tags", {})
141
+ tags["celery_task_id"] = uuid
142
+ extra = event.setdefault("extra", {})
143
+ extra["celery-job"] = {
144
+ "task_name": task.name,
145
+ "args": args,
146
+ "kwargs": kwargs,
147
+ }
148
+
149
+ if "exc_info" in hint:
150
+ with capture_internal_exceptions():
151
+ if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded):
152
+ event["fingerprint"] = [
153
+ "celery",
154
+ "SoftTimeLimitExceeded",
155
+ getattr(task, "name", task),
156
+ ]
157
+
158
+ return event
159
+
160
+ return event_processor
161
+
162
+
163
+ def _update_celery_task_headers(original_headers, span, monitor_beat_tasks):
164
+ # type: (dict[str, Any], Optional[Span], bool) -> dict[str, Any]
165
+ """
166
+ Updates the headers of the Celery task with the tracing information
167
+ and eventually Sentry Crons monitoring information for beat tasks.
168
+ """
169
+ updated_headers = original_headers.copy()
170
+ with capture_internal_exceptions():
171
+ # if span is None (when the task was started by Celery Beat)
172
+ # this will return the trace headers from the scope.
173
+ headers = dict(
174
+ sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span)
175
+ )
176
+
177
+ if monitor_beat_tasks:
178
+ headers.update(
179
+ {
180
+ "sentry-monitor-start-timestamp-s": "%.9f"
181
+ % _now_seconds_since_epoch(),
182
+ }
183
+ )
184
+
185
+ # Add the time the task was enqueued to the headers
186
+ # This is used in the consumer to calculate the latency
187
+ updated_headers.update(
188
+ {"sentry-task-enqueued-time": _now_seconds_since_epoch()}
189
+ )
190
+
191
+ if headers:
192
+ existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME)
193
+ sentry_baggage = headers.get(BAGGAGE_HEADER_NAME)
194
+
195
+ combined_baggage = sentry_baggage or existing_baggage
196
+ if sentry_baggage and existing_baggage:
197
+ # Merge incoming and sentry baggage, where the sentry trace information
198
+ # in the incoming baggage takes precedence and the third-party items
199
+ # are concatenated.
200
+ incoming = Baggage.from_incoming_header(existing_baggage)
201
+ combined = Baggage.from_incoming_header(sentry_baggage)
202
+ combined.sentry_items.update(incoming.sentry_items)
203
+ combined.third_party_items = ",".join(
204
+ [
205
+ x
206
+ for x in [
207
+ combined.third_party_items,
208
+ incoming.third_party_items,
209
+ ]
210
+ if x is not None and x != ""
211
+ ]
212
+ )
213
+ combined_baggage = combined.serialize(include_third_party=True)
214
+
215
+ updated_headers.update(headers)
216
+ if combined_baggage:
217
+ updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage
218
+
219
+ # https://github.com/celery/celery/issues/4875
220
+ #
221
+ # Need to setdefault the inner headers too since other
222
+ # tracing tools (dd-trace-py) also employ this exact
223
+ # workaround and we don't want to break them.
224
+ updated_headers.setdefault("headers", {}).update(headers)
225
+ if combined_baggage:
226
+ updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage
227
+
228
+ # Add the Sentry options potentially added in `sentry_apply_entry`
229
+ # to the headers (done when auto-instrumenting Celery Beat tasks)
230
+ for key, value in updated_headers.items():
231
+ if key.startswith("sentry-"):
232
+ updated_headers["headers"][key] = value
233
+
234
+ return updated_headers
235
+
236
+
237
+ class NoOpMgr:
238
+ def __enter__(self):
239
+ # type: () -> None
240
+ return None
241
+
242
+ def __exit__(self, exc_type, exc_value, traceback):
243
+ # type: (Any, Any, Any) -> None
244
+ return None
245
+
246
+
247
+ def _wrap_task_run(f):
248
+ # type: (F) -> F
249
+ @wraps(f)
250
+ def apply_async(*args, **kwargs):
251
+ # type: (*Any, **Any) -> Any
252
+ # Note: kwargs can contain headers=None, so no setdefault!
253
+ # Unsure which backend though.
254
+ integration = sentry_sdk.get_client().get_integration(CeleryIntegration)
255
+ if integration is None:
256
+ return f(*args, **kwargs)
257
+
258
+ kwarg_headers = kwargs.get("headers") or {}
259
+ propagate_traces = kwarg_headers.pop(
260
+ "sentry-propagate-traces", integration.propagate_traces
261
+ )
262
+
263
+ if not propagate_traces:
264
+ return f(*args, **kwargs)
265
+
266
+ if isinstance(args[0], Task):
267
+ task_name = args[0].name # type: str
268
+ elif len(args) > 1 and isinstance(args[1], str):
269
+ task_name = args[1]
270
+ else:
271
+ task_name = "<unknown Celery task>"
272
+
273
+ task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat"
274
+
275
+ span_mgr = (
276
+ sentry_sdk.start_span(
277
+ op=OP.QUEUE_SUBMIT_CELERY,
278
+ name=task_name,
279
+ origin=CeleryIntegration.origin,
280
+ )
281
+ if not task_started_from_beat
282
+ else NoOpMgr()
283
+ ) # type: Union[Span, NoOpMgr]
284
+
285
+ with span_mgr as span:
286
+ kwargs["headers"] = _update_celery_task_headers(
287
+ kwarg_headers, span, integration.monitor_beat_tasks
288
+ )
289
+ return f(*args, **kwargs)
290
+
291
+ return apply_async # type: ignore
292
+
293
+
294
+ def _wrap_tracer(task, f):
295
+ # type: (Any, F) -> F
296
+
297
+ # Need to wrap tracer for pushing the scope before prerun is sent, and
298
+ # popping it after postrun is sent.
299
+ #
300
+ # This is the reason we don't use signals for hooking in the first place.
301
+ # Also because in Celery 3, signal dispatch returns early if one handler
302
+ # crashes.
303
+ @wraps(f)
304
+ @ensure_integration_enabled(CeleryIntegration, f)
305
+ def _inner(*args, **kwargs):
306
+ # type: (*Any, **Any) -> Any
307
+ with isolation_scope() as scope:
308
+ scope._name = "celery"
309
+ scope.clear_breadcrumbs()
310
+ scope.add_event_processor(_make_event_processor(task, *args, **kwargs))
311
+
312
+ transaction = None
313
+
314
+ # Celery task objects are not a thing to be trusted. Even
315
+ # something such as attribute access can fail.
316
+ with capture_internal_exceptions():
317
+ headers = args[3].get("headers") or {}
318
+ transaction = continue_trace(
319
+ headers,
320
+ op=OP.QUEUE_TASK_CELERY,
321
+ name="unknown celery task",
322
+ source=TransactionSource.TASK,
323
+ origin=CeleryIntegration.origin,
324
+ )
325
+ transaction.name = task.name
326
+ transaction.set_status(SPANSTATUS.OK)
327
+
328
+ if transaction is None:
329
+ return f(*args, **kwargs)
330
+
331
+ with sentry_sdk.start_transaction(
332
+ transaction,
333
+ custom_sampling_context={
334
+ "celery_job": {
335
+ "task": task.name,
336
+ # for some reason, args[1] is a list if non-empty but a
337
+ # tuple if empty
338
+ "args": list(args[1]),
339
+ "kwargs": args[2],
340
+ }
341
+ },
342
+ ):
343
+ return f(*args, **kwargs)
344
+
345
+ return _inner # type: ignore
346
+
347
+
348
+ def _set_messaging_destination_name(task, span):
349
+ # type: (Any, Span) -> None
350
+ """Set "messaging.destination.name" tag for span"""
351
+ with capture_internal_exceptions():
352
+ delivery_info = task.request.delivery_info
353
+ if delivery_info:
354
+ routing_key = delivery_info.get("routing_key")
355
+ if delivery_info.get("exchange") == "" and routing_key is not None:
356
+ # Empty exchange indicates the default exchange, meaning the tasks
357
+ # are sent to the queue with the same name as the routing key.
358
+ span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key)
359
+
360
+
361
+ def _wrap_task_call(task, f):
362
+ # type: (Any, F) -> F
363
+
364
+ # Need to wrap task call because the exception is caught before we get to
365
+ # see it. Also celery's reported stacktrace is untrustworthy.
366
+
367
+ # functools.wraps is important here because celery-once looks at this
368
+ # method's name. @ensure_integration_enabled internally calls functools.wraps,
369
+ # but if we ever remove the @ensure_integration_enabled decorator, we need
370
+ # to add @functools.wraps(f) here.
371
+ # https://github.com/getsentry/sentry-python/issues/421
372
+ @ensure_integration_enabled(CeleryIntegration, f)
373
+ def _inner(*args, **kwargs):
374
+ # type: (*Any, **Any) -> Any
375
+ try:
376
+ with sentry_sdk.start_span(
377
+ op=OP.QUEUE_PROCESS,
378
+ name=task.name,
379
+ origin=CeleryIntegration.origin,
380
+ ) as span:
381
+ _set_messaging_destination_name(task, span)
382
+
383
+ latency = None
384
+ with capture_internal_exceptions():
385
+ if (
386
+ task.request.headers is not None
387
+ and "sentry-task-enqueued-time" in task.request.headers
388
+ ):
389
+ latency = _now_seconds_since_epoch() - task.request.headers.pop(
390
+ "sentry-task-enqueued-time"
391
+ )
392
+
393
+ if latency is not None:
394
+ latency *= 1000 # milliseconds
395
+ span.set_data(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency)
396
+
397
+ with capture_internal_exceptions():
398
+ span.set_data(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id)
399
+
400
+ with capture_internal_exceptions():
401
+ span.set_data(
402
+ SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries
403
+ )
404
+
405
+ with capture_internal_exceptions():
406
+ span.set_data(
407
+ SPANDATA.MESSAGING_SYSTEM,
408
+ task.app.connection().transport.driver_type,
409
+ )
410
+
411
+ return f(*args, **kwargs)
412
+ except Exception:
413
+ exc_info = sys.exc_info()
414
+ with capture_internal_exceptions():
415
+ _capture_exception(task, exc_info)
416
+ reraise(*exc_info)
417
+
418
+ return _inner # type: ignore
419
+
420
+
421
+ def _patch_build_tracer():
422
+ # type: () -> None
423
+ import celery.app.trace as trace # type: ignore
424
+
425
+ original_build_tracer = trace.build_tracer
426
+
427
+ def sentry_build_tracer(name, task, *args, **kwargs):
428
+ # type: (Any, Any, *Any, **Any) -> Any
429
+ if not getattr(task, "_sentry_is_patched", False):
430
+ # determine whether Celery will use __call__ or run and patch
431
+ # accordingly
432
+ if task_has_custom(task, "__call__"):
433
+ type(task).__call__ = _wrap_task_call(task, type(task).__call__)
434
+ else:
435
+ task.run = _wrap_task_call(task, task.run)
436
+
437
+ # `build_tracer` is apparently called for every task
438
+ # invocation. Can't wrap every celery task for every invocation
439
+ # or we will get infinitely nested wrapper functions.
440
+ task._sentry_is_patched = True
441
+
442
+ return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs))
443
+
444
+ trace.build_tracer = sentry_build_tracer
445
+
446
+
447
+ def _patch_task_apply_async():
448
+ # type: () -> None
449
+ Task.apply_async = _wrap_task_run(Task.apply_async)
450
+
451
+
452
+ def _patch_celery_send_task():
453
+ # type: () -> None
454
+ from celery import Celery
455
+
456
+ Celery.send_task = _wrap_task_run(Celery.send_task)
457
+
458
+
459
+ def _patch_worker_exit():
460
+ # type: () -> None
461
+
462
+ # Need to flush queue before worker shutdown because a crashing worker will
463
+ # call os._exit
464
+ from billiard.pool import Worker # type: ignore
465
+
466
+ original_workloop = Worker.workloop
467
+
468
+ def sentry_workloop(*args, **kwargs):
469
+ # type: (*Any, **Any) -> Any
470
+ try:
471
+ return original_workloop(*args, **kwargs)
472
+ finally:
473
+ with capture_internal_exceptions():
474
+ if (
475
+ sentry_sdk.get_client().get_integration(CeleryIntegration)
476
+ is not None
477
+ ):
478
+ sentry_sdk.flush()
479
+
480
+ Worker.workloop = sentry_workloop
481
+
482
+
483
+ def _patch_producer_publish():
484
+ # type: () -> None
485
+ original_publish = Producer.publish
486
+
487
+ @ensure_integration_enabled(CeleryIntegration, original_publish)
488
+ def sentry_publish(self, *args, **kwargs):
489
+ # type: (Producer, *Any, **Any) -> Any
490
+ kwargs_headers = kwargs.get("headers", {})
491
+ if not isinstance(kwargs_headers, Mapping):
492
+ # Ensure kwargs_headers is a Mapping, so we can safely call get().
493
+ # We don't expect this to happen, but it's better to be safe. Even
494
+ # if it does happen, only our instrumentation breaks. This line
495
+ # does not overwrite kwargs["headers"], so the original publish
496
+ # method will still work.
497
+ kwargs_headers = {}
498
+
499
+ task_name = kwargs_headers.get("task")
500
+ task_id = kwargs_headers.get("id")
501
+ retries = kwargs_headers.get("retries")
502
+
503
+ routing_key = kwargs.get("routing_key")
504
+ exchange = kwargs.get("exchange")
505
+
506
+ with sentry_sdk.start_span(
507
+ op=OP.QUEUE_PUBLISH,
508
+ name=task_name,
509
+ origin=CeleryIntegration.origin,
510
+ ) as span:
511
+ if task_id is not None:
512
+ span.set_data(SPANDATA.MESSAGING_MESSAGE_ID, task_id)
513
+
514
+ if exchange == "" and routing_key is not None:
515
+ # Empty exchange indicates the default exchange, meaning messages are
516
+ # routed to the queue with the same name as the routing key.
517
+ span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key)
518
+
519
+ if retries is not None:
520
+ span.set_data(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries)
521
+
522
+ with capture_internal_exceptions():
523
+ span.set_data(
524
+ SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type
525
+ )
526
+
527
+ return original_publish(self, *args, **kwargs)
528
+
529
+ Producer.publish = sentry_publish