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.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- 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 +81 -19
- sentry_sdk/_types.py +311 -11
- 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 +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- 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 +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- 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 +35 -28
- 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 +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- 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 +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- 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 +20 -11
- 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 +71 -60
- 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 +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- 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 +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- 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 +126 -125
- 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/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.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/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
from __future__ import absolute_import
|
|
2
|
-
|
|
3
1
|
import logging
|
|
4
|
-
import
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from fnmatch import fnmatch
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import sentry_sdk
|
|
7
|
+
from sentry_sdk.client import BaseClient
|
|
8
|
+
from sentry_sdk.logger import _log_level_to_otel
|
|
7
9
|
from sentry_sdk.utils import (
|
|
10
|
+
safe_repr,
|
|
8
11
|
to_string,
|
|
9
12
|
event_from_exception,
|
|
10
13
|
current_stacktrace,
|
|
11
14
|
capture_internal_exceptions,
|
|
15
|
+
has_logs_enabled,
|
|
12
16
|
)
|
|
13
17
|
from sentry_sdk.integrations import Integration
|
|
14
|
-
from sentry_sdk._compat import iteritems
|
|
15
18
|
|
|
16
|
-
from
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
17
20
|
|
|
18
|
-
if
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import MutableMapping
|
|
19
23
|
from logging import LogRecord
|
|
20
24
|
from typing import Any
|
|
21
25
|
from typing import Dict
|
|
@@ -23,6 +27,26 @@ if MYPY:
|
|
|
23
27
|
|
|
24
28
|
DEFAULT_LEVEL = logging.INFO
|
|
25
29
|
DEFAULT_EVENT_LEVEL = logging.ERROR
|
|
30
|
+
LOGGING_TO_EVENT_LEVEL = {
|
|
31
|
+
logging.NOTSET: "notset",
|
|
32
|
+
logging.DEBUG: "debug",
|
|
33
|
+
logging.INFO: "info",
|
|
34
|
+
logging.WARN: "warning", # WARN is same a WARNING
|
|
35
|
+
logging.WARNING: "warning",
|
|
36
|
+
logging.ERROR: "error",
|
|
37
|
+
logging.FATAL: "fatal",
|
|
38
|
+
logging.CRITICAL: "fatal", # CRITICAL is same as FATAL
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Map logging level numbers to corresponding OTel level numbers
|
|
42
|
+
SEVERITY_TO_OTEL_SEVERITY = {
|
|
43
|
+
logging.CRITICAL: 21, # fatal
|
|
44
|
+
logging.ERROR: 17, # error
|
|
45
|
+
logging.WARNING: 13, # warn
|
|
46
|
+
logging.INFO: 9, # info
|
|
47
|
+
logging.DEBUG: 5, # debug
|
|
48
|
+
}
|
|
49
|
+
|
|
26
50
|
|
|
27
51
|
# Capturing events from those loggers causes recursion errors. We cannot allow
|
|
28
52
|
# the user to unconditionally create events from those loggers under any
|
|
@@ -52,14 +76,23 @@ def ignore_logger(
|
|
|
52
76
|
class LoggingIntegration(Integration):
|
|
53
77
|
identifier = "logging"
|
|
54
78
|
|
|
55
|
-
def __init__(
|
|
56
|
-
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
level=DEFAULT_LEVEL,
|
|
82
|
+
event_level=DEFAULT_EVENT_LEVEL,
|
|
83
|
+
sentry_logs_level=DEFAULT_LEVEL,
|
|
84
|
+
):
|
|
85
|
+
# type: (Optional[int], Optional[int], Optional[int]) -> None
|
|
57
86
|
self._handler = None
|
|
58
87
|
self._breadcrumb_handler = None
|
|
88
|
+
self._sentry_logs_handler = None
|
|
59
89
|
|
|
60
90
|
if level is not None:
|
|
61
91
|
self._breadcrumb_handler = BreadcrumbHandler(level=level)
|
|
62
92
|
|
|
93
|
+
if sentry_logs_level is not None:
|
|
94
|
+
self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level)
|
|
95
|
+
|
|
63
96
|
if event_level is not None:
|
|
64
97
|
self._handler = EventHandler(level=event_level)
|
|
65
98
|
|
|
@@ -74,13 +107,23 @@ class LoggingIntegration(Integration):
|
|
|
74
107
|
):
|
|
75
108
|
self._breadcrumb_handler.handle(record)
|
|
76
109
|
|
|
110
|
+
if (
|
|
111
|
+
self._sentry_logs_handler is not None
|
|
112
|
+
and record.levelno >= self._sentry_logs_handler.level
|
|
113
|
+
):
|
|
114
|
+
self._sentry_logs_handler.handle(record)
|
|
115
|
+
|
|
77
116
|
@staticmethod
|
|
78
117
|
def setup_once():
|
|
79
118
|
# type: () -> None
|
|
80
|
-
old_callhandlers = logging.Logger.callHandlers
|
|
119
|
+
old_callhandlers = logging.Logger.callHandlers
|
|
81
120
|
|
|
82
121
|
def sentry_patched_callhandlers(self, record):
|
|
83
122
|
# type: (Any, LogRecord) -> Any
|
|
123
|
+
# keeping a local reference because the
|
|
124
|
+
# global might be discarded on shutdown
|
|
125
|
+
ignored_loggers = _IGNORED_LOGGERS
|
|
126
|
+
|
|
84
127
|
try:
|
|
85
128
|
return old_callhandlers(self, record)
|
|
86
129
|
finally:
|
|
@@ -88,77 +131,75 @@ class LoggingIntegration(Integration):
|
|
|
88
131
|
# the integration. Otherwise we have a high chance of getting
|
|
89
132
|
# into a recursion error when the integration is resolved
|
|
90
133
|
# (this also is slower).
|
|
91
|
-
if
|
|
92
|
-
|
|
134
|
+
if (
|
|
135
|
+
ignored_loggers is not None
|
|
136
|
+
and record.name.strip() not in ignored_loggers
|
|
137
|
+
):
|
|
138
|
+
integration = sentry_sdk.get_client().get_integration(
|
|
139
|
+
LoggingIntegration
|
|
140
|
+
)
|
|
93
141
|
if integration is not None:
|
|
94
142
|
integration._handle_record(record)
|
|
95
143
|
|
|
96
144
|
logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore
|
|
97
145
|
|
|
98
146
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"funcName",
|
|
129
|
-
"levelname",
|
|
130
|
-
"levelno",
|
|
131
|
-
"linenno",
|
|
132
|
-
"lineno",
|
|
133
|
-
"message",
|
|
134
|
-
"module",
|
|
135
|
-
"msecs",
|
|
136
|
-
"msg",
|
|
137
|
-
"name",
|
|
138
|
-
"pathname",
|
|
139
|
-
"process",
|
|
140
|
-
"processName",
|
|
141
|
-
"relativeCreated",
|
|
142
|
-
"stack",
|
|
143
|
-
"tags",
|
|
144
|
-
"thread",
|
|
145
|
-
"threadName",
|
|
146
|
-
"stack_info",
|
|
147
|
+
class _BaseHandler(logging.Handler):
|
|
148
|
+
COMMON_RECORD_ATTRS = frozenset(
|
|
149
|
+
(
|
|
150
|
+
"args",
|
|
151
|
+
"created",
|
|
152
|
+
"exc_info",
|
|
153
|
+
"exc_text",
|
|
154
|
+
"filename",
|
|
155
|
+
"funcName",
|
|
156
|
+
"levelname",
|
|
157
|
+
"levelno",
|
|
158
|
+
"linenno",
|
|
159
|
+
"lineno",
|
|
160
|
+
"message",
|
|
161
|
+
"module",
|
|
162
|
+
"msecs",
|
|
163
|
+
"msg",
|
|
164
|
+
"name",
|
|
165
|
+
"pathname",
|
|
166
|
+
"process",
|
|
167
|
+
"processName",
|
|
168
|
+
"relativeCreated",
|
|
169
|
+
"stack",
|
|
170
|
+
"tags",
|
|
171
|
+
"taskName",
|
|
172
|
+
"thread",
|
|
173
|
+
"threadName",
|
|
174
|
+
"stack_info",
|
|
175
|
+
)
|
|
147
176
|
)
|
|
148
|
-
)
|
|
149
177
|
|
|
178
|
+
def _can_record(self, record):
|
|
179
|
+
# type: (LogRecord) -> bool
|
|
180
|
+
"""Prevents ignored loggers from recording"""
|
|
181
|
+
for logger in _IGNORED_LOGGERS:
|
|
182
|
+
if fnmatch(record.name.strip(), logger):
|
|
183
|
+
return False
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
def _logging_to_event_level(self, record):
|
|
187
|
+
# type: (LogRecord) -> str
|
|
188
|
+
return LOGGING_TO_EVENT_LEVEL.get(
|
|
189
|
+
record.levelno, record.levelname.lower() if record.levelname else ""
|
|
190
|
+
)
|
|
150
191
|
|
|
151
|
-
def _extra_from_record(record):
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
192
|
+
def _extra_from_record(self, record):
|
|
193
|
+
# type: (LogRecord) -> MutableMapping[str, object]
|
|
194
|
+
return {
|
|
195
|
+
k: v
|
|
196
|
+
for k, v in vars(record).items()
|
|
197
|
+
if k not in self.COMMON_RECORD_ATTRS
|
|
198
|
+
and (not isinstance(k, str) or not k.startswith("_"))
|
|
199
|
+
}
|
|
159
200
|
|
|
160
201
|
|
|
161
|
-
class EventHandler(
|
|
202
|
+
class EventHandler(_BaseHandler):
|
|
162
203
|
"""
|
|
163
204
|
A logging handler that emits Sentry events for each log record
|
|
164
205
|
|
|
@@ -173,23 +214,28 @@ class EventHandler(logging.Handler, object):
|
|
|
173
214
|
|
|
174
215
|
def _emit(self, record):
|
|
175
216
|
# type: (LogRecord) -> None
|
|
176
|
-
if not _can_record(record):
|
|
217
|
+
if not self._can_record(record):
|
|
177
218
|
return
|
|
178
219
|
|
|
179
|
-
|
|
180
|
-
if
|
|
220
|
+
client = sentry_sdk.get_client()
|
|
221
|
+
if not client.is_active():
|
|
181
222
|
return
|
|
182
223
|
|
|
183
|
-
client_options =
|
|
224
|
+
client_options = client.options
|
|
184
225
|
|
|
185
226
|
# exc_info might be None or (None, None, None)
|
|
186
|
-
|
|
227
|
+
#
|
|
228
|
+
# exc_info may also be any falsy value due to Python stdlib being
|
|
229
|
+
# liberal with what it receives and Celery's billiard being "liberal"
|
|
230
|
+
# with what it sends. See
|
|
231
|
+
# https://github.com/getsentry/sentry-python/issues/904
|
|
232
|
+
if record.exc_info and record.exc_info[0] is not None:
|
|
187
233
|
event, hint = event_from_exception(
|
|
188
234
|
record.exc_info,
|
|
189
235
|
client_options=client_options,
|
|
190
236
|
mechanism={"type": "logging", "handled": True},
|
|
191
237
|
)
|
|
192
|
-
elif record.exc_info and record.exc_info[0] is None:
|
|
238
|
+
elif (record.exc_info and record.exc_info[0] is None) or record.stack_info:
|
|
193
239
|
event = {}
|
|
194
240
|
hint = {}
|
|
195
241
|
with capture_internal_exceptions():
|
|
@@ -197,7 +243,10 @@ class EventHandler(logging.Handler, object):
|
|
|
197
243
|
"values": [
|
|
198
244
|
{
|
|
199
245
|
"stacktrace": current_stacktrace(
|
|
200
|
-
client_options[
|
|
246
|
+
include_local_variables=client_options[
|
|
247
|
+
"include_local_variables"
|
|
248
|
+
],
|
|
249
|
+
max_value_length=client_options["max_value_length"],
|
|
201
250
|
),
|
|
202
251
|
"crashed": False,
|
|
203
252
|
"current": True,
|
|
@@ -210,19 +259,41 @@ class EventHandler(logging.Handler, object):
|
|
|
210
259
|
|
|
211
260
|
hint["log_record"] = record
|
|
212
261
|
|
|
213
|
-
|
|
262
|
+
level = self._logging_to_event_level(record)
|
|
263
|
+
if level in {"debug", "info", "warning", "error", "critical", "fatal"}:
|
|
264
|
+
event["level"] = level # type: ignore[typeddict-item]
|
|
214
265
|
event["logger"] = record.name
|
|
215
|
-
event["logentry"] = {"message": to_string(record.msg), "params": record.args}
|
|
216
|
-
event["extra"] = _extra_from_record(record)
|
|
217
266
|
|
|
218
|
-
|
|
267
|
+
if (
|
|
268
|
+
sys.version_info < (3, 11)
|
|
269
|
+
and record.name == "py.warnings"
|
|
270
|
+
and record.msg == "%s"
|
|
271
|
+
):
|
|
272
|
+
# warnings module on Python 3.10 and below sets record.msg to "%s"
|
|
273
|
+
# and record.args[0] to the actual warning message.
|
|
274
|
+
# This was fixed in https://github.com/python/cpython/pull/30975.
|
|
275
|
+
message = record.args[0]
|
|
276
|
+
params = ()
|
|
277
|
+
else:
|
|
278
|
+
message = record.msg
|
|
279
|
+
params = record.args
|
|
280
|
+
|
|
281
|
+
event["logentry"] = {
|
|
282
|
+
"message": to_string(message),
|
|
283
|
+
"formatted": record.getMessage(),
|
|
284
|
+
"params": params,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
event["extra"] = self._extra_from_record(record)
|
|
288
|
+
|
|
289
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
219
290
|
|
|
220
291
|
|
|
221
292
|
# Legacy name
|
|
222
293
|
SentryHandler = EventHandler
|
|
223
294
|
|
|
224
295
|
|
|
225
|
-
class BreadcrumbHandler(
|
|
296
|
+
class BreadcrumbHandler(_BaseHandler):
|
|
226
297
|
"""
|
|
227
298
|
A logging handler that records breadcrumbs for each log record.
|
|
228
299
|
|
|
@@ -237,9 +308,114 @@ class BreadcrumbHandler(logging.Handler, object):
|
|
|
237
308
|
|
|
238
309
|
def _emit(self, record):
|
|
239
310
|
# type: (LogRecord) -> None
|
|
240
|
-
if not _can_record(record):
|
|
311
|
+
if not self._can_record(record):
|
|
241
312
|
return
|
|
242
313
|
|
|
243
|
-
|
|
244
|
-
_breadcrumb_from_record(record), hint={"log_record": record}
|
|
314
|
+
sentry_sdk.add_breadcrumb(
|
|
315
|
+
self._breadcrumb_from_record(record), hint={"log_record": record}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def _breadcrumb_from_record(self, record):
|
|
319
|
+
# type: (LogRecord) -> Dict[str, Any]
|
|
320
|
+
return {
|
|
321
|
+
"type": "log",
|
|
322
|
+
"level": self._logging_to_event_level(record),
|
|
323
|
+
"category": record.name,
|
|
324
|
+
"message": record.message,
|
|
325
|
+
"timestamp": datetime.fromtimestamp(record.created, timezone.utc),
|
|
326
|
+
"data": self._extra_from_record(record),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class SentryLogsHandler(_BaseHandler):
|
|
331
|
+
"""
|
|
332
|
+
A logging handler that records Sentry logs for each Python log record.
|
|
333
|
+
|
|
334
|
+
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def emit(self, record):
|
|
338
|
+
# type: (LogRecord) -> Any
|
|
339
|
+
with capture_internal_exceptions():
|
|
340
|
+
self.format(record)
|
|
341
|
+
if not self._can_record(record):
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
client = sentry_sdk.get_client()
|
|
345
|
+
if not client.is_active():
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
if not has_logs_enabled(client.options):
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
self._capture_log_from_record(client, record)
|
|
352
|
+
|
|
353
|
+
def _capture_log_from_record(self, client, record):
|
|
354
|
+
# type: (BaseClient, LogRecord) -> None
|
|
355
|
+
otel_severity_number, otel_severity_text = _log_level_to_otel(
|
|
356
|
+
record.levelno, SEVERITY_TO_OTEL_SEVERITY
|
|
357
|
+
)
|
|
358
|
+
project_root = client.options["project_root"]
|
|
359
|
+
|
|
360
|
+
attrs = self._extra_from_record(record) # type: Any
|
|
361
|
+
attrs["sentry.origin"] = "auto.log.stdlib"
|
|
362
|
+
|
|
363
|
+
parameters_set = False
|
|
364
|
+
if record.args is not None:
|
|
365
|
+
if isinstance(record.args, tuple):
|
|
366
|
+
parameters_set = bool(record.args)
|
|
367
|
+
for i, arg in enumerate(record.args):
|
|
368
|
+
attrs[f"sentry.message.parameter.{i}"] = (
|
|
369
|
+
arg
|
|
370
|
+
if isinstance(arg, (str, float, int, bool))
|
|
371
|
+
else safe_repr(arg)
|
|
372
|
+
)
|
|
373
|
+
elif isinstance(record.args, dict):
|
|
374
|
+
parameters_set = bool(record.args)
|
|
375
|
+
for key, value in record.args.items():
|
|
376
|
+
attrs[f"sentry.message.parameter.{key}"] = (
|
|
377
|
+
value
|
|
378
|
+
if isinstance(value, (str, float, int, bool))
|
|
379
|
+
else safe_repr(value)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if parameters_set and isinstance(record.msg, str):
|
|
383
|
+
# only include template if there is at least one
|
|
384
|
+
# sentry.message.parameter.X set
|
|
385
|
+
attrs["sentry.message.template"] = record.msg
|
|
386
|
+
|
|
387
|
+
if record.lineno:
|
|
388
|
+
attrs["code.line.number"] = record.lineno
|
|
389
|
+
|
|
390
|
+
if record.pathname:
|
|
391
|
+
if project_root is not None and record.pathname.startswith(project_root):
|
|
392
|
+
attrs["code.file.path"] = record.pathname[len(project_root) + 1 :]
|
|
393
|
+
else:
|
|
394
|
+
attrs["code.file.path"] = record.pathname
|
|
395
|
+
|
|
396
|
+
if record.funcName:
|
|
397
|
+
attrs["code.function.name"] = record.funcName
|
|
398
|
+
|
|
399
|
+
if record.thread:
|
|
400
|
+
attrs["thread.id"] = record.thread
|
|
401
|
+
if record.threadName:
|
|
402
|
+
attrs["thread.name"] = record.threadName
|
|
403
|
+
|
|
404
|
+
if record.process:
|
|
405
|
+
attrs["process.pid"] = record.process
|
|
406
|
+
if record.processName:
|
|
407
|
+
attrs["process.executable.name"] = record.processName
|
|
408
|
+
if record.name:
|
|
409
|
+
attrs["logger.name"] = record.name
|
|
410
|
+
|
|
411
|
+
# noinspection PyProtectedMember
|
|
412
|
+
client._capture_log(
|
|
413
|
+
{
|
|
414
|
+
"severity_text": otel_severity_text,
|
|
415
|
+
"severity_number": otel_severity_number,
|
|
416
|
+
"body": record.message,
|
|
417
|
+
"attributes": attrs,
|
|
418
|
+
"time_unix_nano": int(record.created * 1e9),
|
|
419
|
+
"trace_id": None,
|
|
420
|
+
},
|
|
245
421
|
)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
5
|
+
from sentry_sdk.integrations.logging import (
|
|
6
|
+
BreadcrumbHandler,
|
|
7
|
+
EventHandler,
|
|
8
|
+
_BaseHandler,
|
|
9
|
+
)
|
|
10
|
+
from sentry_sdk.logger import _log_level_to_otel
|
|
11
|
+
from sentry_sdk.utils import has_logs_enabled, safe_repr
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from logging import LogRecord
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import loguru
|
|
21
|
+
from loguru import logger
|
|
22
|
+
from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from loguru import Message
|
|
26
|
+
except ImportError:
|
|
27
|
+
raise DidNotEnable("LOGURU is not installed")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LoggingLevels(enum.IntEnum):
|
|
31
|
+
TRACE = 5
|
|
32
|
+
DEBUG = 10
|
|
33
|
+
INFO = 20
|
|
34
|
+
SUCCESS = 25
|
|
35
|
+
WARNING = 30
|
|
36
|
+
ERROR = 40
|
|
37
|
+
CRITICAL = 50
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DEFAULT_LEVEL = LoggingLevels.INFO.value
|
|
41
|
+
DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
|
|
45
|
+
"TRACE": "DEBUG",
|
|
46
|
+
"DEBUG": "DEBUG",
|
|
47
|
+
"INFO": "INFO",
|
|
48
|
+
"SUCCESS": "INFO",
|
|
49
|
+
"WARNING": "WARNING",
|
|
50
|
+
"ERROR": "ERROR",
|
|
51
|
+
"CRITICAL": "CRITICAL",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Map Loguru level numbers to corresponding OTel level numbers
|
|
55
|
+
SEVERITY_TO_OTEL_SEVERITY = {
|
|
56
|
+
LoggingLevels.CRITICAL: 21, # fatal
|
|
57
|
+
LoggingLevels.ERROR: 17, # error
|
|
58
|
+
LoggingLevels.WARNING: 13, # warn
|
|
59
|
+
LoggingLevels.SUCCESS: 11, # info
|
|
60
|
+
LoggingLevels.INFO: 9, # info
|
|
61
|
+
LoggingLevels.DEBUG: 5, # debug
|
|
62
|
+
LoggingLevels.TRACE: 1, # trace
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class LoguruIntegration(Integration):
|
|
67
|
+
identifier = "loguru"
|
|
68
|
+
|
|
69
|
+
level = DEFAULT_LEVEL # type: Optional[int]
|
|
70
|
+
event_level = DEFAULT_EVENT_LEVEL # type: Optional[int]
|
|
71
|
+
breadcrumb_format = DEFAULT_FORMAT
|
|
72
|
+
event_format = DEFAULT_FORMAT
|
|
73
|
+
sentry_logs_level = DEFAULT_LEVEL # type: Optional[int]
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
level=DEFAULT_LEVEL,
|
|
78
|
+
event_level=DEFAULT_EVENT_LEVEL,
|
|
79
|
+
breadcrumb_format=DEFAULT_FORMAT,
|
|
80
|
+
event_format=DEFAULT_FORMAT,
|
|
81
|
+
sentry_logs_level=DEFAULT_LEVEL,
|
|
82
|
+
):
|
|
83
|
+
# type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[int]) -> None
|
|
84
|
+
LoguruIntegration.level = level
|
|
85
|
+
LoguruIntegration.event_level = event_level
|
|
86
|
+
LoguruIntegration.breadcrumb_format = breadcrumb_format
|
|
87
|
+
LoguruIntegration.event_format = event_format
|
|
88
|
+
LoguruIntegration.sentry_logs_level = sentry_logs_level
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def setup_once():
|
|
92
|
+
# type: () -> None
|
|
93
|
+
if LoguruIntegration.level is not None:
|
|
94
|
+
logger.add(
|
|
95
|
+
LoguruBreadcrumbHandler(level=LoguruIntegration.level),
|
|
96
|
+
level=LoguruIntegration.level,
|
|
97
|
+
format=LoguruIntegration.breadcrumb_format,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if LoguruIntegration.event_level is not None:
|
|
101
|
+
logger.add(
|
|
102
|
+
LoguruEventHandler(level=LoguruIntegration.event_level),
|
|
103
|
+
level=LoguruIntegration.event_level,
|
|
104
|
+
format=LoguruIntegration.event_format,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if LoguruIntegration.sentry_logs_level is not None:
|
|
108
|
+
logger.add(
|
|
109
|
+
loguru_sentry_logs_handler,
|
|
110
|
+
level=LoguruIntegration.sentry_logs_level,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _LoguruBaseHandler(_BaseHandler):
|
|
115
|
+
def __init__(self, *args, **kwargs):
|
|
116
|
+
# type: (*Any, **Any) -> None
|
|
117
|
+
if kwargs.get("level"):
|
|
118
|
+
kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
|
|
119
|
+
kwargs.get("level", ""), DEFAULT_LEVEL
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
super().__init__(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
def _logging_to_event_level(self, record):
|
|
125
|
+
# type: (LogRecord) -> str
|
|
126
|
+
try:
|
|
127
|
+
return SENTRY_LEVEL_FROM_LOGURU_LEVEL[
|
|
128
|
+
LoggingLevels(record.levelno).name
|
|
129
|
+
].lower()
|
|
130
|
+
except (ValueError, KeyError):
|
|
131
|
+
return record.levelname.lower() if record.levelname else ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
|
|
135
|
+
"""Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
|
|
136
|
+
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
|
|
141
|
+
"""Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
|
|
142
|
+
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def loguru_sentry_logs_handler(message):
|
|
147
|
+
# type: (Message) -> None
|
|
148
|
+
# This is intentionally a callable sink instead of a standard logging handler
|
|
149
|
+
# since otherwise we wouldn't get direct access to message.record
|
|
150
|
+
client = sentry_sdk.get_client()
|
|
151
|
+
|
|
152
|
+
if not client.is_active():
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if not has_logs_enabled(client.options):
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
record = message.record
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
LoguruIntegration.sentry_logs_level is None
|
|
162
|
+
or record["level"].no < LoguruIntegration.sentry_logs_level
|
|
163
|
+
):
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
otel_severity_number, otel_severity_text = _log_level_to_otel(
|
|
167
|
+
record["level"].no, SEVERITY_TO_OTEL_SEVERITY
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
attrs = {"sentry.origin": "auto.log.loguru"} # type: dict[str, Any]
|
|
171
|
+
|
|
172
|
+
project_root = client.options["project_root"]
|
|
173
|
+
if record.get("file"):
|
|
174
|
+
if project_root is not None and record["file"].path.startswith(project_root):
|
|
175
|
+
attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :]
|
|
176
|
+
else:
|
|
177
|
+
attrs["code.file.path"] = record["file"].path
|
|
178
|
+
|
|
179
|
+
if record.get("line") is not None:
|
|
180
|
+
attrs["code.line.number"] = record["line"]
|
|
181
|
+
|
|
182
|
+
if record.get("function"):
|
|
183
|
+
attrs["code.function.name"] = record["function"]
|
|
184
|
+
|
|
185
|
+
if record.get("thread"):
|
|
186
|
+
attrs["thread.name"] = record["thread"].name
|
|
187
|
+
attrs["thread.id"] = record["thread"].id
|
|
188
|
+
|
|
189
|
+
if record.get("process"):
|
|
190
|
+
attrs["process.pid"] = record["process"].id
|
|
191
|
+
attrs["process.executable.name"] = record["process"].name
|
|
192
|
+
|
|
193
|
+
if record.get("name"):
|
|
194
|
+
attrs["logger.name"] = record["name"]
|
|
195
|
+
|
|
196
|
+
extra = record.get("extra")
|
|
197
|
+
if isinstance(extra, dict):
|
|
198
|
+
for key, value in extra.items():
|
|
199
|
+
if isinstance(value, (str, int, float, bool)):
|
|
200
|
+
attrs[f"sentry.message.parameter.{key}"] = value
|
|
201
|
+
else:
|
|
202
|
+
attrs[f"sentry.message.parameter.{key}"] = safe_repr(value)
|
|
203
|
+
|
|
204
|
+
client._capture_log(
|
|
205
|
+
{
|
|
206
|
+
"severity_text": otel_severity_text,
|
|
207
|
+
"severity_number": otel_severity_number,
|
|
208
|
+
"body": record["message"],
|
|
209
|
+
"attributes": attrs,
|
|
210
|
+
"time_unix_nano": int(record["time"].timestamp() * 1e9),
|
|
211
|
+
"trace_id": None,
|
|
212
|
+
}
|
|
213
|
+
)
|