Skip to content

Commit ae03a35

Browse files
committed
feat(ourlogs): Make the python logger go to ourlogs
1 parent 5dcda1d commit ae03a35

File tree

4 files changed

+113
-8
lines changed

4 files changed

+113
-8
lines changed

sentry_sdk/_experimental_logger.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ def _capture_log(severity_text, severity_number, template, **kwargs):
99
# type: (str, int, str, **Any) -> None
1010
client = get_client()
1111
scope = get_current_scope()
12-
client.capture_log(scope, severity_text, severity_number, template, **kwargs)
12+
kwargs["sentry.message.template"] = template
13+
client.capture_log(
14+
scope, severity_text, severity_number, template.format(**kwargs), **kwargs
15+
)
1316

1417

1518
trace = functools.partial(_capture_log, "trace", 1)

sentry_sdk/client.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,9 @@ def capture_event(self, *args, **kwargs):
209209
# type: (*Any, **Any) -> Optional[str]
210210
return None
211211

212-
def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
212+
def _capture_experimental_log(
213+
self, scope, severity_text, severity_number, body, **kwargs
214+
):
213215
# type: (Scope, str, int, str, **Any) -> None
214216
pass
215217

@@ -854,7 +856,9 @@ def capture_event(
854856

855857
return return_value
856858

857-
def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
859+
def _capture_experimental_log(
860+
self, scope, severity_text, severity_number, body, **kwargs
861+
):
858862
# type: (Scope, str, int, str, **Any) -> None
859863
logs_enabled = self.options["_experiments"].get("enable_sentry_logs", False)
860864
if not logs_enabled:
@@ -864,9 +868,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs)
864868
"sent_at": format_timestamp(datetime.now(timezone.utc)),
865869
} # type: dict[str, object]
866870

867-
attrs = {
868-
"sentry.message.template": template,
869-
} # type: dict[str, str | bool | float | int]
871+
attrs = {} # type: dict[str, str | bool | float | int]
870872

871873
kwargs_attributes = kwargs.get("attributes")
872874
if kwargs_attributes is not None:
@@ -890,9 +892,9 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs)
890892
log = {
891893
"severity_text": severity_text,
892894
"severity_number": severity_number,
893-
"body": template.format(**kwargs),
895+
"body": body,
894896
"attributes": attrs,
895-
"time_unix_nano": time.time_ns(),
897+
"time_unix_nano": kwargs.pop("time_unix_nano", time.time_ns()),
896898
"trace_id": None,
897899
} # type: Log
898900

@@ -907,6 +909,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs)
907909
"error": logging.ERROR,
908910
"fatal": logging.CRITICAL,
909911
}
912+
# Be careful editing this line, you can add infinite logging loops with the logger integration
910913
logger.log(
911914
severity_text_to_logging_level.get(severity_text, logging.DEBUG),
912915
f'[Sentry Logs] {log["body"]}',

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class CompressionAlgo(Enum):
7878
Callable[[str, MetricValue, MeasurementUnit, MetricTags], bool]
7979
],
8080
"metric_code_locations": Optional[bool],
81+
"enable_sentry_logs": Optional[bool],
8182
},
8283
total=False,
8384
)

sentry_sdk/integrations/logging.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import json
12
import logging
23
from datetime import datetime, timezone
34
from fnmatch import fnmatch
45

56
import sentry_sdk
7+
from sentry_sdk.client import BaseClient
68
from 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

Comments
 (0)