1
+ import json
1
2
import logging
2
3
from datetime import datetime , timezone
3
4
from fnmatch import fnmatch
4
5
5
6
import sentry_sdk
7
+ from sentry_sdk .client import BaseClient
6
8
from sentry_sdk .utils import (
7
9
to_string ,
8
10
event_from_exception ,
11
13
)
12
14
from sentry_sdk .integrations import Integration
13
15
14
- from typing import TYPE_CHECKING
16
+ from typing import TYPE_CHECKING , Tuple
15
17
16
18
if TYPE_CHECKING :
17
19
from collections .abc import MutableMapping
@@ -61,14 +63,23 @@ def ignore_logger(
61
63
class LoggingIntegration (Integration ):
62
64
identifier = "logging"
63
65
64
- def __init__ (self , level = DEFAULT_LEVEL , event_level = DEFAULT_EVENT_LEVEL ):
65
- # type: (Optional[int], Optional[int]) -> None
66
+ def __init__ (
67
+ self ,
68
+ level = DEFAULT_LEVEL ,
69
+ event_level = DEFAULT_EVENT_LEVEL ,
70
+ sentry_logs_level = DEFAULT_LEVEL ,
71
+ ):
72
+ # type: (Optional[int], Optional[int], Optional[int]) -> None
66
73
self ._handler = None
67
74
self ._breadcrumb_handler = None
75
+ self ._sentry_logs_handler = None
68
76
69
77
if level is not None :
70
78
self ._breadcrumb_handler = BreadcrumbHandler (level = level )
71
79
80
+ if sentry_logs_level is not None :
81
+ self ._sentry_logs_handler = SentryLogsHandler (level = sentry_logs_level )
82
+
72
83
if event_level is not None :
73
84
self ._handler = EventHandler (level = event_level )
74
85
@@ -83,6 +94,12 @@ def _handle_record(self, record):
83
94
):
84
95
self ._breadcrumb_handler .handle (record )
85
96
97
+ if (
98
+ self ._sentry_logs_handler is not None
99
+ and record .levelno >= self ._sentry_logs_handler .level
100
+ ):
101
+ self ._sentry_logs_handler .handle (record )
102
+
86
103
@staticmethod
87
104
def setup_once ():
88
105
# type: () -> None
@@ -296,3 +313,90 @@ def _breadcrumb_from_record(self, record):
296
313
"timestamp" : datetime .fromtimestamp (record .created , timezone .utc ),
297
314
"data" : self ._extra_from_record (record ),
298
315
}
316
+
317
+
318
+ def _python_level_to_otel (record_level ):
319
+ # type: (int) -> Tuple[int, str]
320
+ for py_level , otel_severity_number , otel_severity_text in [
321
+ (50 , 21 , "fatal" ),
322
+ (40 , 17 , "error" ),
323
+ (30 , 13 , "warn" ),
324
+ (20 , 9 , "info" ),
325
+ (10 , 5 , "debug" ),
326
+ (5 , 1 , "trace" ),
327
+ ]:
328
+ if record_level >= py_level :
329
+ return otel_severity_number , otel_severity_text
330
+ return 0 , "default"
331
+
332
+
333
+ class SentryLogsHandler (_BaseHandler ):
334
+ """
335
+ A logging handler that records Sentry logs for each Python log record.
336
+
337
+ Note that you do not have to use this class if the logging integration is enabled, which it is by default.
338
+ """
339
+
340
+ def emit (self , record ):
341
+ # type: (LogRecord) -> Any
342
+ with capture_internal_exceptions ():
343
+ self .format (record )
344
+ if not self ._can_record (record ):
345
+ return
346
+
347
+ client = sentry_sdk .get_client ()
348
+ if not client .is_active ():
349
+ return
350
+
351
+ if not client .options ["_experiments" ].get ("enable_sentry_logs" , False ):
352
+ return
353
+
354
+ SentryLogsHandler ._capture_log_from_record (client , record )
355
+
356
+ @staticmethod
357
+ def _capture_log_from_record (client , record ):
358
+ # type: (BaseClient, LogRecord) -> None
359
+ scope = sentry_sdk .get_current_scope ()
360
+ otel_severity_number , otel_severity_text = _python_level_to_otel (record .levelno )
361
+ attrs = {
362
+ "sentry.message.template" : (
363
+ record .msg if isinstance (record .msg , str ) else json .dumps (record .msg )
364
+ ),
365
+ } # type: dict[str, str | bool | float | int]
366
+ if record .args is not None :
367
+ if isinstance (record .args , tuple ):
368
+ for i , arg in enumerate (record .args ):
369
+ attrs [f"sentry.message.parameters.{ i } " ] = (
370
+ arg if isinstance (arg , str ) else json .dumps (arg )
371
+ )
372
+ if record .lineno :
373
+ attrs ["code.line.number" ] = record .lineno
374
+ if record .pathname :
375
+ attrs ["code.file.path" ] = record .pathname
376
+ if record .funcName :
377
+ attrs ["code.function.name" ] = record .funcName
378
+
379
+ if record .thread :
380
+ attrs ["thread.id" ] = record .thread
381
+ if record .threadName :
382
+ attrs ["thread.name" ] = record .threadName
383
+
384
+ if record .process :
385
+ attrs ["process.pid" ] = record .process
386
+ if record .processName :
387
+ attrs ["process.executable.name" ] = record .processName
388
+ if record .name :
389
+ attrs ["logger.name" ] = record .name
390
+
391
+ # noinspection PyProtectedMember
392
+ client ._capture_experimental_log (
393
+ scope ,
394
+ {
395
+ "severity_text" : otel_severity_text ,
396
+ "severity_number" : otel_severity_number ,
397
+ "body" : record .message ,
398
+ "attributes" : attrs ,
399
+ "time_unix_nano" : int (record .created * 1e9 ),
400
+ "trace_id" : None ,
401
+ },
402
+ )
0 commit comments