sentry-sdk 3.0.0a1__py2.py3-none-any.whl → 3.0.0a3__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sentry-sdk might be problematic. Click here for more details.

Files changed (157) hide show
  1. sentry_sdk/__init__.py +2 -0
  2. sentry_sdk/_compat.py +5 -12
  3. sentry_sdk/_init_implementation.py +7 -7
  4. sentry_sdk/_log_batcher.py +17 -29
  5. sentry_sdk/_lru_cache.py +7 -9
  6. sentry_sdk/_queue.py +2 -4
  7. sentry_sdk/_types.py +11 -18
  8. sentry_sdk/_werkzeug.py +5 -7
  9. sentry_sdk/ai/monitoring.py +44 -31
  10. sentry_sdk/ai/utils.py +3 -4
  11. sentry_sdk/api.py +75 -87
  12. sentry_sdk/attachments.py +10 -12
  13. sentry_sdk/client.py +137 -155
  14. sentry_sdk/consts.py +430 -174
  15. sentry_sdk/crons/api.py +16 -17
  16. sentry_sdk/crons/decorator.py +25 -27
  17. sentry_sdk/debug.py +4 -6
  18. sentry_sdk/envelope.py +46 -112
  19. sentry_sdk/feature_flags.py +9 -15
  20. sentry_sdk/integrations/__init__.py +24 -19
  21. sentry_sdk/integrations/_asgi_common.py +15 -18
  22. sentry_sdk/integrations/_wsgi_common.py +22 -33
  23. sentry_sdk/integrations/aiohttp.py +32 -30
  24. sentry_sdk/integrations/anthropic.py +42 -37
  25. sentry_sdk/integrations/argv.py +3 -4
  26. sentry_sdk/integrations/ariadne.py +16 -18
  27. sentry_sdk/integrations/arq.py +21 -29
  28. sentry_sdk/integrations/asgi.py +63 -37
  29. sentry_sdk/integrations/asyncio.py +14 -16
  30. sentry_sdk/integrations/atexit.py +6 -10
  31. sentry_sdk/integrations/aws_lambda.py +26 -36
  32. sentry_sdk/integrations/beam.py +10 -18
  33. sentry_sdk/integrations/boto3.py +18 -16
  34. sentry_sdk/integrations/bottle.py +25 -34
  35. sentry_sdk/integrations/celery/__init__.py +41 -61
  36. sentry_sdk/integrations/celery/beat.py +23 -27
  37. sentry_sdk/integrations/celery/utils.py +15 -17
  38. sentry_sdk/integrations/chalice.py +8 -10
  39. sentry_sdk/integrations/clickhouse_driver.py +21 -31
  40. sentry_sdk/integrations/cloud_resource_context.py +9 -16
  41. sentry_sdk/integrations/cohere.py +27 -33
  42. sentry_sdk/integrations/dedupe.py +5 -8
  43. sentry_sdk/integrations/django/__init__.py +57 -72
  44. sentry_sdk/integrations/django/asgi.py +26 -34
  45. sentry_sdk/integrations/django/caching.py +23 -19
  46. sentry_sdk/integrations/django/middleware.py +17 -20
  47. sentry_sdk/integrations/django/signals_handlers.py +11 -10
  48. sentry_sdk/integrations/django/templates.py +19 -16
  49. sentry_sdk/integrations/django/transactions.py +16 -11
  50. sentry_sdk/integrations/django/views.py +6 -10
  51. sentry_sdk/integrations/dramatiq.py +21 -21
  52. sentry_sdk/integrations/excepthook.py +10 -10
  53. sentry_sdk/integrations/executing.py +3 -4
  54. sentry_sdk/integrations/falcon.py +27 -42
  55. sentry_sdk/integrations/fastapi.py +13 -16
  56. sentry_sdk/integrations/flask.py +31 -38
  57. sentry_sdk/integrations/gcp.py +13 -16
  58. sentry_sdk/integrations/gnu_backtrace.py +4 -6
  59. sentry_sdk/integrations/gql.py +16 -17
  60. sentry_sdk/integrations/graphene.py +13 -12
  61. sentry_sdk/integrations/grpc/__init__.py +19 -1
  62. sentry_sdk/integrations/grpc/aio/server.py +15 -14
  63. sentry_sdk/integrations/grpc/client.py +19 -9
  64. sentry_sdk/integrations/grpc/consts.py +2 -0
  65. sentry_sdk/integrations/grpc/server.py +12 -8
  66. sentry_sdk/integrations/httpx.py +9 -12
  67. sentry_sdk/integrations/huey.py +13 -20
  68. sentry_sdk/integrations/huggingface_hub.py +18 -18
  69. sentry_sdk/integrations/langchain.py +203 -113
  70. sentry_sdk/integrations/launchdarkly.py +13 -10
  71. sentry_sdk/integrations/litestar.py +37 -35
  72. sentry_sdk/integrations/logging.py +52 -65
  73. sentry_sdk/integrations/loguru.py +127 -57
  74. sentry_sdk/integrations/modules.py +3 -4
  75. sentry_sdk/integrations/openai.py +100 -88
  76. sentry_sdk/integrations/openai_agents/__init__.py +49 -0
  77. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  78. sentry_sdk/integrations/openai_agents/patches/__init__.py +4 -0
  79. sentry_sdk/integrations/openai_agents/patches/agent_run.py +152 -0
  80. sentry_sdk/integrations/openai_agents/patches/models.py +52 -0
  81. sentry_sdk/integrations/openai_agents/patches/runner.py +42 -0
  82. sentry_sdk/integrations/openai_agents/patches/tools.py +84 -0
  83. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  84. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +20 -0
  85. sentry_sdk/integrations/openai_agents/spans/ai_client.py +46 -0
  86. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +47 -0
  87. sentry_sdk/integrations/openai_agents/spans/handoff.py +24 -0
  88. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +41 -0
  89. sentry_sdk/integrations/openai_agents/utils.py +201 -0
  90. sentry_sdk/integrations/openfeature.py +11 -6
  91. sentry_sdk/integrations/pure_eval.py +6 -10
  92. sentry_sdk/integrations/pymongo.py +13 -17
  93. sentry_sdk/integrations/pyramid.py +31 -36
  94. sentry_sdk/integrations/quart.py +23 -28
  95. sentry_sdk/integrations/ray.py +73 -64
  96. sentry_sdk/integrations/redis/__init__.py +7 -4
  97. sentry_sdk/integrations/redis/_async_common.py +25 -12
  98. sentry_sdk/integrations/redis/_sync_common.py +19 -13
  99. sentry_sdk/integrations/redis/modules/caches.py +17 -8
  100. sentry_sdk/integrations/redis/modules/queries.py +9 -8
  101. sentry_sdk/integrations/redis/rb.py +3 -2
  102. sentry_sdk/integrations/redis/redis.py +4 -4
  103. sentry_sdk/integrations/redis/redis_cluster.py +21 -13
  104. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +3 -2
  105. sentry_sdk/integrations/redis/utils.py +23 -24
  106. sentry_sdk/integrations/rq.py +13 -16
  107. sentry_sdk/integrations/rust_tracing.py +9 -6
  108. sentry_sdk/integrations/sanic.py +34 -46
  109. sentry_sdk/integrations/serverless.py +22 -27
  110. sentry_sdk/integrations/socket.py +27 -15
  111. sentry_sdk/integrations/spark/__init__.py +1 -0
  112. sentry_sdk/integrations/spark/spark_driver.py +45 -83
  113. sentry_sdk/integrations/spark/spark_worker.py +7 -11
  114. sentry_sdk/integrations/sqlalchemy.py +22 -19
  115. sentry_sdk/integrations/starlette.py +86 -90
  116. sentry_sdk/integrations/starlite.py +28 -34
  117. sentry_sdk/integrations/statsig.py +5 -4
  118. sentry_sdk/integrations/stdlib.py +28 -24
  119. sentry_sdk/integrations/strawberry.py +62 -49
  120. sentry_sdk/integrations/sys_exit.py +7 -11
  121. sentry_sdk/integrations/threading.py +12 -14
  122. sentry_sdk/integrations/tornado.py +28 -32
  123. sentry_sdk/integrations/trytond.py +4 -3
  124. sentry_sdk/integrations/typer.py +8 -6
  125. sentry_sdk/integrations/unleash.py +5 -4
  126. sentry_sdk/integrations/wsgi.py +47 -46
  127. sentry_sdk/logger.py +41 -10
  128. sentry_sdk/monitor.py +16 -28
  129. sentry_sdk/opentelemetry/consts.py +11 -4
  130. sentry_sdk/opentelemetry/contextvars_context.py +26 -16
  131. sentry_sdk/opentelemetry/propagator.py +38 -21
  132. sentry_sdk/opentelemetry/sampler.py +51 -34
  133. sentry_sdk/opentelemetry/scope.py +36 -37
  134. sentry_sdk/opentelemetry/span_processor.py +48 -58
  135. sentry_sdk/opentelemetry/tracing.py +58 -14
  136. sentry_sdk/opentelemetry/utils.py +186 -194
  137. sentry_sdk/profiler/continuous_profiler.py +108 -97
  138. sentry_sdk/profiler/transaction_profiler.py +70 -97
  139. sentry_sdk/profiler/utils.py +11 -15
  140. sentry_sdk/scope.py +251 -273
  141. sentry_sdk/scrubber.py +22 -26
  142. sentry_sdk/serializer.py +40 -54
  143. sentry_sdk/session.py +44 -61
  144. sentry_sdk/sessions.py +35 -49
  145. sentry_sdk/spotlight.py +15 -21
  146. sentry_sdk/tracing.py +121 -187
  147. sentry_sdk/tracing_utils.py +104 -122
  148. sentry_sdk/transport.py +131 -157
  149. sentry_sdk/utils.py +232 -309
  150. sentry_sdk/worker.py +16 -28
  151. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/METADATA +3 -3
  152. sentry_sdk-3.0.0a3.dist-info/RECORD +168 -0
  153. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/WHEEL +1 -1
  154. sentry_sdk-3.0.0a1.dist-info/RECORD +0 -154
  155. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/entry_points.txt +0 -0
  156. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/licenses/LICENSE +0 -0
  157. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  from collections.abc import Set
2
3
  import sentry_sdk
3
4
  from sentry_sdk.consts import OP, TransactionSource, SOURCE_FOR_STYLE
@@ -52,13 +53,12 @@ class LitestarIntegration(Integration):
52
53
 
53
54
  def __init__(
54
55
  self,
55
- failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int]
56
+ failed_request_status_codes: Set[int] = _DEFAULT_FAILED_REQUEST_STATUS_CODES,
56
57
  ) -> None:
57
58
  self.failed_request_status_codes = failed_request_status_codes
58
59
 
59
60
  @staticmethod
60
- def setup_once():
61
- # type: () -> None
61
+ def setup_once() -> None:
62
62
  patch_app_init()
63
63
  patch_middlewares()
64
64
  patch_http_route_handle()
@@ -75,8 +75,9 @@ class LitestarIntegration(Integration):
75
75
 
76
76
 
77
77
  class SentryLitestarASGIMiddleware(SentryAsgiMiddleware):
78
- def __init__(self, app, span_origin=LitestarIntegration.origin):
79
- # type: (ASGIApp, str) -> None
78
+ def __init__(
79
+ self, app: ASGIApp, span_origin: str = LitestarIntegration.origin
80
+ ) -> None:
80
81
 
81
82
  super().__init__(
82
83
  app=app,
@@ -86,9 +87,16 @@ class SentryLitestarASGIMiddleware(SentryAsgiMiddleware):
86
87
  span_origin=span_origin,
87
88
  )
88
89
 
90
+ def _capture_request_exception(self, exc: Exception) -> None:
91
+ """Avoid catching exceptions from request handlers.
92
+
93
+ Those exceptions are already handled in Litestar.after_exception handler.
94
+ We still catch exceptions from application lifespan handlers.
95
+ """
96
+ pass
97
+
89
98
 
90
- def patch_app_init():
91
- # type: () -> None
99
+ def patch_app_init() -> None:
92
100
  """
93
101
  Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the
94
102
  `SentryLitestarASGIMiddleware` as the outmost middleware in the stack.
@@ -99,8 +107,7 @@ def patch_app_init():
99
107
  old__init__ = Litestar.__init__
100
108
 
101
109
  @ensure_integration_enabled(LitestarIntegration, old__init__)
102
- def injection_wrapper(self, *args, **kwargs):
103
- # type: (Litestar, *Any, **Any) -> None
110
+ def injection_wrapper(self: Litestar, *args: Any, **kwargs: Any) -> None:
104
111
  kwargs["after_exception"] = [
105
112
  exception_handler,
106
113
  *(kwargs.get("after_exception") or []),
@@ -114,13 +121,11 @@ def patch_app_init():
114
121
  Litestar.__init__ = injection_wrapper
115
122
 
116
123
 
117
- def patch_middlewares():
118
- # type: () -> None
124
+ def patch_middlewares() -> None:
119
125
  old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware
120
126
 
121
127
  @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack)
122
- def resolve_middleware_wrapper(self):
123
- # type: (BaseRouteHandler) -> list[Middleware]
128
+ def resolve_middleware_wrapper(self: BaseRouteHandler) -> list[Middleware]:
124
129
  return [
125
130
  enable_span_for_middleware(middleware)
126
131
  for middleware in old_resolve_middleware_stack(self)
@@ -129,8 +134,7 @@ def patch_middlewares():
129
134
  BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper
130
135
 
131
136
 
132
- def enable_span_for_middleware(middleware):
133
- # type: (Middleware) -> Middleware
137
+ def enable_span_for_middleware(middleware: Middleware) -> Middleware:
134
138
  if (
135
139
  not hasattr(middleware, "__call__") # noqa: B004
136
140
  or middleware is SentryLitestarASGIMiddleware
@@ -138,12 +142,13 @@ def enable_span_for_middleware(middleware):
138
142
  return middleware
139
143
 
140
144
  if isinstance(middleware, DefineMiddleware):
141
- old_call = middleware.middleware.__call__ # type: ASGIApp
145
+ old_call: ASGIApp = middleware.middleware.__call__
142
146
  else:
143
147
  old_call = middleware.__call__
144
148
 
145
- async def _create_span_call(self, scope, receive, send):
146
- # type: (MiddlewareProtocol, LitestarScope, Receive, Send) -> None
149
+ async def _create_span_call(
150
+ self: MiddlewareProtocol, scope: LitestarScope, receive: Receive, send: Send
151
+ ) -> None:
147
152
  if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
148
153
  return await old_call(self, scope, receive, send)
149
154
 
@@ -157,8 +162,9 @@ def enable_span_for_middleware(middleware):
157
162
  middleware_span.set_tag("litestar.middleware_name", middleware_name)
158
163
 
159
164
  # Creating spans for the "receive" callback
160
- async def _sentry_receive(*args, **kwargs):
161
- # type: (*Any, **Any) -> Union[HTTPReceiveMessage, WebSocketReceiveMessage]
165
+ async def _sentry_receive(
166
+ *args: Any, **kwargs: Any
167
+ ) -> Union[HTTPReceiveMessage, WebSocketReceiveMessage]:
162
168
  if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
163
169
  return await receive(*args, **kwargs)
164
170
  with sentry_sdk.start_span(
@@ -175,8 +181,7 @@ def enable_span_for_middleware(middleware):
175
181
  new_receive = _sentry_receive if not receive_patched else receive
176
182
 
177
183
  # Creating spans for the "send" callback
178
- async def _sentry_send(message):
179
- # type: (Message) -> None
184
+ async def _sentry_send(message: Message) -> None:
180
185
  if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
181
186
  return await send(message)
182
187
  with sentry_sdk.start_span(
@@ -205,19 +210,19 @@ def enable_span_for_middleware(middleware):
205
210
  return middleware
206
211
 
207
212
 
208
- def patch_http_route_handle():
209
- # type: () -> None
213
+ def patch_http_route_handle() -> None:
210
214
  old_handle = HTTPRoute.handle
211
215
 
212
- async def handle_wrapper(self, scope, receive, send):
213
- # type: (HTTPRoute, HTTPScope, Receive, Send) -> None
216
+ async def handle_wrapper(
217
+ self: HTTPRoute, scope: HTTPScope, receive: Receive, send: Send
218
+ ) -> None:
214
219
  if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
215
220
  return await old_handle(self, scope, receive, send)
216
221
 
217
222
  sentry_scope = sentry_sdk.get_isolation_scope()
218
- request = scope["app"].request_class(
223
+ request: Request[Any, Any] = scope["app"].request_class(
219
224
  scope=scope, receive=receive, send=send
220
- ) # type: Request[Any, Any]
225
+ )
221
226
  extracted_request_data = ConnectionDataExtractor(
222
227
  parse_body=True, parse_query=True
223
228
  )(request)
@@ -225,8 +230,7 @@ def patch_http_route_handle():
225
230
 
226
231
  request_data = await body
227
232
 
228
- def event_processor(event, _):
229
- # type: (Event, Hint) -> Event
233
+ def event_processor(event: Event, _: Hint) -> Event:
230
234
  route_handler = scope.get("route_handler")
231
235
 
232
236
  request_info = event.get("request", {})
@@ -270,8 +274,7 @@ def patch_http_route_handle():
270
274
  HTTPRoute.handle = handle_wrapper
271
275
 
272
276
 
273
- def retrieve_user_from_scope(scope):
274
- # type: (LitestarScope) -> Optional[dict[str, Any]]
277
+ def retrieve_user_from_scope(scope: LitestarScope) -> Optional[dict[str, Any]]:
275
278
  scope_user = scope.get("user")
276
279
  if isinstance(scope_user, dict):
277
280
  return scope_user
@@ -282,9 +285,8 @@ def retrieve_user_from_scope(scope):
282
285
 
283
286
 
284
287
  @ensure_integration_enabled(LitestarIntegration)
285
- def exception_handler(exc, scope):
286
- # type: (Exception, LitestarScope) -> None
287
- user_info = None # type: Optional[dict[str, Any]]
288
+ def exception_handler(exc: Exception, scope: LitestarScope) -> None:
289
+ user_info: Optional[dict[str, Any]] = None
288
290
  if should_send_default_pii():
289
291
  user_info = retrieve_user_from_scope(scope)
290
292
  if user_info and isinstance(user_info, dict):
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import logging
2
3
  import sys
3
4
  from datetime import datetime, timezone
@@ -5,6 +6,7 @@ from fnmatch import fnmatch
5
6
 
6
7
  import sentry_sdk
7
8
  from sentry_sdk.client import BaseClient
9
+ from sentry_sdk.logger import _log_level_to_otel
8
10
  from sentry_sdk.utils import (
9
11
  safe_repr,
10
12
  to_string,
@@ -14,7 +16,7 @@ from sentry_sdk.utils import (
14
16
  )
15
17
  from sentry_sdk.integrations import Integration
16
18
 
17
- from typing import TYPE_CHECKING, Tuple
19
+ from typing import TYPE_CHECKING
18
20
 
19
21
  if TYPE_CHECKING:
20
22
  from collections.abc import MutableMapping
@@ -36,6 +38,16 @@ LOGGING_TO_EVENT_LEVEL = {
36
38
  logging.CRITICAL: "fatal", # CRITICAL is same as FATAL
37
39
  }
38
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
+
50
+
39
51
  # Capturing events from those loggers causes recursion errors. We cannot allow
40
52
  # the user to unconditionally create events from those loggers under any
41
53
  # circumstances.
@@ -53,9 +65,8 @@ _IGNORED_LOGGERS = set(
53
65
 
54
66
 
55
67
  def ignore_logger(
56
- name, # type: str
57
- ):
58
- # type: (...) -> None
68
+ name: str,
69
+ ) -> None:
59
70
  """This disables recording (both in breadcrumbs and as events) calls to
60
71
  a logger of a specific name. Among other uses, many of our integrations
61
72
  use this to prevent their actions being recorded as breadcrumbs. Exposed
@@ -71,11 +82,10 @@ class LoggingIntegration(Integration):
71
82
 
72
83
  def __init__(
73
84
  self,
74
- level=DEFAULT_LEVEL,
75
- event_level=DEFAULT_EVENT_LEVEL,
76
- sentry_logs_level=DEFAULT_LEVEL,
77
- ):
78
- # type: (Optional[int], Optional[int], Optional[int]) -> None
85
+ level: Optional[int] = DEFAULT_LEVEL,
86
+ event_level: Optional[int] = DEFAULT_EVENT_LEVEL,
87
+ sentry_logs_level: Optional[int] = DEFAULT_LEVEL,
88
+ ) -> None:
79
89
  self._handler = None
80
90
  self._breadcrumb_handler = None
81
91
  self._sentry_logs_handler = None
@@ -89,8 +99,7 @@ class LoggingIntegration(Integration):
89
99
  if event_level is not None:
90
100
  self._handler = EventHandler(level=event_level)
91
101
 
92
- def _handle_record(self, record):
93
- # type: (LogRecord) -> None
102
+ def _handle_record(self, record: LogRecord) -> None:
94
103
  if self._handler is not None and record.levelno >= self._handler.level:
95
104
  self._handler.handle(record)
96
105
 
@@ -107,12 +116,10 @@ class LoggingIntegration(Integration):
107
116
  self._sentry_logs_handler.handle(record)
108
117
 
109
118
  @staticmethod
110
- def setup_once():
111
- # type: () -> None
119
+ def setup_once() -> None:
112
120
  old_callhandlers = logging.Logger.callHandlers
113
121
 
114
- def sentry_patched_callhandlers(self, record):
115
- # type: (Any, LogRecord) -> Any
122
+ def sentry_patched_callhandlers(self: Any, record: LogRecord) -> Any:
116
123
  # keeping a local reference because the
117
124
  # global might be discarded on shutdown
118
125
  ignored_loggers = _IGNORED_LOGGERS
@@ -124,7 +131,10 @@ class LoggingIntegration(Integration):
124
131
  # the integration. Otherwise we have a high chance of getting
125
132
  # into a recursion error when the integration is resolved
126
133
  # (this also is slower).
127
- if ignored_loggers is not None and record.name not in ignored_loggers:
134
+ if (
135
+ ignored_loggers is not None
136
+ and record.name.strip() not in ignored_loggers
137
+ ):
128
138
  integration = sentry_sdk.get_client().get_integration(
129
139
  LoggingIntegration
130
140
  )
@@ -165,22 +175,19 @@ class _BaseHandler(logging.Handler):
165
175
  )
166
176
  )
167
177
 
168
- def _can_record(self, record):
169
- # type: (LogRecord) -> bool
178
+ def _can_record(self, record: LogRecord) -> bool:
170
179
  """Prevents ignored loggers from recording"""
171
180
  for logger in _IGNORED_LOGGERS:
172
- if fnmatch(record.name, logger):
181
+ if fnmatch(record.name.strip(), logger):
173
182
  return False
174
183
  return True
175
184
 
176
- def _logging_to_event_level(self, record):
177
- # type: (LogRecord) -> str
185
+ def _logging_to_event_level(self, record: LogRecord) -> str:
178
186
  return LOGGING_TO_EVENT_LEVEL.get(
179
187
  record.levelno, record.levelname.lower() if record.levelname else ""
180
188
  )
181
189
 
182
- def _extra_from_record(self, record):
183
- # type: (LogRecord) -> MutableMapping[str, object]
190
+ def _extra_from_record(self, record: LogRecord) -> MutableMapping[str, object]:
184
191
  return {
185
192
  k: v
186
193
  for k, v in vars(record).items()
@@ -196,14 +203,12 @@ class EventHandler(_BaseHandler):
196
203
  Note that you do not have to use this class if the logging integration is enabled, which it is by default.
197
204
  """
198
205
 
199
- def emit(self, record):
200
- # type: (LogRecord) -> Any
206
+ def emit(self, record: LogRecord) -> Any:
201
207
  with capture_internal_exceptions():
202
208
  self.format(record)
203
209
  return self._emit(record)
204
210
 
205
- def _emit(self, record):
206
- # type: (LogRecord) -> None
211
+ def _emit(self, record: LogRecord) -> None:
207
212
  if not self._can_record(record):
208
213
  return
209
214
 
@@ -290,14 +295,12 @@ class BreadcrumbHandler(_BaseHandler):
290
295
  Note that you do not have to use this class if the logging integration is enabled, which it is by default.
291
296
  """
292
297
 
293
- def emit(self, record):
294
- # type: (LogRecord) -> Any
298
+ def emit(self, record: LogRecord) -> Any:
295
299
  with capture_internal_exceptions():
296
300
  self.format(record)
297
301
  return self._emit(record)
298
302
 
299
- def _emit(self, record):
300
- # type: (LogRecord) -> None
303
+ def _emit(self, record: LogRecord) -> None:
301
304
  if not self._can_record(record):
302
305
  return
303
306
 
@@ -305,8 +308,7 @@ class BreadcrumbHandler(_BaseHandler):
305
308
  self._breadcrumb_from_record(record), hint={"log_record": record}
306
309
  )
307
310
 
308
- def _breadcrumb_from_record(self, record):
309
- # type: (LogRecord) -> Dict[str, Any]
311
+ def _breadcrumb_from_record(self, record: LogRecord) -> Dict[str, Any]:
310
312
  return {
311
313
  "type": "log",
312
314
  "level": self._logging_to_event_level(record),
@@ -317,21 +319,6 @@ class BreadcrumbHandler(_BaseHandler):
317
319
  }
318
320
 
319
321
 
320
- def _python_level_to_otel(record_level):
321
- # type: (int) -> Tuple[int, str]
322
- for py_level, otel_severity_number, otel_severity_text in [
323
- (50, 21, "fatal"),
324
- (40, 17, "error"),
325
- (30, 13, "warn"),
326
- (20, 9, "info"),
327
- (10, 5, "debug"),
328
- (5, 1, "trace"),
329
- ]:
330
- if record_level >= py_level:
331
- return otel_severity_number, otel_severity_text
332
- return 0, "default"
333
-
334
-
335
322
  class SentryLogsHandler(_BaseHandler):
336
323
  """
337
324
  A logging handler that records Sentry logs for each Python log record.
@@ -339,8 +326,7 @@ class SentryLogsHandler(_BaseHandler):
339
326
  Note that you do not have to use this class if the logging integration is enabled, which it is by default.
340
327
  """
341
328
 
342
- def emit(self, record):
343
- # type: (LogRecord) -> Any
329
+ def emit(self, record: LogRecord) -> Any:
344
330
  with capture_internal_exceptions():
345
331
  self.format(record)
346
332
  if not self._can_record(record):
@@ -353,30 +339,32 @@ class SentryLogsHandler(_BaseHandler):
353
339
  if not client.options["_experiments"].get("enable_logs", False):
354
340
  return
355
341
 
356
- SentryLogsHandler._capture_log_from_record(client, record)
342
+ self._capture_log_from_record(client, record)
357
343
 
358
- @staticmethod
359
- def _capture_log_from_record(client, record):
360
- # type: (BaseClient, LogRecord) -> None
361
- scope = sentry_sdk.get_current_scope()
362
- otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno)
344
+ def _capture_log_from_record(self, client: BaseClient, record: LogRecord) -> None:
345
+ otel_severity_number, otel_severity_text = _log_level_to_otel(
346
+ record.levelno, SEVERITY_TO_OTEL_SEVERITY
347
+ )
363
348
  project_root = client.options["project_root"]
364
- attrs = {
365
- "sentry.origin": "auto.logger.log",
366
- } # type: dict[str, str | bool | float | int]
349
+ attrs: Any = self._extra_from_record(record)
350
+ attrs["sentry.origin"] = "auto.logger.log"
367
351
  if isinstance(record.msg, str):
368
352
  attrs["sentry.message.template"] = record.msg
369
353
  if record.args is not None:
370
354
  if isinstance(record.args, tuple):
371
355
  for i, arg in enumerate(record.args):
372
- attrs[f"sentry.message.parameters.{i}"] = (
356
+ attrs[f"sentry.message.parameter.{i}"] = (
373
357
  arg
374
- if isinstance(arg, str)
375
- or isinstance(arg, float)
376
- or isinstance(arg, int)
377
- or isinstance(arg, bool)
358
+ if isinstance(arg, (str, float, int, bool))
378
359
  else safe_repr(arg)
379
360
  )
361
+ elif isinstance(record.args, dict):
362
+ for key, value in record.args.items():
363
+ attrs[f"sentry.message.parameter.{key}"] = (
364
+ value
365
+ if isinstance(value, (str, float, int, bool))
366
+ else safe_repr(value)
367
+ )
380
368
  if record.lineno:
381
369
  attrs["code.line.number"] = record.lineno
382
370
  if record.pathname:
@@ -401,7 +389,6 @@ class SentryLogsHandler(_BaseHandler):
401
389
 
402
390
  # noinspection PyProtectedMember
403
391
  client._capture_experimental_log(
404
- scope,
405
392
  {
406
393
  "severity_text": otel_severity_text,
407
394
  "severity_number": otel_severity_number,
@@ -1,22 +1,28 @@
1
+ from __future__ import annotations
1
2
  import enum
2
3
 
4
+ import sentry_sdk
3
5
  from sentry_sdk.integrations import Integration, DidNotEnable
4
6
  from sentry_sdk.integrations.logging import (
5
7
  BreadcrumbHandler,
6
8
  EventHandler,
7
9
  _BaseHandler,
8
10
  )
11
+ from sentry_sdk.logger import _log_level_to_otel
9
12
 
10
13
  from typing import TYPE_CHECKING
11
14
 
12
15
  if TYPE_CHECKING:
13
16
  from logging import LogRecord
14
- from typing import Optional, Tuple, Any
17
+ from typing import Any, Optional
15
18
 
16
19
  try:
17
20
  import loguru
18
21
  from loguru import logger
19
22
  from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT
23
+
24
+ if TYPE_CHECKING:
25
+ from loguru import Message
20
26
  except ImportError:
21
27
  raise DidNotEnable("LOGURU is not installed")
22
28
 
@@ -31,6 +37,10 @@ class LoggingLevels(enum.IntEnum):
31
37
  CRITICAL = 50
32
38
 
33
39
 
40
+ DEFAULT_LEVEL = LoggingLevels.INFO.value
41
+ DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
42
+
43
+
34
44
  SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
35
45
  "TRACE": "DEBUG",
36
46
  "DEBUG": "DEBUG",
@@ -41,61 +51,74 @@ SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
41
51
  "CRITICAL": "CRITICAL",
42
52
  }
43
53
 
44
- DEFAULT_LEVEL = LoggingLevels.INFO.value
45
- DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value
46
- # We need to save the handlers to be able to remove them later
47
- # in tests (they call `LoguruIntegration.__init__` multiple times,
48
- # and we can't use `setup_once` because it's called before
49
- # than we get configuration).
50
- _ADDED_HANDLERS = (None, None) # type: Tuple[Optional[int], Optional[int]]
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
+ }
51
64
 
52
65
 
53
66
  class LoguruIntegration(Integration):
54
67
  identifier = "loguru"
55
68
 
69
+ level: Optional[int] = DEFAULT_LEVEL
70
+ event_level: Optional[int] = DEFAULT_EVENT_LEVEL
71
+ breadcrumb_format = DEFAULT_FORMAT
72
+ event_format = DEFAULT_FORMAT
73
+ sentry_logs_level: Optional[int] = DEFAULT_LEVEL
74
+
56
75
  def __init__(
57
76
  self,
58
- level=DEFAULT_LEVEL,
59
- event_level=DEFAULT_EVENT_LEVEL,
60
- breadcrumb_format=DEFAULT_FORMAT,
61
- event_format=DEFAULT_FORMAT,
62
- ):
63
- # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction) -> None
64
- global _ADDED_HANDLERS
65
- breadcrumb_handler, event_handler = _ADDED_HANDLERS
66
-
67
- if breadcrumb_handler is not None:
68
- logger.remove(breadcrumb_handler)
69
- breadcrumb_handler = None
70
- if event_handler is not None:
71
- logger.remove(event_handler)
72
- event_handler = None
73
-
74
- if level is not None:
75
- breadcrumb_handler = logger.add(
76
- LoguruBreadcrumbHandler(level=level),
77
- level=level,
78
- format=breadcrumb_format,
79
- )
77
+ level: Optional[int] = DEFAULT_LEVEL,
78
+ event_level: Optional[int] = DEFAULT_EVENT_LEVEL,
79
+ breadcrumb_format: str | loguru.FormatFunction = DEFAULT_FORMAT,
80
+ event_format: str | loguru.FormatFunction = DEFAULT_FORMAT,
81
+ sentry_logs_level: Optional[int] = DEFAULT_LEVEL,
82
+ ) -> None:
83
+ LoguruIntegration.level = level
84
+ LoguruIntegration.event_level = event_level
85
+ LoguruIntegration.breadcrumb_format = breadcrumb_format
86
+ LoguruIntegration.event_format = event_format
87
+ LoguruIntegration.sentry_logs_level = sentry_logs_level
80
88
 
81
- if event_level is not None:
82
- event_handler = logger.add(
83
- LoguruEventHandler(level=event_level),
84
- level=event_level,
85
- format=event_format,
89
+ @staticmethod
90
+ def setup_once() -> None:
91
+ if LoguruIntegration.level is not None:
92
+ logger.add(
93
+ LoguruBreadcrumbHandler(level=LoguruIntegration.level),
94
+ level=LoguruIntegration.level,
95
+ format=LoguruIntegration.breadcrumb_format,
86
96
  )
87
97
 
88
- _ADDED_HANDLERS = (breadcrumb_handler, event_handler)
98
+ if LoguruIntegration.event_level is not None:
99
+ logger.add(
100
+ LoguruEventHandler(level=LoguruIntegration.event_level),
101
+ level=LoguruIntegration.event_level,
102
+ format=LoguruIntegration.event_format,
103
+ )
89
104
 
90
- @staticmethod
91
- def setup_once():
92
- # type: () -> None
93
- pass # we do everything in __init__
105
+ if LoguruIntegration.sentry_logs_level is not None:
106
+ logger.add(
107
+ loguru_sentry_logs_handler,
108
+ level=LoguruIntegration.sentry_logs_level,
109
+ )
94
110
 
95
111
 
96
112
  class _LoguruBaseHandler(_BaseHandler):
97
- def _logging_to_event_level(self, record):
98
- # type: (LogRecord) -> str
113
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
114
+ if kwargs.get("level"):
115
+ kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
116
+ kwargs.get("level", ""), DEFAULT_LEVEL
117
+ )
118
+
119
+ super().__init__(*args, **kwargs)
120
+
121
+ def _logging_to_event_level(self, record: LogRecord) -> str:
99
122
  try:
100
123
  return SENTRY_LEVEL_FROM_LOGURU_LEVEL[
101
124
  LoggingLevels(record.levelno).name
@@ -107,24 +130,71 @@ class _LoguruBaseHandler(_BaseHandler):
107
130
  class LoguruEventHandler(_LoguruBaseHandler, EventHandler):
108
131
  """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
109
132
 
110
- def __init__(self, *args, **kwargs):
111
- # type: (*Any, **Any) -> None
112
- if kwargs.get("level"):
113
- kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
114
- kwargs.get("level", ""), DEFAULT_LEVEL
115
- )
116
-
117
- super().__init__(*args, **kwargs)
133
+ pass
118
134
 
119
135
 
120
136
  class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler):
121
137
  """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
122
138
 
123
- def __init__(self, *args, **kwargs):
124
- # type: (*Any, **Any) -> None
125
- if kwargs.get("level"):
126
- kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get(
127
- kwargs.get("level", ""), DEFAULT_LEVEL
128
- )
139
+ pass
129
140
 
130
- super().__init__(*args, **kwargs)
141
+
142
+ def loguru_sentry_logs_handler(message: Message) -> None:
143
+ # This is intentionally a callable sink instead of a standard logging handler
144
+ # since otherwise we wouldn't get direct access to message.record
145
+ client = sentry_sdk.get_client()
146
+
147
+ if not client.is_active():
148
+ return
149
+
150
+ if not client.options["_experiments"].get("enable_logs", False):
151
+ return
152
+
153
+ record = message.record
154
+
155
+ if (
156
+ LoguruIntegration.sentry_logs_level is None
157
+ or record["level"].no < LoguruIntegration.sentry_logs_level
158
+ ):
159
+ return
160
+
161
+ otel_severity_number, otel_severity_text = _log_level_to_otel(
162
+ record["level"].no, SEVERITY_TO_OTEL_SEVERITY
163
+ )
164
+
165
+ attrs: dict[str, Any] = {"sentry.origin": "auto.logger.loguru"}
166
+
167
+ project_root = client.options["project_root"]
168
+ if record.get("file"):
169
+ if project_root is not None and record["file"].path.startswith(project_root):
170
+ attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :]
171
+ else:
172
+ attrs["code.file.path"] = record["file"].path
173
+
174
+ if record.get("line") is not None:
175
+ attrs["code.line.number"] = record["line"]
176
+
177
+ if record.get("function"):
178
+ attrs["code.function.name"] = record["function"]
179
+
180
+ if record.get("thread"):
181
+ attrs["thread.name"] = record["thread"].name
182
+ attrs["thread.id"] = record["thread"].id
183
+
184
+ if record.get("process"):
185
+ attrs["process.pid"] = record["process"].id
186
+ attrs["process.executable.name"] = record["process"].name
187
+
188
+ if record.get("name"):
189
+ attrs["logger.name"] = record["name"]
190
+
191
+ client._capture_experimental_log(
192
+ {
193
+ "severity_text": otel_severity_text,
194
+ "severity_number": otel_severity_number,
195
+ "body": record["message"],
196
+ "attributes": attrs,
197
+ "time_unix_nano": int(record["time"].timestamp() * 1e9),
198
+ "trace_id": None,
199
+ }
200
+ )