Skip to content

Commit 486d733

Browse files
feat(logs): Add alpha version of Sentry logs (#4126)
Logs are coming to sentry! This commit: - Adds `sentry_sdk._experimental_logger.{info, warn, ...}` methods - Adds `_experimental` options for `before_send_log` and `enable_sentry_logs` There are no tests (yet), and this still uses the otel_log schema. Example usage: ```python sentry_sdk.init( dsn=..., _experiments={"enable_sentry_logs": True}, ) from sentry_sdk import _experimental_logger as sentry_logger sentry_logger.info('Finished sending answer! #chunks={num_chunks}', num_chunks=10) ``` --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent 380e32f commit 486d733

File tree

6 files changed

+396
-1
lines changed

6 files changed

+396
-1
lines changed

sentry_sdk/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"start_transaction",
4646
"trace",
4747
"monitor",
48+
"_experimental_logger.py",
4849
]
4950

5051
# Initialize the debug support after everything is loaded

sentry_sdk/_experimental_logger.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# NOTE: this is the logger sentry exposes to users, not some generic logger.
2+
import functools
3+
from typing import Any
4+
5+
from sentry_sdk import get_client, get_current_scope
6+
7+
8+
def _capture_log(severity_text, severity_number, template, **kwargs):
9+
# type: (str, int, str, **Any) -> None
10+
client = get_client()
11+
scope = get_current_scope()
12+
client.capture_log(scope, severity_text, severity_number, template, **kwargs)
13+
14+
15+
trace = functools.partial(_capture_log, "trace", 1)
16+
debug = functools.partial(_capture_log, "debug", 5)
17+
info = functools.partial(_capture_log, "info", 9)
18+
warn = functools.partial(_capture_log, "warn", 13)
19+
error = functools.partial(_capture_log, "error", 17)
20+
fatal = functools.partial(_capture_log, "fatal", 21)

sentry_sdk/_types.py

+13
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,17 @@ class SDKInfo(TypedDict):
207207
]
208208

209209
Hint = Dict[str, Any]
210+
Log = TypedDict(
211+
"Log",
212+
{
213+
"severity_text": str,
214+
"severity_number": int,
215+
"body": str,
216+
"attributes": dict[str, str | bool | float | int],
217+
"time_unix_nano": int,
218+
"trace_id": Optional[str],
219+
},
220+
)
210221

211222
Breadcrumb = Dict[str, Any]
212223
BreadcrumbHint = Dict[str, Any]
@@ -217,6 +228,7 @@ class SDKInfo(TypedDict):
217228
ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]]
218229
BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]]
219230
TransactionProcessor = Callable[[Event, Hint], Optional[Event]]
231+
LogProcessor = Callable[[Log, Hint], Optional[Log]]
220232

221233
TracesSampler = Callable[[SamplingContext], Union[float, int, bool]]
222234

@@ -237,6 +249,7 @@ class SDKInfo(TypedDict):
237249
"metric_bucket",
238250
"monitor",
239251
"span",
252+
"log",
240253
]
241254
SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
242255

sentry_sdk/client.py

+112-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import json
12
import os
3+
import time
24
import uuid
35
import random
46
import socket
7+
import logging
58
from collections.abc import Mapping
69
from datetime import datetime, timezone
710
from importlib import import_module
@@ -55,7 +58,7 @@
5558
from typing import Union
5659
from typing import TypeVar
5760

58-
from sentry_sdk._types import Event, Hint, SDKInfo
61+
from sentry_sdk._types import Event, Hint, SDKInfo, Log
5962
from sentry_sdk.integrations import Integration
6063
from sentry_sdk.metrics import MetricsAggregator
6164
from sentry_sdk.scope import Scope
@@ -206,6 +209,10 @@ def capture_event(self, *args, **kwargs):
206209
# type: (*Any, **Any) -> Optional[str]
207210
return None
208211

212+
def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
213+
# type: (Scope, str, int, str, **Any) -> None
214+
pass
215+
209216
def capture_session(self, *args, **kwargs):
210217
# type: (*Any, **Any) -> None
211218
return None
@@ -847,6 +854,110 @@ def capture_event(
847854

848855
return return_value
849856

857+
def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
858+
# type: (Scope, str, int, str, **Any) -> None
859+
logs_enabled = self.options["_experiments"].get("enable_sentry_logs", False)
860+
if not logs_enabled:
861+
return
862+
863+
headers = {
864+
"sent_at": format_timestamp(datetime.now(timezone.utc)),
865+
} # type: dict[str, object]
866+
867+
attrs = {
868+
"sentry.message.template": template,
869+
} # type: dict[str, str | bool | float | int]
870+
871+
kwargs_attributes = kwargs.get("attributes")
872+
if kwargs_attributes is not None:
873+
attrs.update(kwargs_attributes)
874+
875+
environment = self.options.get("environment")
876+
if environment is not None:
877+
attrs["sentry.environment"] = environment
878+
879+
release = self.options.get("release")
880+
if release is not None:
881+
attrs["sentry.release"] = release
882+
883+
span = scope.span
884+
if span is not None:
885+
attrs["sentry.trace.parent_span_id"] = span.span_id
886+
887+
for k, v in kwargs.items():
888+
attrs[f"sentry.message.parameters.{k}"] = v
889+
890+
log = {
891+
"severity_text": severity_text,
892+
"severity_number": severity_number,
893+
"body": template.format(**kwargs),
894+
"attributes": attrs,
895+
"time_unix_nano": time.time_ns(),
896+
"trace_id": None,
897+
} # type: Log
898+
899+
# If debug is enabled, log the log to the console
900+
debug = self.options.get("debug", False)
901+
if debug:
902+
severity_text_to_logging_level = {
903+
"trace": logging.DEBUG,
904+
"debug": logging.DEBUG,
905+
"info": logging.INFO,
906+
"warn": logging.WARNING,
907+
"error": logging.ERROR,
908+
"fatal": logging.CRITICAL,
909+
}
910+
logger.log(
911+
severity_text_to_logging_level.get(severity_text, logging.DEBUG),
912+
f'[Sentry Logs] {log["body"]}',
913+
)
914+
915+
propagation_context = scope.get_active_propagation_context()
916+
if propagation_context is not None:
917+
headers["trace_id"] = propagation_context.trace_id
918+
log["trace_id"] = propagation_context.trace_id
919+
920+
envelope = Envelope(headers=headers)
921+
922+
before_emit_log = self.options["_experiments"].get("before_emit_log")
923+
if before_emit_log is not None:
924+
log = before_emit_log(log, {})
925+
if log is None:
926+
return
927+
928+
def format_attribute(key, val):
929+
# type: (str, int | float | str | bool) -> Any
930+
if isinstance(val, bool):
931+
return {"key": key, "value": {"boolValue": val}}
932+
if isinstance(val, int):
933+
return {"key": key, "value": {"intValue": str(val)}}
934+
if isinstance(val, float):
935+
return {"key": key, "value": {"doubleValue": val}}
936+
if isinstance(val, str):
937+
return {"key": key, "value": {"stringValue": val}}
938+
return {"key": key, "value": {"stringValue": json.dumps(val)}}
939+
940+
otel_log = {
941+
"severityText": log["severity_text"],
942+
"severityNumber": log["severity_number"],
943+
"body": {"stringValue": log["body"]},
944+
"timeUnixNano": str(log["time_unix_nano"]),
945+
"attributes": [
946+
format_attribute(k, v) for (k, v) in log["attributes"].items()
947+
],
948+
}
949+
950+
if "trace_id" in log:
951+
otel_log["traceId"] = log["trace_id"]
952+
953+
envelope.add_log(otel_log) # TODO: batch these
954+
955+
if self.spotlight:
956+
self.spotlight.capture_envelope(envelope)
957+
958+
if self.transport is not None:
959+
self.transport.capture_envelope(envelope)
960+
850961
def capture_session(
851962
self, session # type: Session
852963
):

sentry_sdk/envelope.py

+8
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ def add_sessions(
102102
# type: (...) -> None
103103
self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
104104

105+
def add_log(
106+
self, log # type: Any
107+
):
108+
# type: (...) -> None
109+
self.add_item(Item(payload=PayloadRef(json=log), type="otel_log"))
110+
105111
def add_item(
106112
self, item # type: Item
107113
):
@@ -268,6 +274,8 @@ def data_category(self):
268274
return "transaction"
269275
elif ty == "event":
270276
return "error"
277+
elif ty == "otel_log":
278+
return "log"
271279
elif ty == "client_report":
272280
return "internal"
273281
elif ty == "profile":

0 commit comments

Comments
 (0)