1
1
import enum
2
2
3
+ import sentry_sdk
3
4
from sentry_sdk .integrations import Integration , DidNotEnable
4
5
from sentry_sdk .integrations .logging import (
5
6
BreadcrumbHandler ,
6
7
EventHandler ,
7
8
_BaseHandler ,
8
9
)
10
+ from sentry_sdk .logger import _log_level_to_otel
9
11
10
12
from typing import TYPE_CHECKING
11
13
12
14
if TYPE_CHECKING :
13
15
from logging import LogRecord
14
- from typing import Optional , Any
16
+ from typing import Any , Optional
15
17
16
18
try :
17
19
import loguru
18
20
from loguru import logger
19
21
from loguru ._defaults import LOGURU_FORMAT as DEFAULT_FORMAT
22
+
23
+ if TYPE_CHECKING :
24
+ from loguru import Message
20
25
except ImportError :
21
26
raise DidNotEnable ("LOGURU is not installed" )
22
27
@@ -31,6 +36,10 @@ class LoggingLevels(enum.IntEnum):
31
36
CRITICAL = 50
32
37
33
38
39
+ DEFAULT_LEVEL = LoggingLevels .INFO .value
40
+ DEFAULT_EVENT_LEVEL = LoggingLevels .ERROR .value
41
+
42
+
34
43
SENTRY_LEVEL_FROM_LOGURU_LEVEL = {
35
44
"TRACE" : "DEBUG" ,
36
45
"DEBUG" : "DEBUG" ,
@@ -41,8 +50,16 @@ class LoggingLevels(enum.IntEnum):
41
50
"CRITICAL" : "CRITICAL" ,
42
51
}
43
52
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
+ }
46
63
47
64
48
65
class LoguruIntegration (Integration ):
@@ -52,19 +69,22 @@ class LoguruIntegration(Integration):
52
69
event_level = DEFAULT_EVENT_LEVEL # type: Optional[int]
53
70
breadcrumb_format = DEFAULT_FORMAT
54
71
event_format = DEFAULT_FORMAT
72
+ sentry_logs_level = DEFAULT_LEVEL # type: Optional[int]
55
73
56
74
def __init__ (
57
75
self ,
58
76
level = DEFAULT_LEVEL ,
59
77
event_level = DEFAULT_EVENT_LEVEL ,
60
78
breadcrumb_format = DEFAULT_FORMAT ,
61
79
event_format = DEFAULT_FORMAT ,
80
+ sentry_logs_level = DEFAULT_LEVEL ,
62
81
):
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
64
83
LoguruIntegration .level = level
65
84
LoguruIntegration .event_level = event_level
66
85
LoguruIntegration .breadcrumb_format = breadcrumb_format
67
86
LoguruIntegration .event_format = event_format
87
+ LoguruIntegration .sentry_logs_level = sentry_logs_level
68
88
69
89
@staticmethod
70
90
def setup_once ():
@@ -83,8 +103,23 @@ def setup_once():
83
103
format = LoguruIntegration .event_format ,
84
104
)
85
105
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
+
86
112
87
113
class _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
+
88
123
def _logging_to_event_level (self , record ):
89
124
# type: (LogRecord) -> str
90
125
try :
@@ -98,24 +133,72 @@ def _logging_to_event_level(self, record):
98
133
class LoguruEventHandler (_LoguruBaseHandler , EventHandler ):
99
134
"""Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names."""
100
135
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
109
137
110
138
111
139
class LoguruBreadcrumbHandler (_LoguruBaseHandler , BreadcrumbHandler ):
112
140
"""Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names."""
113
141
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
120
143
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