sentry-sdk 0.7.5__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.
- sentry_sdk/__init__.py +48 -30
- sentry_sdk/_compat.py +74 -61
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +289 -0
- sentry_sdk/_types.py +338 -0
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +134 -0
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +141 -0
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +112 -68
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +201 -119
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from sentry_sdk.crons import capture_checkin, MonitorStatus
|
|
3
|
+
from sentry_sdk.integrations import DidNotEnable
|
|
4
|
+
from sentry_sdk.integrations.celery.utils import (
|
|
5
|
+
_get_humanized_interval,
|
|
6
|
+
_now_seconds_since_epoch,
|
|
7
|
+
)
|
|
8
|
+
from sentry_sdk.utils import (
|
|
9
|
+
logger,
|
|
10
|
+
match_regex_list,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import Any, Optional, TypeVar, Union
|
|
18
|
+
from sentry_sdk._types import (
|
|
19
|
+
MonitorConfig,
|
|
20
|
+
MonitorConfigScheduleType,
|
|
21
|
+
MonitorConfigScheduleUnit,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from celery import Task, Celery # type: ignore
|
|
29
|
+
from celery.beat import Scheduler # type: ignore
|
|
30
|
+
from celery.schedules import crontab, schedule # type: ignore
|
|
31
|
+
from celery.signals import ( # type: ignore
|
|
32
|
+
task_failure,
|
|
33
|
+
task_success,
|
|
34
|
+
task_retry,
|
|
35
|
+
)
|
|
36
|
+
except ImportError:
|
|
37
|
+
raise DidNotEnable("Celery not installed")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from redbeat.schedulers import RedBeatScheduler # type: ignore
|
|
41
|
+
except ImportError:
|
|
42
|
+
RedBeatScheduler = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_headers(task):
|
|
46
|
+
# type: (Task) -> dict[str, Any]
|
|
47
|
+
headers = task.request.get("headers") or {}
|
|
48
|
+
|
|
49
|
+
# flatten nested headers
|
|
50
|
+
if "headers" in headers:
|
|
51
|
+
headers.update(headers["headers"])
|
|
52
|
+
del headers["headers"]
|
|
53
|
+
|
|
54
|
+
headers.update(task.request.get("properties") or {})
|
|
55
|
+
|
|
56
|
+
return headers
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_monitor_config(celery_schedule, app, monitor_name):
|
|
60
|
+
# type: (Any, Celery, str) -> MonitorConfig
|
|
61
|
+
monitor_config = {} # type: MonitorConfig
|
|
62
|
+
schedule_type = None # type: Optional[MonitorConfigScheduleType]
|
|
63
|
+
schedule_value = None # type: Optional[Union[str, int]]
|
|
64
|
+
schedule_unit = None # type: Optional[MonitorConfigScheduleUnit]
|
|
65
|
+
|
|
66
|
+
if isinstance(celery_schedule, crontab):
|
|
67
|
+
schedule_type = "crontab"
|
|
68
|
+
schedule_value = (
|
|
69
|
+
"{0._orig_minute} "
|
|
70
|
+
"{0._orig_hour} "
|
|
71
|
+
"{0._orig_day_of_month} "
|
|
72
|
+
"{0._orig_month_of_year} "
|
|
73
|
+
"{0._orig_day_of_week}".format(celery_schedule)
|
|
74
|
+
)
|
|
75
|
+
elif isinstance(celery_schedule, schedule):
|
|
76
|
+
schedule_type = "interval"
|
|
77
|
+
(schedule_value, schedule_unit) = _get_humanized_interval(
|
|
78
|
+
celery_schedule.seconds
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if schedule_unit == "second":
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.",
|
|
84
|
+
monitor_name,
|
|
85
|
+
schedule_value,
|
|
86
|
+
)
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
logger.warning(
|
|
91
|
+
"Celery schedule type '%s' not supported by Sentry Crons.",
|
|
92
|
+
type(celery_schedule),
|
|
93
|
+
)
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
monitor_config["schedule"] = {}
|
|
97
|
+
monitor_config["schedule"]["type"] = schedule_type
|
|
98
|
+
monitor_config["schedule"]["value"] = schedule_value
|
|
99
|
+
|
|
100
|
+
if schedule_unit is not None:
|
|
101
|
+
monitor_config["schedule"]["unit"] = schedule_unit
|
|
102
|
+
|
|
103
|
+
monitor_config["timezone"] = (
|
|
104
|
+
(
|
|
105
|
+
hasattr(celery_schedule, "tz")
|
|
106
|
+
and celery_schedule.tz is not None
|
|
107
|
+
and str(celery_schedule.tz)
|
|
108
|
+
)
|
|
109
|
+
or app.timezone
|
|
110
|
+
or "UTC"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return monitor_config
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration):
|
|
117
|
+
# type: (Any, Any, sentry_sdk.integrations.celery.CeleryIntegration) -> None
|
|
118
|
+
"""
|
|
119
|
+
Add Sentry Crons information to the schedule_entry headers.
|
|
120
|
+
"""
|
|
121
|
+
if not integration.monitor_beat_tasks:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
monitor_name = schedule_entry.name
|
|
125
|
+
|
|
126
|
+
task_should_be_excluded = match_regex_list(
|
|
127
|
+
monitor_name, integration.exclude_beat_tasks
|
|
128
|
+
)
|
|
129
|
+
if task_should_be_excluded:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
celery_schedule = schedule_entry.schedule
|
|
133
|
+
app = scheduler.app
|
|
134
|
+
|
|
135
|
+
monitor_config = _get_monitor_config(celery_schedule, app, monitor_name)
|
|
136
|
+
|
|
137
|
+
is_supported_schedule = bool(monitor_config)
|
|
138
|
+
if not is_supported_schedule:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
headers = schedule_entry.options.pop("headers", {})
|
|
142
|
+
headers.update(
|
|
143
|
+
{
|
|
144
|
+
"sentry-monitor-slug": monitor_name,
|
|
145
|
+
"sentry-monitor-config": monitor_config,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
check_in_id = capture_checkin(
|
|
150
|
+
monitor_slug=monitor_name,
|
|
151
|
+
monitor_config=monitor_config,
|
|
152
|
+
status=MonitorStatus.IN_PROGRESS,
|
|
153
|
+
)
|
|
154
|
+
headers.update({"sentry-monitor-check-in-id": check_in_id})
|
|
155
|
+
|
|
156
|
+
# Set the Sentry configuration in the options of the ScheduleEntry.
|
|
157
|
+
# Those will be picked up in `apply_async` and added to the headers.
|
|
158
|
+
schedule_entry.options["headers"] = headers
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _wrap_beat_scheduler(original_function):
|
|
162
|
+
# type: (Callable[..., Any]) -> Callable[..., Any]
|
|
163
|
+
"""
|
|
164
|
+
Makes sure that:
|
|
165
|
+
- a new Sentry trace is started for each task started by Celery Beat and
|
|
166
|
+
it is propagated to the task.
|
|
167
|
+
- the Sentry Crons information is set in the Celery Beat task's
|
|
168
|
+
headers so that is is monitored with Sentry Crons.
|
|
169
|
+
|
|
170
|
+
After the patched function is called,
|
|
171
|
+
Celery Beat will call apply_async to put the task in the queue.
|
|
172
|
+
"""
|
|
173
|
+
# Patch only once
|
|
174
|
+
# Can't use __name__ here, because some of our tests mock original_apply_entry
|
|
175
|
+
already_patched = "sentry_patched_scheduler" in str(original_function)
|
|
176
|
+
if already_patched:
|
|
177
|
+
return original_function
|
|
178
|
+
|
|
179
|
+
from sentry_sdk.integrations.celery import CeleryIntegration
|
|
180
|
+
|
|
181
|
+
def sentry_patched_scheduler(*args, **kwargs):
|
|
182
|
+
# type: (*Any, **Any) -> None
|
|
183
|
+
integration = sentry_sdk.get_client().get_integration(CeleryIntegration)
|
|
184
|
+
if integration is None:
|
|
185
|
+
return original_function(*args, **kwargs)
|
|
186
|
+
|
|
187
|
+
# Tasks started by Celery Beat start a new Trace
|
|
188
|
+
scope = sentry_sdk.get_isolation_scope()
|
|
189
|
+
scope.set_new_propagation_context()
|
|
190
|
+
scope._name = "celery-beat"
|
|
191
|
+
|
|
192
|
+
scheduler, schedule_entry = args
|
|
193
|
+
_apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration)
|
|
194
|
+
|
|
195
|
+
return original_function(*args, **kwargs)
|
|
196
|
+
|
|
197
|
+
return sentry_patched_scheduler
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _patch_beat_apply_entry():
|
|
201
|
+
# type: () -> None
|
|
202
|
+
Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _patch_redbeat_apply_async():
|
|
206
|
+
# type: () -> None
|
|
207
|
+
if RedBeatScheduler is None:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _setup_celery_beat_signals(monitor_beat_tasks):
|
|
214
|
+
# type: (bool) -> None
|
|
215
|
+
if monitor_beat_tasks:
|
|
216
|
+
task_success.connect(crons_task_success)
|
|
217
|
+
task_failure.connect(crons_task_failure)
|
|
218
|
+
task_retry.connect(crons_task_retry)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def crons_task_success(sender, **kwargs):
|
|
222
|
+
# type: (Task, dict[Any, Any]) -> None
|
|
223
|
+
logger.debug("celery_task_success %s", sender)
|
|
224
|
+
headers = _get_headers(sender)
|
|
225
|
+
|
|
226
|
+
if "sentry-monitor-slug" not in headers:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
monitor_config = headers.get("sentry-monitor-config", {})
|
|
230
|
+
|
|
231
|
+
start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s")
|
|
232
|
+
|
|
233
|
+
capture_checkin(
|
|
234
|
+
monitor_slug=headers["sentry-monitor-slug"],
|
|
235
|
+
monitor_config=monitor_config,
|
|
236
|
+
check_in_id=headers["sentry-monitor-check-in-id"],
|
|
237
|
+
duration=(
|
|
238
|
+
_now_seconds_since_epoch() - float(start_timestamp_s)
|
|
239
|
+
if start_timestamp_s
|
|
240
|
+
else None
|
|
241
|
+
),
|
|
242
|
+
status=MonitorStatus.OK,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def crons_task_failure(sender, **kwargs):
|
|
247
|
+
# type: (Task, dict[Any, Any]) -> None
|
|
248
|
+
logger.debug("celery_task_failure %s", sender)
|
|
249
|
+
headers = _get_headers(sender)
|
|
250
|
+
|
|
251
|
+
if "sentry-monitor-slug" not in headers:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
monitor_config = headers.get("sentry-monitor-config", {})
|
|
255
|
+
|
|
256
|
+
start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s")
|
|
257
|
+
|
|
258
|
+
capture_checkin(
|
|
259
|
+
monitor_slug=headers["sentry-monitor-slug"],
|
|
260
|
+
monitor_config=monitor_config,
|
|
261
|
+
check_in_id=headers["sentry-monitor-check-in-id"],
|
|
262
|
+
duration=(
|
|
263
|
+
_now_seconds_since_epoch() - float(start_timestamp_s)
|
|
264
|
+
if start_timestamp_s
|
|
265
|
+
else None
|
|
266
|
+
),
|
|
267
|
+
status=MonitorStatus.ERROR,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def crons_task_retry(sender, **kwargs):
|
|
272
|
+
# type: (Task, dict[Any, Any]) -> None
|
|
273
|
+
logger.debug("celery_task_retry %s", sender)
|
|
274
|
+
headers = _get_headers(sender)
|
|
275
|
+
|
|
276
|
+
if "sentry-monitor-slug" not in headers:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
monitor_config = headers.get("sentry-monitor-config", {})
|
|
280
|
+
|
|
281
|
+
start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s")
|
|
282
|
+
|
|
283
|
+
capture_checkin(
|
|
284
|
+
monitor_slug=headers["sentry-monitor-slug"],
|
|
285
|
+
monitor_config=monitor_config,
|
|
286
|
+
check_in_id=headers["sentry-monitor-check-in-id"],
|
|
287
|
+
duration=(
|
|
288
|
+
_now_seconds_since_epoch() - float(start_timestamp_s)
|
|
289
|
+
if start_timestamp_s
|
|
290
|
+
else None
|
|
291
|
+
),
|
|
292
|
+
status=MonitorStatus.ERROR,
|
|
293
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import TYPE_CHECKING, cast
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Any, Tuple
|
|
6
|
+
from sentry_sdk._types import MonitorConfigScheduleUnit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _now_seconds_since_epoch():
|
|
10
|
+
# type: () -> float
|
|
11
|
+
# We cannot use `time.perf_counter()` when dealing with the duration
|
|
12
|
+
# of a Celery task, because the start of a Celery task and
|
|
13
|
+
# the end are recorded in different processes.
|
|
14
|
+
# Start happens in the Celery Beat process,
|
|
15
|
+
# the end in a Celery Worker process.
|
|
16
|
+
return time.time()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_humanized_interval(seconds):
|
|
20
|
+
# type: (float) -> Tuple[int, MonitorConfigScheduleUnit]
|
|
21
|
+
TIME_UNITS = ( # noqa: N806
|
|
22
|
+
("day", 60 * 60 * 24.0),
|
|
23
|
+
("hour", 60 * 60.0),
|
|
24
|
+
("minute", 60.0),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
seconds = float(seconds)
|
|
28
|
+
for unit, divider in TIME_UNITS:
|
|
29
|
+
if seconds >= divider:
|
|
30
|
+
interval = int(seconds / divider)
|
|
31
|
+
return (interval, cast("MonitorConfigScheduleUnit", unit))
|
|
32
|
+
|
|
33
|
+
return (int(seconds), "second")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NoOpMgr:
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
# type: () -> None
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
42
|
+
# type: (Any, Any, Any) -> None
|
|
43
|
+
return None
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
import sentry_sdk
|
|
5
|
+
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
6
|
+
from sentry_sdk.integrations.aws_lambda import _make_request_event_processor
|
|
7
|
+
from sentry_sdk.tracing import TransactionSource
|
|
8
|
+
from sentry_sdk.utils import (
|
|
9
|
+
capture_internal_exceptions,
|
|
10
|
+
event_from_exception,
|
|
11
|
+
parse_version,
|
|
12
|
+
reraise,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import chalice # type: ignore
|
|
17
|
+
from chalice import __version__ as CHALICE_VERSION
|
|
18
|
+
from chalice import Chalice, ChaliceViewError
|
|
19
|
+
from chalice.app import EventSourceHandler as ChaliceEventSourceHandler # type: ignore
|
|
20
|
+
except ImportError:
|
|
21
|
+
raise DidNotEnable("Chalice is not installed")
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from typing import Any
|
|
27
|
+
from typing import Dict
|
|
28
|
+
from typing import TypeVar
|
|
29
|
+
from typing import Callable
|
|
30
|
+
|
|
31
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore
|
|
35
|
+
def __call__(self, event, context):
|
|
36
|
+
# type: (Any, Any) -> Any
|
|
37
|
+
client = sentry_sdk.get_client()
|
|
38
|
+
|
|
39
|
+
with sentry_sdk.isolation_scope() as scope:
|
|
40
|
+
with capture_internal_exceptions():
|
|
41
|
+
configured_time = context.get_remaining_time_in_millis()
|
|
42
|
+
scope.add_event_processor(
|
|
43
|
+
_make_request_event_processor(event, context, configured_time)
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
return ChaliceEventSourceHandler.__call__(self, event, context)
|
|
47
|
+
except Exception:
|
|
48
|
+
exc_info = sys.exc_info()
|
|
49
|
+
event, hint = event_from_exception(
|
|
50
|
+
exc_info,
|
|
51
|
+
client_options=client.options,
|
|
52
|
+
mechanism={"type": "chalice", "handled": False},
|
|
53
|
+
)
|
|
54
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
55
|
+
client.flush()
|
|
56
|
+
reraise(*exc_info)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_view_function_response(app, view_function, function_args):
|
|
60
|
+
# type: (Any, F, Any) -> F
|
|
61
|
+
@wraps(view_function)
|
|
62
|
+
def wrapped_view_function(**function_args):
|
|
63
|
+
# type: (**Any) -> Any
|
|
64
|
+
client = sentry_sdk.get_client()
|
|
65
|
+
with sentry_sdk.isolation_scope() as scope:
|
|
66
|
+
with capture_internal_exceptions():
|
|
67
|
+
configured_time = app.lambda_context.get_remaining_time_in_millis()
|
|
68
|
+
scope.set_transaction_name(
|
|
69
|
+
app.lambda_context.function_name,
|
|
70
|
+
source=TransactionSource.COMPONENT,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
scope.add_event_processor(
|
|
74
|
+
_make_request_event_processor(
|
|
75
|
+
app.current_request.to_dict(),
|
|
76
|
+
app.lambda_context,
|
|
77
|
+
configured_time,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
return view_function(**function_args)
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
if isinstance(exc, ChaliceViewError):
|
|
84
|
+
raise
|
|
85
|
+
exc_info = sys.exc_info()
|
|
86
|
+
event, hint = event_from_exception(
|
|
87
|
+
exc_info,
|
|
88
|
+
client_options=client.options,
|
|
89
|
+
mechanism={"type": "chalice", "handled": False},
|
|
90
|
+
)
|
|
91
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
92
|
+
client.flush()
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
return wrapped_view_function # type: ignore
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ChaliceIntegration(Integration):
|
|
99
|
+
identifier = "chalice"
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def setup_once():
|
|
103
|
+
# type: () -> None
|
|
104
|
+
|
|
105
|
+
version = parse_version(CHALICE_VERSION)
|
|
106
|
+
|
|
107
|
+
if version is None:
|
|
108
|
+
raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION))
|
|
109
|
+
|
|
110
|
+
if version < (1, 20):
|
|
111
|
+
old_get_view_function_response = Chalice._get_view_function_response
|
|
112
|
+
else:
|
|
113
|
+
from chalice.app import RestAPIEventHandler
|
|
114
|
+
|
|
115
|
+
old_get_view_function_response = (
|
|
116
|
+
RestAPIEventHandler._get_view_function_response
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def sentry_event_response(app, view_function, function_args):
|
|
120
|
+
# type: (Any, F, Dict[str, Any]) -> Any
|
|
121
|
+
wrapped_view_function = _get_view_function_response(
|
|
122
|
+
app, view_function, function_args
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return old_get_view_function_response(
|
|
126
|
+
app, wrapped_view_function, function_args
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if version < (1, 20):
|
|
130
|
+
Chalice._get_view_function_response = sentry_event_response
|
|
131
|
+
else:
|
|
132
|
+
RestAPIEventHandler._get_view_function_response = sentry_event_response
|
|
133
|
+
# for everything else (like events)
|
|
134
|
+
chalice.app.EventSourceHandler = EventSourceHandler
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from sentry_sdk.consts import OP, SPANDATA
|
|
3
|
+
from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
|
|
4
|
+
from sentry_sdk.tracing import Span
|
|
5
|
+
from sentry_sdk.scope import should_send_default_pii
|
|
6
|
+
from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
9
|
+
|
|
10
|
+
# Hack to get new Python features working in older versions
|
|
11
|
+
# without introducing a hard dependency on `typing_extensions`
|
|
12
|
+
# from: https://stackoverflow.com/a/71944042/300572
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from typing import Any, ParamSpec, Callable
|
|
16
|
+
else:
|
|
17
|
+
# Fake ParamSpec
|
|
18
|
+
class ParamSpec:
|
|
19
|
+
def __init__(self, _):
|
|
20
|
+
self.args = None
|
|
21
|
+
self.kwargs = None
|
|
22
|
+
|
|
23
|
+
# Callable[anything] will return None
|
|
24
|
+
class _Callable:
|
|
25
|
+
def __getitem__(self, _):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
# Make instances
|
|
29
|
+
Callable = _Callable()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import clickhouse_driver # type: ignore[import-not-found]
|
|
34
|
+
|
|
35
|
+
except ImportError:
|
|
36
|
+
raise DidNotEnable("clickhouse-driver not installed.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ClickhouseDriverIntegration(Integration):
|
|
40
|
+
identifier = "clickhouse_driver"
|
|
41
|
+
origin = f"auto.db.{identifier}"
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def setup_once() -> None:
|
|
45
|
+
_check_minimum_version(ClickhouseDriverIntegration, clickhouse_driver.VERSION)
|
|
46
|
+
|
|
47
|
+
# Every query is done using the Connection's `send_query` function
|
|
48
|
+
clickhouse_driver.connection.Connection.send_query = _wrap_start(
|
|
49
|
+
clickhouse_driver.connection.Connection.send_query
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# If the query contains parameters then the send_data function is used to send those parameters to clickhouse
|
|
53
|
+
_wrap_send_data()
|
|
54
|
+
|
|
55
|
+
# Every query ends either with the Client's `receive_end_of_query` (no result expected)
|
|
56
|
+
# or its `receive_result` (result expected)
|
|
57
|
+
clickhouse_driver.client.Client.receive_end_of_query = _wrap_end(
|
|
58
|
+
clickhouse_driver.client.Client.receive_end_of_query
|
|
59
|
+
)
|
|
60
|
+
if hasattr(clickhouse_driver.client.Client, "receive_end_of_insert_query"):
|
|
61
|
+
# In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query`
|
|
62
|
+
clickhouse_driver.client.Client.receive_end_of_insert_query = _wrap_end(
|
|
63
|
+
clickhouse_driver.client.Client.receive_end_of_insert_query
|
|
64
|
+
)
|
|
65
|
+
clickhouse_driver.client.Client.receive_result = _wrap_end(
|
|
66
|
+
clickhouse_driver.client.Client.receive_result
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
P = ParamSpec("P")
|
|
71
|
+
T = TypeVar("T")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _wrap_start(f: Callable[P, T]) -> Callable[P, T]:
|
|
75
|
+
@ensure_integration_enabled(ClickhouseDriverIntegration, f)
|
|
76
|
+
def _inner(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
77
|
+
connection = args[0]
|
|
78
|
+
query = args[1]
|
|
79
|
+
query_id = args[2] if len(args) > 2 else kwargs.get("query_id")
|
|
80
|
+
params = args[3] if len(args) > 3 else kwargs.get("params")
|
|
81
|
+
|
|
82
|
+
span = sentry_sdk.start_span(
|
|
83
|
+
op=OP.DB,
|
|
84
|
+
name=query,
|
|
85
|
+
origin=ClickhouseDriverIntegration.origin,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
connection._sentry_span = span # type: ignore[attr-defined]
|
|
89
|
+
|
|
90
|
+
_set_db_data(span, connection)
|
|
91
|
+
|
|
92
|
+
span.set_data("query", query)
|
|
93
|
+
|
|
94
|
+
if query_id:
|
|
95
|
+
span.set_data("db.query_id", query_id)
|
|
96
|
+
|
|
97
|
+
if params and should_send_default_pii():
|
|
98
|
+
span.set_data("db.params", params)
|
|
99
|
+
|
|
100
|
+
# run the original code
|
|
101
|
+
ret = f(*args, **kwargs)
|
|
102
|
+
|
|
103
|
+
return ret
|
|
104
|
+
|
|
105
|
+
return _inner
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _wrap_end(f: Callable[P, T]) -> Callable[P, T]:
|
|
109
|
+
def _inner_end(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
110
|
+
res = f(*args, **kwargs)
|
|
111
|
+
instance = args[0]
|
|
112
|
+
span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined]
|
|
113
|
+
|
|
114
|
+
if span is not None:
|
|
115
|
+
if res is not None and should_send_default_pii():
|
|
116
|
+
span.set_data("db.result", res)
|
|
117
|
+
|
|
118
|
+
with capture_internal_exceptions():
|
|
119
|
+
span.scope.add_breadcrumb(
|
|
120
|
+
message=span._data.pop("query"), category="query", data=span._data
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
span.finish()
|
|
124
|
+
|
|
125
|
+
return res
|
|
126
|
+
|
|
127
|
+
return _inner_end
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _wrap_send_data() -> None:
|
|
131
|
+
original_send_data = clickhouse_driver.client.Client.send_data
|
|
132
|
+
|
|
133
|
+
def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data
|
|
134
|
+
self, sample_block, data, types_check=False, columnar=False, *args, **kwargs
|
|
135
|
+
):
|
|
136
|
+
span = getattr(self.connection, "_sentry_span", None)
|
|
137
|
+
|
|
138
|
+
if span is not None:
|
|
139
|
+
_set_db_data(span, self.connection)
|
|
140
|
+
|
|
141
|
+
if should_send_default_pii():
|
|
142
|
+
db_params = span._data.get("db.params", [])
|
|
143
|
+
|
|
144
|
+
if isinstance(data, (list, tuple)):
|
|
145
|
+
db_params.extend(data)
|
|
146
|
+
|
|
147
|
+
else: # data is a generic iterator
|
|
148
|
+
orig_data = data
|
|
149
|
+
|
|
150
|
+
# Wrap the generator to add items to db.params as they are yielded.
|
|
151
|
+
# This allows us to send the params to Sentry without needing to allocate
|
|
152
|
+
# memory for the entire generator at once.
|
|
153
|
+
def wrapped_generator() -> "Iterator[Any]":
|
|
154
|
+
for item in orig_data:
|
|
155
|
+
db_params.append(item)
|
|
156
|
+
yield item
|
|
157
|
+
|
|
158
|
+
# Replace the original iterator with the wrapped one.
|
|
159
|
+
data = wrapped_generator()
|
|
160
|
+
|
|
161
|
+
span.set_data("db.params", db_params)
|
|
162
|
+
|
|
163
|
+
return original_send_data(
|
|
164
|
+
self, sample_block, data, types_check, columnar, *args, **kwargs
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
clickhouse_driver.client.Client.send_data = _inner_send_data
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _set_db_data(
|
|
171
|
+
span: Span, connection: clickhouse_driver.connection.Connection
|
|
172
|
+
) -> None:
|
|
173
|
+
span.set_data(SPANDATA.DB_SYSTEM, "clickhouse")
|
|
174
|
+
span.set_data(SPANDATA.SERVER_ADDRESS, connection.host)
|
|
175
|
+
span.set_data(SPANDATA.SERVER_PORT, connection.port)
|
|
176
|
+
span.set_data(SPANDATA.DB_NAME, connection.database)
|
|
177
|
+
span.set_data(SPANDATA.DB_USER, connection.user)
|