11import enum
22
3+ import sentry_sdk
34from sentry_sdk .integrations import Integration , DidNotEnable
45from sentry_sdk .integrations .logging import (
56 BreadcrumbHandler ,
67 EventHandler ,
78 _BaseHandler ,
89)
10+ from sentry_sdk .logger import _log_level_to_otel
911
1012from typing import TYPE_CHECKING
1113
1214if TYPE_CHECKING :
1315 from logging import LogRecord
14- from typing import Optional , Any
16+ from typing import Any , Optional
1517
1618try :
1719 import loguru
1820 from loguru import logger
1921 from loguru ._defaults import LOGURU_FORMAT as DEFAULT_FORMAT
22+
23+ if TYPE_CHECKING :
24+ from loguru import Message
2025except ImportError :
2126 raise DidNotEnable ("LOGURU is not installed" )
2227
@@ -31,6 +36,10 @@ class LoggingLevels(enum.IntEnum):
3136 CRITICAL = 50
3237
3338
39+ DEFAULT_LEVEL = LoggingLevels .INFO .value
40+ DEFAULT_EVENT_LEVEL = LoggingLevels .ERROR .value
41+
42+
3443SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
3544 "TRACE" : "DEBUG" ,
3645 "DEBUG" : "DEBUG" ,
@@ -41,8 +50,16 @@ class LoggingLevels(enum.IntEnum):
4150 "CRITICAL" : "CRITICAL" ,
4251}
4352
44- DEFAULT_LEVEL = LoggingLevels .INFO .value
45- DEFAULT_EVENT_LEVEL = LoggingLevels .ERROR .value
53+ # Map Loguru level numbers to corresponding OTel level numbers
54+ SEVERITY_TO_OTEL_SEVERITY = {
55+ LoggingLevels .CRITICAL : 21 , # fatal
56+ LoggingLevels .ERROR : 17 , # error
57+ LoggingLevels .WARNING : 13 , # warn
58+ LoggingLevels .SUCCESS : 11 , # info
59+ LoggingLevels .INFO : 9 , # info
60+ LoggingLevels .DEBUG : 5 , # debug
61+ LoggingLevels .TRACE : 1 , # trace
62+ }
4663
4764
4865class LoguruIntegration (Integration ):
@@ -52,19 +69,22 @@ class LoguruIntegration(Integration):
5269 event_level = DEFAULT_EVENT_LEVEL # type: Optional[int]
5370 breadcrumb_format = DEFAULT_FORMAT
5471 event_format = DEFAULT_FORMAT
72+ sentry_logs_level = DEFAULT_LEVEL # type: Optional[int]
5573
5674 def __init__ (
5775 self ,
5876 level = DEFAULT_LEVEL ,
5977 event_level = DEFAULT_EVENT_LEVEL ,
6078 breadcrumb_format = DEFAULT_FORMAT ,
6179 event_format = DEFAULT_FORMAT ,
80+ sentry_logs_level = DEFAULT_LEVEL ,
6281 ):
63- # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction) -> None
82+ # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction, Optional[int] ) -> None
6483 LoguruIntegration .level = level
6584 LoguruIntegration .event_level = event_level
6685 LoguruIntegration .breadcrumb_format = breadcrumb_format
6786 LoguruIntegration .event_format = event_format
87+ LoguruIntegration .sentry_logs_level = sentry_logs_level
6888
6989 @staticmethod
7090 def setup_once ():
@@ -83,8 +103,23 @@ def setup_once():
83103 format = LoguruIntegration .event_format ,
84104 )
85105
106+ if LoguruIntegration .sentry_logs_level is not None :
107+ logger .add (
108+ loguru_sentry_logs_handler ,
109+ level = LoguruIntegration .sentry_logs_level ,
110+ )
111+
86112
87113class _LoguruBaseHandler (_BaseHandler ):
114+ def __init__ (self , * args , ** kwargs ):
115+ # type: (*Any, **Any) -> None
116+ if kwargs .get ("level" ):
117+ kwargs ["level" ] = SENTRY_LEVEL_FROM_LOGURU_LEVEL .get (
118+ kwargs .get ("level" , "" ), DEFAULT_LEVEL
119+ )
120+
121+ super ().__init__ (* args , ** kwargs )
122+
88123 def _logging_to_event_level (self , record ):
89124 # type: (LogRecord) -> str
90125 try :
@@ -98,24 +133,72 @@ def _logging_to_event_level(self, record):
98133class LoguruEventHandler (_LoguruBaseHandler , EventHandler ):
99134 """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
100135
101- def __init__ (self , * args , ** kwargs ):
102- # type: (*Any, **Any) -> None
103- if kwargs .get ("level" ):
104- kwargs ["level" ] = SENTRY_LEVEL_FROM_LOGURU_LEVEL .get (
105- kwargs .get ("level" , "" ), DEFAULT_LEVEL
106- )
107-
108- super ().__init__ (* args , ** kwargs )
136+ pass
109137
110138
111139class LoguruBreadcrumbHandler (_LoguruBaseHandler , BreadcrumbHandler ):
112140 """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
113141
114- def __init__ (self , * args , ** kwargs ):
115- # type: (*Any, **Any) -> None
116- if kwargs .get ("level" ):
117- kwargs ["level" ] = SENTRY_LEVEL_FROM_LOGURU_LEVEL .get (
118- kwargs .get ("level" , "" ), DEFAULT_LEVEL
119- )
142+ pass
120143
121- super ().__init__ (* args , ** kwargs )
144+
145+ def loguru_sentry_logs_handler (message ):
146+ # type: (Message) -> None
147+ # This is intentionally a callable sink instead of a standard logging handler
148+ # since otherwise we wouldn't get direct access to message.record
149+ client = sentry_sdk .get_client ()
150+
151+ if not client .is_active ():
152+ return
153+
154+ if not client .options ["_experiments" ].get ("enable_logs" , False ):
155+ return
156+
157+ record = message .record
158+
159+ if (
160+ LoguruIntegration .sentry_logs_level is None
161+ or record ["level" ].no < LoguruIntegration .sentry_logs_level
162+ ):
163+ return
164+
165+ otel_severity_number , otel_severity_text = _log_level_to_otel (
166+ record ["level" ].no , SEVERITY_TO_OTEL_SEVERITY
167+ )
168+
169+ attrs = {"sentry.origin" : "auto.logger.loguru" } # type: dict[str, Any]
170+
171+ project_root = client .options ["project_root" ]
172+ if record .get ("file" ):
173+ if project_root is not None and record ["file" ].path .startswith (project_root ):
174+ attrs ["code.file.path" ] = record ["file" ].path [len (project_root ) + 1 :]
175+ else :
176+ attrs ["code.file.path" ] = record ["file" ].path
177+
178+ if record .get ("line" ) is not None :
179+ attrs ["code.line.number" ] = record ["line" ]
180+
181+ if record .get ("function" ):
182+ attrs ["code.function.name" ] = record ["function" ]
183+
184+ if record .get ("thread" ):
185+ attrs ["thread.name" ] = record ["thread" ].name
186+ attrs ["thread.id" ] = record ["thread" ].id
187+
188+ if record .get ("process" ):
189+ attrs ["process.pid" ] = record ["process" ].id
190+ attrs ["process.executable.name" ] = record ["process" ].name
191+
192+ if record .get ("name" ):
193+ attrs ["logger.name" ] = record ["name" ]
194+
195+ client ._capture_experimental_log (
196+ {
197+ "severity_text" : otel_severity_text ,
198+ "severity_number" : otel_severity_number ,
199+ "body" : record ["message" ],
200+ "attributes" : attrs ,
201+ "time_unix_nano" : int (record ["time" ].timestamp () * 1e9 ),
202+ "trace_id" : None ,
203+ }
204+ )
0 commit comments