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 ,
@@ -65,9 +67,11 @@ def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
6567 # type: (Optional[int], Optional[int]) -> None
6668 self ._handler = None
6769 self ._breadcrumb_handler = None
70+ self ._sentry_logs_handler = None
6871
6972 if level is not None :
7073 self ._breadcrumb_handler = BreadcrumbHandler (level = level )
74+ self ._sentry_logs_handler = SentryLogsHandler (level = level )
7175
7276 if event_level is not None :
7377 self ._handler = EventHandler (level = event_level )
@@ -83,6 +87,12 @@ def _handle_record(self, record):
8387 ):
8488 self ._breadcrumb_handler .handle (record )
8589
90+ if (
91+ self ._sentry_logs_handler is not None
92+ and record .levelno >= self ._sentry_logs_handler .level
93+ ):
94+ self ._sentry_logs_handler .handle (record )
95+
8696 @staticmethod
8797 def setup_once ():
8898 # type: () -> None
@@ -296,3 +306,91 @@ def _breadcrumb_from_record(self, record):
296306 "timestamp" : datetime .fromtimestamp (record .created , timezone .utc ),
297307 "data" : self ._extra_from_record (record ),
298308 }
309+
310+
311+ def _python_level_to_otel (record_level ):
312+ # type: (int) -> (int, str)
313+ for py_level , otel_severity_number , otel_severity_text in [
314+ (50 , 21 , "fatal" ),
315+ (40 , 17 , "error" ),
316+ (30 , 13 , "warn" ),
317+ (20 , 9 , "info" ),
318+ (10 , 5 , "debug" ),
319+ (5 , 1 , "trace" ),
320+ (0 , 0 , "default" ),
321+ ]:
322+ if max (record_level , 0 ) >= py_level :
323+ return otel_severity_number , otel_severity_text
324+
325+
326+ class SentryLogsHandler (_BaseHandler ):
327+ """
328+ A logging handler that records Sentry logs for each Python log record.
329+
330+ Note that you do not have to use this class if the logging integration is enabled, which it is by default.
331+ """
332+
333+ def emit (self , record ):
334+ # type: (LogRecord) -> Any
335+ with capture_internal_exceptions ():
336+ self .format (record )
337+ if not self ._can_record (record ):
338+ return
339+
340+ client = sentry_sdk .get_client ()
341+ if not client .is_active ():
342+ return
343+
344+ if not client .options ["_experiments" ].get ("enable_sentry_logs" , False ):
345+ return
346+
347+ if record .msg .startswith ("[Sentry Logs]" ):
348+ return # avoid infinite loop when debug is true
349+
350+ SentryLogsHandler ._capture_log_from_record (client , record )
351+
352+ @staticmethod
353+ def _capture_log_from_record (client , record ):
354+ # type: (BaseClient, LogRecord) -> None
355+ scope = sentry_sdk .get_current_scope ()
356+ otel_severity_number , otel_severity_text = _python_level_to_otel (record .levelno )
357+ kwargs = {
358+ "sentry.message.template" : (
359+ record .msg if isinstance (record .msg , str ) else json .dumps (record .msg )
360+ ),
361+ }
362+ if record .args is not None :
363+ if isinstance (record .args , tuple ):
364+ for i , arg in enumerate (record .args ):
365+ kwargs [f"sentry.message.parameters.{ i } " ] = (
366+ arg if isinstance (arg , str ) else json .dumps (arg )
367+ )
368+ if record .lineno :
369+ kwargs ["code.line.number" ] = record .lineno
370+ if record .pathname :
371+ kwargs ["code.file.path" ] = record .pathname
372+ if record .funcName :
373+ kwargs ["code.function.name" ] = record .funcName
374+
375+ if record .thread :
376+ kwargs ["thread.id" ] = record .thread
377+ if record .threadName :
378+ kwargs ["thread.name" ] = record .threadName
379+
380+ if record .process :
381+ kwargs ["process.pid" ] = record .process
382+ if record .processName :
383+ kwargs ["process.executable.name" ] = record .processName
384+ if record .name :
385+ kwargs ["logger.name" ] = record .name
386+ if record .created :
387+ kwargs ["time_unix_nano" ] = int (record .created * 1e9 )
388+
389+ # noinspection PyProtectedMember
390+ client ._capture_experimental_log (
391+ scope ,
392+ otel_severity_text ,
393+ otel_severity_number ,
394+ record .getMessage (),
395+ ** kwargs ,
396+ )
0 commit comments