Skip to content

Commit 673c34c

Browse files
committed
Merge branch 'master' into potel-base
2 parents 6ad4031 + 51db87c commit 673c34c

File tree

9 files changed

+747
-253
lines changed

9 files changed

+747
-253
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ for your feedback. How was the migration? Is everything working as expected? Is
142142
sentry_sdk.init(
143143
dsn="...",
144144
_experiments={
145-
"enable_sentry_logs": True
145+
"enable_logs": True
146146
}
147147
integrations=[
148148
LoggingIntegration(sentry_logs_level=logging.ERROR),

sentry_sdk/client.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from importlib import import_module
88
from typing import TYPE_CHECKING, List, Dict, cast, overload
99

10+
import sentry_sdk
1011
from sentry_sdk._compat import check_uwsgi_thread_support
1112
from sentry_sdk.utils import (
1213
AnnotatedValue,
@@ -190,8 +191,8 @@ def capture_event(self, *args, **kwargs):
190191
# type: (*Any, **Any) -> Optional[str]
191192
return None
192193

193-
def _capture_experimental_log(self, scope, log):
194-
# type: (Scope, Log) -> None
194+
def _capture_experimental_log(self, log):
195+
# type: (Log) -> None
195196
pass
196197

197198
def capture_session(self, *args, **kwargs):
@@ -846,12 +847,14 @@ def capture_event(
846847

847848
return return_value
848849

849-
def _capture_experimental_log(self, current_scope, log):
850-
# type: (Scope, Log) -> None
850+
def _capture_experimental_log(self, log):
851+
# type: (Log) -> None
851852
logs_enabled = self.options["_experiments"].get("enable_logs", False)
852853
if not logs_enabled:
853854
return
854-
isolation_scope = current_scope.get_isolation_scope()
855+
856+
current_scope = sentry_sdk.get_current_scope()
857+
isolation_scope = sentry_sdk.get_isolation_scope()
855858

856859
log["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
857860
log["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
@@ -880,6 +883,21 @@ def _capture_experimental_log(self, current_scope, log):
880883
elif propagation_context is not None:
881884
log["trace_id"] = propagation_context.trace_id
882885

886+
# The user, if present, is always set on the isolation scope.
887+
if isolation_scope._user is not None:
888+
for log_attribute, user_attribute in (
889+
("user.id", "id"),
890+
("user.name", "username"),
891+
("user.email", "email"),
892+
):
893+
if (
894+
user_attribute in isolation_scope._user
895+
and log_attribute not in log["attributes"]
896+
):
897+
log["attributes"][log_attribute] = isolation_scope._user[
898+
user_attribute
899+
]
900+
883901
# If debug is enabled, log the log to the console
884902
debug = self.options.get("debug", False)
885903
if debug:

sentry_sdk/integrations/logging.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import sentry_sdk
77
from sentry_sdk.client import BaseClient
8+
from sentry_sdk.logger import _log_level_to_otel
89
from sentry_sdk.utils import (
910
safe_repr,
1011
to_string,
@@ -14,7 +15,7 @@
1415
)
1516
from sentry_sdk.integrations import Integration
1617

17-
from typing import TYPE_CHECKING, Tuple
18+
from typing import TYPE_CHECKING
1819

1920
if TYPE_CHECKING:
2021
from collections.abc import MutableMapping
@@ -36,6 +37,16 @@
3637
logging.CRITICAL: "fatal", # CRITICAL is same as FATAL
3738
}
3839

40+
# Map logging level numbers to corresponding OTel level numbers
41+
SEVERITY_TO_OTEL_SEVERITY = {
42+
logging.CRITICAL: 21, # fatal
43+
logging.ERROR: 17, # error
44+
logging.WARNING: 13, # warn
45+
logging.INFO: 9, # info
46+
logging.DEBUG: 5, # debug
47+
}
48+
49+
3950
# Capturing events from those loggers causes recursion errors. We cannot allow
4051
# the user to unconditionally create events from those loggers under any
4152
# circumstances.
@@ -124,7 +135,10 @@ def sentry_patched_callhandlers(self, record):
124135
# the integration. Otherwise we have a high chance of getting
125136
# into a recursion error when the integration is resolved
126137
# (this also is slower).
127-
if ignored_loggers is not None and record.name not in ignored_loggers:
138+
if (
139+
ignored_loggers is not None
140+
and record.name.strip() not in ignored_loggers
141+
):
128142
integration = sentry_sdk.get_client().get_integration(
129143
LoggingIntegration
130144
)
@@ -169,7 +183,7 @@ def _can_record(self, record):
169183
# type: (LogRecord) -> bool
170184
"""Prevents ignored loggers from recording"""
171185
for logger in _IGNORED_LOGGERS:
172-
if fnmatch(record.name, logger):
186+
if fnmatch(record.name.strip(), logger):
173187
return False
174188
return True
175189

@@ -317,21 +331,6 @@ def _breadcrumb_from_record(self, record):
317331
}
318332

319333

320-
def _python_level_to_otel(record_level):
321-
# type: (int) -> Tuple[int, str]
322-
for py_level, otel_severity_number, otel_severity_text in [
323-
(50, 21, "fatal"),
324-
(40, 17, "error"),
325-
(30, 13, "warn"),
326-
(20, 9, "info"),
327-
(10, 5, "debug"),
328-
(5, 1, "trace"),
329-
]:
330-
if record_level >= py_level:
331-
return otel_severity_number, otel_severity_text
332-
return 0, "default"
333-
334-
335334
class SentryLogsHandler(_BaseHandler):
336335
"""
337336
A logging handler that records Sentry logs for each Python log record.
@@ -357,8 +356,9 @@ def emit(self, record):
357356

358357
def _capture_log_from_record(self, client, record):
359358
# type: (BaseClient, LogRecord) -> None
360-
scope = sentry_sdk.get_current_scope()
361-
otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno)
359+
otel_severity_number, otel_severity_text = _log_level_to_otel(
360+
record.levelno, SEVERITY_TO_OTEL_SEVERITY
361+
)
362362
project_root = client.options["project_root"]
363363
attrs = self._extra_from_record(record) # type: Any
364364
attrs["sentry.origin"] = "auto.logger.log"
@@ -369,10 +369,7 @@ def _capture_log_from_record(self, client, record):
369369
for i, arg in enumerate(record.args):
370370
attrs[f"sentry.message.parameter.{i}"] = (
371371
arg
372-
if isinstance(arg, str)
373-
or isinstance(arg, float)
374-
or isinstance(arg, int)
375-
or isinstance(arg, bool)
372+
if isinstance(arg, (str, float, int, bool))
376373
else safe_repr(arg)
377374
)
378375
if record.lineno:
@@ -399,7 +396,6 @@ def _capture_log_from_record(self, client, record):
399396

400397
# noinspection PyProtectedMember
401398
client._capture_experimental_log(
402-
scope,
403399
{
404400
"severity_text": otel_severity_text,
405401
"severity_number": otel_severity_number,

sentry_sdk/integrations/loguru.py

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
import enum
22

3+
import sentry_sdk
34
from sentry_sdk.integrations import Integration, DidNotEnable
45
from sentry_sdk.integrations.logging import (
56
BreadcrumbHandler,
67
EventHandler,
78
_BaseHandler,
89
)
10+
from sentry_sdk.logger import _log_level_to_otel
911

1012
from typing import TYPE_CHECKING
1113

1214
if TYPE_CHECKING:
1315
from logging import LogRecord
14-
from typing import Optional, Any
16+
from typing import Any, Optional
1517

1618
try:
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
2025
except 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+
3443
SENTRY_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

4865
class 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

87113
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+
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):
98133
class 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

111139
class 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

Comments
 (0)