Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(logs): Add alpha version of Sentry logs #4126

Merged
merged 31 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4a34301
feat(ourlogs): Add alpha version of logger
colin-sentry Mar 10, 2025
a94de84
circular import, etc
colin-sentry Mar 10, 2025
aa54b5c
camel the item
colin-sentry Mar 10, 2025
26a1fe4
mypy
colin-sentry Mar 10, 2025
ba7db76
mypy
colin-sentry Mar 10, 2025
0da4340
move to experimental
colin-sentry Mar 10, 2025
e8a1c08
mypy
colin-sentry Mar 10, 2025
24ae29a
mypy
colin-sentry Mar 10, 2025
41d39e9
mypu
colin-sentry Mar 11, 2025
bef8fff
rename before_send_log, handle parent_span_id
colin-sentry Mar 11, 2025
e2d39f0
whitespace
antonpirker Mar 13, 2025
d1fce6c
Added some basic unit tests
antonpirker Mar 13, 2025
5891202
explainational text
antonpirker Mar 13, 2025
bad1974
the last assert
antonpirker Mar 13, 2025
92b7e6d
Logs tied to transactions/spans
antonpirker Mar 13, 2025
176f45e
Added todos
antonpirker Mar 13, 2025
42f1fb8
Fixed boolValue
antonpirker Mar 13, 2025
1be2ce3
typo
antonpirker Mar 13, 2025
688c77e
typo
antonpirker Mar 13, 2025
dfa1618
Merge branch 'master' into logging_integration
antonpirker Mar 13, 2025
017251e
Merge branch 'master' into logging_integration
antonpirker Mar 14, 2025
88a7780
renamed file
antonpirker Mar 14, 2025
d90a2da
Mark capture_log as private
antonpirker Mar 14, 2025
cccce0f
Move config params into _experimental
antonpirker Mar 14, 2025
7472d5f
Cleaned up tests
antonpirker Mar 14, 2025
712eb5b
Format tests
antonpirker Mar 14, 2025
e8168fe
Linting
antonpirker Mar 14, 2025
4dd9854
Merge branch 'master' into logging_integration
antonpirker Mar 14, 2025
194b939
If debug is enabled, also print logs to console
antonpirker Mar 14, 2025
96b3a13
Skip the tests below Python 3.7
antonpirker Mar 14, 2025
1906ab4
pytest skipping tests
antonpirker Mar 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sentry_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"start_transaction",
"trace",
"monitor",
"_experimental_logger.py",
]

# Initialize the debug support after everything is loaded
Expand Down
20 changes: 20 additions & 0 deletions sentry_sdk/_experimental_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# NOTE: this is the logger sentry exposes to users, not some generic logger.
import functools
from typing import Any

from sentry_sdk import get_client, get_current_scope


def _capture_log(severity_text, severity_number, template, **kwargs):
# type: (str, int, str, **Any) -> None
client = get_client()
scope = get_current_scope()
client.capture_log(scope, severity_text, severity_number, template, **kwargs)


trace = functools.partial(_capture_log, "trace", 1)
debug = functools.partial(_capture_log, "debug", 5)
info = functools.partial(_capture_log, "info", 9)
warn = functools.partial(_capture_log, "warn", 13)
error = functools.partial(_capture_log, "error", 17)
fatal = functools.partial(_capture_log, "fatal", 21)
13 changes: 13 additions & 0 deletions sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,17 @@ class SDKInfo(TypedDict):
]

Hint = Dict[str, Any]
Log = TypedDict(
"Log",
{
"severity_text": str,
"severity_number": int,
"body": str,
"attributes": dict[str, str | bool | float | int],
"time_unix_nano": int,
"trace_id": Optional[str],
},
)

Breadcrumb = Dict[str, Any]
BreadcrumbHint = Dict[str, Any]
Expand All @@ -217,6 +228,7 @@ class SDKInfo(TypedDict):
ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]]
BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]]
TransactionProcessor = Callable[[Event, Hint], Optional[Event]]
LogProcessor = Callable[[Log, Hint], Optional[Log]]

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

Expand All @@ -237,6 +249,7 @@ class SDKInfo(TypedDict):
"metric_bucket",
"monitor",
"span",
"log",
]
SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]

Expand Down
113 changes: 112 additions & 1 deletion sentry_sdk/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import json
import os
import time
import uuid
import random
import socket
import logging
from collections.abc import Mapping
from datetime import datetime, timezone
from importlib import import_module
Expand Down Expand Up @@ -55,7 +58,7 @@
from typing import Union
from typing import TypeVar

from sentry_sdk._types import Event, Hint, SDKInfo
from sentry_sdk._types import Event, Hint, SDKInfo, Log
from sentry_sdk.integrations import Integration
from sentry_sdk.metrics import MetricsAggregator
from sentry_sdk.scope import Scope
Expand Down Expand Up @@ -206,6 +209,10 @@ def capture_event(self, *args, **kwargs):
# type: (*Any, **Any) -> Optional[str]
return None

def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
# type: (Scope, str, int, str, **Any) -> None
pass

def capture_session(self, *args, **kwargs):
# type: (*Any, **Any) -> None
return None
Expand Down Expand Up @@ -847,6 +854,110 @@ def capture_event(

return return_value

def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
# type: (Scope, str, int, str, **Any) -> None
logs_enabled = self.options["_experiments"].get("enable_sentry_logs", False)
if not logs_enabled:
return

headers = {
"sent_at": format_timestamp(datetime.now(timezone.utc)),
} # type: dict[str, object]

attrs = {
"sentry.message.template": template,
} # type: dict[str, str | bool | float | int]

kwargs_attributes = kwargs.get("attributes")
if kwargs_attributes is not None:
attrs.update(kwargs_attributes)

environment = self.options.get("environment")
if environment is not None:
attrs["sentry.environment"] = environment

release = self.options.get("release")
if release is not None:
attrs["sentry.release"] = release

span = scope.span
if span is not None:
attrs["sentry.trace.parent_span_id"] = span.span_id

for k, v in kwargs.items():
attrs[f"sentry.message.parameters.{k}"] = v

log = {
"severity_text": severity_text,
"severity_number": severity_number,
"body": template.format(**kwargs),
"attributes": attrs,
"time_unix_nano": time.time_ns(),
"trace_id": None,
} # type: Log

# If debug is enabled, log the log to the console
debug = self.options.get("debug", False)
if debug:
severity_text_to_logging_level = {
"trace": logging.DEBUG,
"debug": logging.DEBUG,
"info": logging.INFO,
"warn": logging.WARNING,
"error": logging.ERROR,
"fatal": logging.CRITICAL,
}
logger.log(
severity_text_to_logging_level.get(severity_text, logging.DEBUG),
f'[Sentry Logs] {log["body"]}',
)

propagation_context = scope.get_active_propagation_context()
if propagation_context is not None:
headers["trace_id"] = propagation_context.trace_id
log["trace_id"] = propagation_context.trace_id

envelope = Envelope(headers=headers)

before_emit_log = self.options["_experiments"].get("before_emit_log")
if before_emit_log is not None:
log = before_emit_log(log, {})
if log is None:
return

def format_attribute(key, val):
# type: (str, int | float | str | bool) -> Any
if isinstance(val, bool):
return {"key": key, "value": {"boolValue": val}}
if isinstance(val, int):
return {"key": key, "value": {"intValue": str(val)}}
if isinstance(val, float):
return {"key": key, "value": {"doubleValue": val}}
if isinstance(val, str):
return {"key": key, "value": {"stringValue": val}}
return {"key": key, "value": {"stringValue": json.dumps(val)}}

otel_log = {
"severityText": log["severity_text"],
"severityNumber": log["severity_number"],
"body": {"stringValue": log["body"]},
"timeUnixNano": str(log["time_unix_nano"]),
"attributes": [
format_attribute(k, v) for (k, v) in log["attributes"].items()
],
}

if "trace_id" in log:
otel_log["traceId"] = log["trace_id"]

envelope.add_log(otel_log) # TODO: batch these

if self.spotlight:
self.spotlight.capture_envelope(envelope)

if self.transport is not None:
self.transport.capture_envelope(envelope)

def capture_session(
self, session # type: Session
):
Expand Down
8 changes: 8 additions & 0 deletions sentry_sdk/envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ def add_sessions(
# type: (...) -> None
self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))

def add_log(
self, log # type: Any
):
# type: (...) -> None
self.add_item(Item(payload=PayloadRef(json=log), type="otel_log"))

def add_item(
self, item # type: Item
):
Expand Down Expand Up @@ -268,6 +274,8 @@ def data_category(self):
return "transaction"
elif ty == "event":
return "error"
elif ty == "otel_log":
return "log"
elif ty == "client_report":
return "internal"
elif ty == "profile":
Expand Down
Loading
Loading