1+ import json
12import logging
23from datetime import datetime , timezone
34from fnmatch import fnmatch
45
56import sentry_sdk
7+ from sentry_sdk .client import BaseClient
68from sentry_sdk .utils import (
79 to_string ,
810 event_from_exception ,
1113)
1214from sentry_sdk .integrations import Integration
1315
14- from typing import TYPE_CHECKING
16+ from typing import TYPE_CHECKING , Tuple
1517
1618if TYPE_CHECKING :
1719 from collections .abc import MutableMapping
@@ -61,14 +63,23 @@ def ignore_logger(
6163class LoggingIntegration (Integration ):
6264 identifier = "logging"
6365
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
6673 self ._handler = None
6774 self ._breadcrumb_handler = None
75+ self ._sentry_logs_handler = None
6876
6977 if level is not None :
7078 self ._breadcrumb_handler = BreadcrumbHandler (level = level )
7179
80+ if sentry_logs_level is not None :
81+ self ._sentry_logs_handler = SentryLogsHandler (level = sentry_logs_level )
82+
7283 if event_level is not None :
7384 self ._handler = EventHandler (level = event_level )
7485
@@ -83,6 +94,12 @@ def _handle_record(self, record):
8394 ):
8495 self ._breadcrumb_handler .handle (record )
8596
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+
86103 @staticmethod
87104 def setup_once ():
88105 # type: () -> None
@@ -296,3 +313,90 @@ def _breadcrumb_from_record(self, record):
296313 "timestamp" : datetime .fromtimestamp (record .created , timezone .utc ),
297314 "data" : self ._extra_from_record (record ),
298315 }
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