sentry-sdk 0.7.5__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -30
  2. sentry_sdk/_compat.py +74 -61
  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 +289 -0
  8. sentry_sdk/_types.py +338 -0
  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 +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  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 +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  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 +134 -0
  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 +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  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 +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  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 +141 -0
  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 +112 -68
  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 +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -1,18 +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
18
 
15
- if False:
19
+ from typing import TYPE_CHECKING
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import MutableMapping
16
23
  from logging import LogRecord
17
24
  from typing import Any
18
25
  from typing import Dict
@@ -20,15 +27,48 @@ if False:
20
27
 
21
28
  DEFAULT_LEVEL = logging.INFO
22
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
+
50
+
51
+ # Capturing events from those loggers causes recursion errors. We cannot allow
52
+ # the user to unconditionally create events from those loggers under any
53
+ # circumstances.
54
+ #
55
+ # Note: Ignoring by logger name here is better than mucking with thread-locals.
56
+ # We do not necessarily know whether thread-locals work 100% correctly in the user's environment.
57
+ _IGNORED_LOGGERS = set(
58
+ ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"]
59
+ )
23
60
 
24
- _IGNORED_LOGGERS = set(["sentry_sdk.errors"])
25
61
 
62
+ def ignore_logger(
63
+ name, # type: str
64
+ ):
65
+ # type: (...) -> None
66
+ """This disables recording (both in breadcrumbs and as events) calls to
67
+ a logger of a specific name. Among other uses, many of our integrations
68
+ use this to prevent their actions being recorded as breadcrumbs. Exposed
69
+ to users as a way to quiet spammy loggers.
26
70
 
27
- def ignore_logger(name):
28
- # type: (str) -> None
29
- """This disables the breadcrumb integration for a logger of a specific
30
- name. This primary use is for some integrations to disable breadcrumbs
31
- of this integration.
71
+ :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``).
32
72
  """
33
73
  _IGNORED_LOGGERS.add(name)
34
74
 
@@ -36,14 +76,23 @@ def ignore_logger(name):
36
76
  class LoggingIntegration(Integration):
37
77
  identifier = "logging"
38
78
 
39
- def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
40
- # type: (int, 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
41
86
  self._handler = None
42
87
  self._breadcrumb_handler = None
88
+ self._sentry_logs_handler = None
43
89
 
44
90
  if level is not None:
45
91
  self._breadcrumb_handler = BreadcrumbHandler(level=level)
46
92
 
93
+ if sentry_logs_level is not None:
94
+ self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level)
95
+
47
96
  if event_level is not None:
48
97
  self._handler = EventHandler(level=event_level)
49
98
 
@@ -58,13 +107,23 @@ class LoggingIntegration(Integration):
58
107
  ):
59
108
  self._breadcrumb_handler.handle(record)
60
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
+
61
116
  @staticmethod
62
117
  def setup_once():
63
118
  # type: () -> None
64
- old_callhandlers = logging.Logger.callHandlers # type: ignore
119
+ old_callhandlers = logging.Logger.callHandlers
65
120
 
66
121
  def sentry_patched_callhandlers(self, record):
67
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
+
68
127
  try:
69
128
  return old_callhandlers(self, record)
70
129
  finally:
@@ -72,76 +131,81 @@ class LoggingIntegration(Integration):
72
131
  # the integration. Otherwise we have a high chance of getting
73
132
  # into a recursion error when the integration is resolved
74
133
  # (this also is slower).
75
- if record.name not in _IGNORED_LOGGERS:
76
- 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
+ )
77
141
  if integration is not None:
78
142
  integration._handle_record(record)
79
143
 
80
144
  logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore
81
145
 
82
146
 
83
- def _can_record(record):
84
- # type: (LogRecord) -> bool
85
- return record.name not in _IGNORED_LOGGERS
86
-
87
-
88
- def _breadcrumb_from_record(record):
89
- # type: (LogRecord) -> Dict[str, Any]
90
- return {
91
- "ty": "log",
92
- "level": _logging_to_event_level(record.levelname),
93
- "category": record.name,
94
- "message": record.message,
95
- "timestamp": datetime.datetime.fromtimestamp(record.created),
96
- "data": _extra_from_record(record),
97
- }
98
-
99
-
100
- def _logging_to_event_level(levelname):
101
- # type: (str) -> str
102
- return {"critical": "fatal"}.get(levelname.lower(), levelname.lower())
103
-
104
-
105
- COMMON_RECORD_ATTRS = frozenset(
106
- (
107
- "args",
108
- "created",
109
- "data",
110
- "exc_info",
111
- "exc_text",
112
- "filename",
113
- "funcName",
114
- "levelname",
115
- "levelno",
116
- "linenno",
117
- "lineno",
118
- "message",
119
- "module",
120
- "msecs",
121
- "msg",
122
- "name",
123
- "pathname",
124
- "process",
125
- "processName",
126
- "relativeCreated",
127
- "stack",
128
- "tags",
129
- "thread",
130
- "threadName",
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
+ )
131
176
  )
132
- )
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
+ )
191
+
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
+ }
133
200
 
134
201
 
135
- def _extra_from_record(record):
136
- # type: (LogRecord) -> Dict[str, None]
137
- return {
138
- k: v
139
- for k, v in vars(record).items()
140
- if k not in COMMON_RECORD_ATTRS and not k.startswith("_")
141
- }
202
+ class EventHandler(_BaseHandler):
203
+ """
204
+ A logging handler that emits Sentry events for each log record
142
205
 
206
+ Note that you do not have to use this class if the logging integration is enabled, which it is by default.
207
+ """
143
208
 
144
- class EventHandler(logging.Handler, object):
145
209
  def emit(self, record):
146
210
  # type: (LogRecord) -> Any
147
211
  with capture_internal_exceptions():
@@ -150,50 +214,92 @@ class EventHandler(logging.Handler, object):
150
214
 
151
215
  def _emit(self, record):
152
216
  # type: (LogRecord) -> None
153
- if not _can_record(record):
217
+ if not self._can_record(record):
154
218
  return
155
219
 
156
- hub = Hub.current
157
- if hub.client is None:
220
+ client = sentry_sdk.get_client()
221
+ if not client.is_active():
158
222
  return
159
223
 
160
- hint = None # type: Optional[Dict[str, Any]]
161
- client_options = hub.client.options
224
+ client_options = client.options
162
225
 
163
226
  # exc_info might be None or (None, None, None)
164
- 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:
165
233
  event, hint = event_from_exception(
166
234
  record.exc_info,
167
235
  client_options=client_options,
168
236
  mechanism={"type": "logging", "handled": True},
169
237
  )
170
- 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:
171
239
  event = {}
172
- hint = None
240
+ hint = {}
173
241
  with capture_internal_exceptions():
174
- event["threads"] = [
175
- {
176
- "stacktrace": current_stacktrace(client_options["with_locals"]),
177
- "crashed": False,
178
- "current": True,
179
- }
180
- ]
242
+ event["threads"] = {
243
+ "values": [
244
+ {
245
+ "stacktrace": current_stacktrace(
246
+ include_local_variables=client_options[
247
+ "include_local_variables"
248
+ ],
249
+ max_value_length=client_options["max_value_length"],
250
+ ),
251
+ "crashed": False,
252
+ "current": True,
253
+ }
254
+ ]
255
+ }
181
256
  else:
182
257
  event = {}
258
+ hint = {}
259
+
260
+ hint["log_record"] = record
183
261
 
184
- 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]
185
265
  event["logger"] = record.name
186
- event["logentry"] = {"message": to_string(record.msg), "params": record.args}
187
- event["extra"] = _extra_from_record(record)
188
266
 
189
- 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)
190
290
 
191
291
 
192
292
  # Legacy name
193
293
  SentryHandler = EventHandler
194
294
 
195
295
 
196
- class BreadcrumbHandler(logging.Handler, object):
296
+ class BreadcrumbHandler(_BaseHandler):
297
+ """
298
+ A logging handler that records breadcrumbs for each log record.
299
+
300
+ Note that you do not have to use this class if the logging integration is enabled, which it is by default.
301
+ """
302
+
197
303
  def emit(self, record):
198
304
  # type: (LogRecord) -> Any
199
305
  with capture_internal_exceptions():
@@ -202,9 +308,114 @@ class BreadcrumbHandler(logging.Handler, object):
202
308
 
203
309
  def _emit(self, record):
204
310
  # type: (LogRecord) -> None
205
- if not _can_record(record):
311
+ if not self._can_record(record):
206
312
  return
207
313
 
208
- Hub.current.add_breadcrumb(
209
- _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
+ },
210
421
  )