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
@@ -66,14 +68,23 @@ def ignore_logger(
6668class LoggingIntegration (Integration ):
6769 identifier = "logging"
6870
69- def __init__ (self , level = DEFAULT_LEVEL , event_level = DEFAULT_EVENT_LEVEL ):
70- # type: (Optional[int], Optional[int]) -> None
71+ def __init__ (
72+ self ,
73+ level = DEFAULT_LEVEL ,
74+ event_level = DEFAULT_EVENT_LEVEL ,
75+ sentry_logs_level = DEFAULT_LEVEL ,
76+ ):
77+ # type: (Optional[int], Optional[int], Optional[int]) -> None
7178 self ._handler = None
7279 self ._breadcrumb_handler = None
80+ self ._sentry_logs_handler = None
7381
7482 if level is not None :
7583 self ._breadcrumb_handler = BreadcrumbHandler (level = level )
7684
85+ if sentry_logs_level is not None :
86+ self ._sentry_logs_handler = SentryLogsHandler (level = sentry_logs_level )
87+
7788 if event_level is not None :
7889 self ._handler = EventHandler (level = event_level )
7990
@@ -88,6 +99,12 @@ def _handle_record(self, record):
8899 ):
89100 self ._breadcrumb_handler .handle (record )
90101
102+ if (
103+ self ._sentry_logs_handler is not None
104+ and record .levelno >= self ._sentry_logs_handler .level
105+ ):
106+ self ._sentry_logs_handler .handle (record )
107+
91108 @staticmethod
92109 def setup_once ():
93110 # type: () -> None
@@ -301,3 +318,90 @@ def _breadcrumb_from_record(self, record):
301318 "timestamp" : datetime .fromtimestamp (record .created , timezone .utc ),
302319 "data" : self ._extra_from_record (record ),
303320 }
321+
322+
323+ def _python_level_to_otel (record_level ):
324+ # type: (int) -> Tuple[int, str]
325+ for py_level , otel_severity_number , otel_severity_text in [
326+ (50 , 21 , "fatal" ),
327+ (40 , 17 , "error" ),
328+ (30 , 13 , "warn" ),
329+ (20 , 9 , "info" ),
330+ (10 , 5 , "debug" ),
331+ (5 , 1 , "trace" ),
332+ ]:
333+ if record_level >= py_level :
334+ return otel_severity_number , otel_severity_text
335+ return 0 , "default"
336+
337+
338+ class SentryLogsHandler (_BaseHandler ):
339+ """
340+ A logging handler that records Sentry logs for each Python log record.
341+
342+ Note that you do not have to use this class if the logging integration is enabled, which it is by default.
343+ """
344+
345+ def emit (self , record ):
346+ # type: (LogRecord) -> Any
347+ with capture_internal_exceptions ():
348+ self .format (record )
349+ if not self ._can_record (record ):
350+ return
351+
352+ client = sentry_sdk .get_client ()
353+ if not client .is_active ():
354+ return
355+
356+ if not client .options ["_experiments" ].get ("enable_sentry_logs" , False ):
357+ return
358+
359+ SentryLogsHandler ._capture_log_from_record (client , record )
360+
361+ @staticmethod
362+ def _capture_log_from_record (client , record ):
363+ # type: (BaseClient, LogRecord) -> None
364+ scope = sentry_sdk .get_current_scope ()
365+ otel_severity_number , otel_severity_text = _python_level_to_otel (record .levelno )
366+ attrs = {
367+ "sentry.message.template" : (
368+ record .msg if isinstance (record .msg , str ) else json .dumps (record .msg )
369+ ),
370+ } # type: dict[str, str | bool | float | int]
371+ if record .args is not None :
372+ if isinstance (record .args , tuple ):
373+ for i , arg in enumerate (record .args ):
374+ attrs [f"sentry.message.parameters.{ i } " ] = (
375+ arg if isinstance (arg , str ) else json .dumps (arg )
376+ )
377+ if record .lineno :
378+ attrs ["code.line.number" ] = record .lineno
379+ if record .pathname :
380+ attrs ["code.file.path" ] = record .pathname
381+ if record .funcName :
382+ attrs ["code.function.name" ] = record .funcName
383+
384+ if record .thread :
385+ attrs ["thread.id" ] = record .thread
386+ if record .threadName :
387+ attrs ["thread.name" ] = record .threadName
388+
389+ if record .process :
390+ attrs ["process.pid" ] = record .process
391+ if record .processName :
392+ attrs ["process.executable.name" ] = record .processName
393+ if record .name :
394+ attrs ["logger.name" ] = record .name
395+
396+ # noinspection PyProtectedMember
397+ client ._capture_experimental_log (
398+ scope ,
399+ {
400+ "severity_text" : otel_severity_text ,
401+ "severity_number" : otel_severity_number ,
402+ "body" : record .message ,
403+ "attributes" : attrs ,
404+ "time_unix_nano" : int (record .created * 1e9 ),
405+ "trace_id" : None ,
406+ },
407+ )
0 commit comments