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
@@ -1,21 +1,25 @@
1
- from __future__ import absolute_import
2
-
3
1
  import logging
4
- import datetime
2
+ import sys
3
+ from datetime import datetime, timezone
4
+ from fnmatch import fnmatch
5
5
 
6
- from sentry_sdk.hub import Hub
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 sentry_sdk._types import MYPY
19
+ from typing import TYPE_CHECKING
17
20
 
18
- if MYPY:
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__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
56
- # type: (Optional[int], Optional[int]) -> None
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 # type: ignore
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 record.name not in _IGNORED_LOGGERS:
92
- integration = Hub.current.get_integration(LoggingIntegration)
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
- def _can_record(record):
100
- # type: (LogRecord) -> bool
101
- return record.name not in _IGNORED_LOGGERS
102
-
103
-
104
- def _breadcrumb_from_record(record):
105
- # type: (LogRecord) -> Dict[str, Any]
106
- return {
107
- "ty": "log",
108
- "level": _logging_to_event_level(record.levelname),
109
- "category": record.name,
110
- "message": record.message,
111
- "timestamp": datetime.datetime.utcfromtimestamp(record.created),
112
- "data": _extra_from_record(record),
113
- }
114
-
115
-
116
- def _logging_to_event_level(levelname):
117
- # type: (str) -> str
118
- return {"critical": "fatal"}.get(levelname.lower(), levelname.lower())
119
-
120
-
121
- COMMON_RECORD_ATTRS = frozenset(
122
- (
123
- "args",
124
- "created",
125
- "exc_info",
126
- "exc_text",
127
- "filename",
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
- # type: (LogRecord) -> Dict[str, None]
153
- return {
154
- k: v
155
- for k, v in iteritems(vars(record))
156
- if k not in COMMON_RECORD_ATTRS
157
- and (not isinstance(k, str) or not k.startswith("_"))
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(logging.Handler, object):
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
- hub = Hub.current
180
- if hub.client is None:
220
+ client = sentry_sdk.get_client()
221
+ if not client.is_active():
181
222
  return
182
223
 
183
- client_options = hub.client.options
224
+ client_options = client.options
184
225
 
185
226
  # exc_info might be None or (None, None, None)
186
- if record.exc_info is not None and record.exc_info[0] is not None:
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["with_locals"]
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
- event["level"] = _logging_to_event_level(record.levelname)
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
- hub.capture_event(event, hint=hint)
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(logging.Handler, object):
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
- Hub.current.add_breadcrumb(
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
+ )