From 4a34301562a1924bb10d904037e6ac9beb707865 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 14:59:09 -0400 Subject: [PATCH 01/28] feat(ourlogs): Add alpha version of logger --- sentry_sdk/__init__.py | 1 + sentry_sdk/_types.py | 2 ++ sentry_sdk/client.py | 70 ++++++++++++++++++++++++++++++++++++++++++ sentry_sdk/consts.py | 3 ++ sentry_sdk/envelope.py | 8 ++++- sentry_sdk/logger.py | 19 ++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 sentry_sdk/logger.py diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 1c9cedec5f..b4859cc5d2 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -45,6 +45,7 @@ "start_transaction", "trace", "monitor", + "logger", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 883b4cbc81..5cb48903ab 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -207,6 +207,7 @@ class SDKInfo(TypedDict): ] Hint = Dict[str, Any] + Log = Dict[str, Any] Breadcrumb = Dict[str, Any] BreadcrumbHint = Dict[str, Any] @@ -217,6 +218,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]] diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 4f5c1566b3..caf52933b9 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -1,4 +1,6 @@ +import json import os +import time import uuid import random import socket @@ -8,6 +10,7 @@ from typing import TYPE_CHECKING, List, Dict, cast, overload import warnings +from sentry_sdk import get_current_scope from sentry_sdk._compat import PY37, check_uwsgi_thread_support from sentry_sdk.utils import ( AnnotatedValue, @@ -206,6 +209,10 @@ def capture_event(self, *args, **kwargs): # type: (*Any, **Any) -> Optional[str] return None + def capture_log(self, severity_text, severity_number, template, **kwargs): + # type: (str, int, str, **Any) -> None + pass + def capture_session(self, *args, **kwargs): # type: (*Any, **Any) -> None return None @@ -847,6 +854,69 @@ def capture_event( return return_value + def capture_log(self, severity_text, severity_number, template, **kwargs): + # type: (str, int, str, **Any) -> None + if not self.options.get("enable_sentry_logs", False): + return + + headers = { + "sent_at": format_timestamp(datetime.now(timezone.utc)), + } # type: dict[str, object] + + def format_attribute(key: str, val: int | float | str | bool): + if isinstance(val, int): + return {"key": key, "value": {"int_value": val}} + if isinstance(val, str): + return {"key": key, "value": {"string_value": val}} + if isinstance(val, float): + return {"key": key, "value": {"double_value": val}} + if isinstance(val, bool): + return {"key": key, "value": {"bool_value": val}} + return {"key": key, "value": {"string_value": json.dumps(val)}} + + attrs = { + "sentry.message.template": template, + } + if (extra_attrs := kwargs.get("attributes")) is not None: + attrs.update(extra_attrs) + if (env := self.options.get("environment")) is not None: + attrs["sentry.environment"] = env + if (release := self.options.get("release")) is not None: + attrs["sentry.release"] = release + 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(), + } + + if (ctx := get_current_scope().get_active_propagation_context()) is not None: + headers["trace_id"] = ctx.trace_id + log["trace_id"] = ctx.trace_id + envelope = Envelope(headers=headers) + + before_send_log = self.options.get("before_send_log") + if before_send_log is not None: + log = before_send_log(log) + + # convert to otel form - otel_log has a different schema than just 'log' + log["body"] = {"string_value": log["body"]} + log["attributes"] = [ + format_attribute(k, v) for (k, v) in log["attributes"].items() + ] + + envelope.add_log(log) # TODO: batch these + if self.spotlight: + self.spotlight.capture_envelope(envelope) + + if self.transport is not None: + self.transport.capture_envelope(envelope) + return None + def capture_session( self, session # type: Session ): diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 20179e2231..cf826c25bb 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -53,6 +53,7 @@ class CompressionAlgo(Enum): TransactionProcessor, MetricTags, MetricValue, + LogProcessor, ) # Experiments are feature flags to enable and disable certain unstable SDK @@ -539,6 +540,8 @@ def __init__( proxy_headers=None, # type: Optional[Dict[str, str]] instrumenter=INSTRUMENTER.SENTRY, # type: Optional[str] before_send_transaction=None, # type: Optional[TransactionProcessor] + enable_sentry_logs=False, # type: bool + before_send_log=None, # type: Optional[LogProcessor] project_root=None, # type: Optional[str] enable_tracing=None, # type: Optional[bool] include_local_variables=True, # type: Optional[bool] diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 760116daa1..0f3c00d79d 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -15,7 +15,7 @@ from typing import List from typing import Iterator - from sentry_sdk._types import Event, EventDataCategory + from sentry_sdk._types import Event, EventDataCategory, Log def parse_json(data): @@ -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: Log + ): + # type: (...) -> None + self.add_item(Item(payload=PayloadRef(json=log), type="otel_log")) + def add_item( self, item # type: Item ): diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py new file mode 100644 index 0000000000..c2f934cdcf --- /dev/null +++ b/sentry_sdk/logger.py @@ -0,0 +1,19 @@ +# NOTE: this is the logger sentry exposes to users, not some generic logger. +import functools +from typing import Any, Optional + +from sentry_sdk import get_client + + +def capture_log(severity_text, severity_number, template, **kwargs): + # type: (str, int, str, **Any) -> Optional[str] + client = get_client() + return client.capture_log(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) From a94de84d64455a4054ca8107cca7e52eac9c2bcb Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 15:14:14 -0400 Subject: [PATCH 02/28] circular import, etc --- sentry_sdk/_types.py | 1 + sentry_sdk/client.py | 11 +++++------ sentry_sdk/envelope.py | 2 ++ sentry_sdk/logger.py | 5 +++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 5cb48903ab..7ae81b665e 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -239,6 +239,7 @@ class SDKInfo(TypedDict): "metric_bucket", "monitor", "span", + "log", ] SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index caf52933b9..372511bba8 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, List, Dict, cast, overload import warnings -from sentry_sdk import get_current_scope from sentry_sdk._compat import PY37, check_uwsgi_thread_support from sentry_sdk.utils import ( AnnotatedValue, @@ -209,8 +208,8 @@ def capture_event(self, *args, **kwargs): # type: (*Any, **Any) -> Optional[str] return None - def capture_log(self, severity_text, severity_number, template, **kwargs): - # type: (str, int, str, **Any) -> 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): @@ -854,8 +853,8 @@ def capture_event( return return_value - def capture_log(self, severity_text, severity_number, template, **kwargs): - # type: (str, int, str, **Any) -> None + def capture_log(self, scope, severity_text, severity_number, template, **kwargs): + # type: (Scope, str, int, str, **Any) -> None if not self.options.get("enable_sentry_logs", False): return @@ -894,7 +893,7 @@ def format_attribute(key: str, val: int | float | str | bool): "time_unix_nano": time.time_ns(), } - if (ctx := get_current_scope().get_active_propagation_context()) is not None: + if (ctx := scope.get_active_propagation_context()) is not None: headers["trace_id"] = ctx.trace_id log["trace_id"] = ctx.trace_id envelope = Envelope(headers=headers) diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 0f3c00d79d..522073f5b3 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -274,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": diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index c2f934cdcf..7abc202f5d 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -2,13 +2,14 @@ import functools from typing import Any, Optional -from sentry_sdk import get_client +from sentry_sdk import get_client, get_current_scope def capture_log(severity_text, severity_number, template, **kwargs): # type: (str, int, str, **Any) -> Optional[str] client = get_client() - return client.capture_log(severity_text, severity_number, template, **kwargs) + scope = get_current_scope() + return client.capture_log(scope, severity_text, severity_number, template, **kwargs) trace = functools.partial(capture_log, "trace", 1) From aa54b5cec6572a265db2a40f422d00e149c163e8 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 16:07:33 -0400 Subject: [PATCH 03/28] camel the item --- sentry_sdk/_types.py | 12 +++++++++- sentry_sdk/client.py | 50 +++++++++++++++++++++++++----------------- sentry_sdk/envelope.py | 4 ++-- sentry_sdk/logger.py | 6 ++--- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 7ae81b665e..bc730719d2 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -207,7 +207,17 @@ class SDKInfo(TypedDict): ] Hint = Dict[str, Any] - Log = 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] diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 372511bba8..e764aee7e0 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -57,7 +57,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 @@ -862,17 +862,6 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) "sent_at": format_timestamp(datetime.now(timezone.utc)), } # type: dict[str, object] - def format_attribute(key: str, val: int | float | str | bool): - if isinstance(val, int): - return {"key": key, "value": {"int_value": val}} - if isinstance(val, str): - return {"key": key, "value": {"string_value": val}} - if isinstance(val, float): - return {"key": key, "value": {"double_value": val}} - if isinstance(val, bool): - return {"key": key, "value": {"bool_value": val}} - return {"key": key, "value": {"string_value": json.dumps(val)}} - attrs = { "sentry.message.template": template, } @@ -885,6 +874,7 @@ def format_attribute(key: str, val: int | float | str | bool): for k, v in kwargs.items(): attrs[f"sentry.message.parameters.{k}"] = v + # type: Log log = { "severity_text": severity_text, "severity_number": severity_number, @@ -901,20 +891,40 @@ def format_attribute(key: str, val: int | float | str | bool): before_send_log = self.options.get("before_send_log") if before_send_log is not None: log = before_send_log(log) + if log is None: + return - # convert to otel form - otel_log has a different schema than just 'log' - log["body"] = {"string_value": log["body"]} - log["attributes"] = [ - format_attribute(k, v) for (k, v) in log["attributes"].items() - ] - - envelope.add_log(log) # TODO: batch these + def format_attribute(key, val): + # type: (str, int | float | str | bool) -> Any + if isinstance(val, int): + return {"key": key, "value": {"intValue": val}} + if isinstance(val, str): + return {"key": key, "value": {"stringValue": val}} + if isinstance(val, float): + return {"key": key, "value": {"doubleValue": val}} + if isinstance(val, bool): + return {"key": key, "value": {"boolValue": 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"]), + } + if isinstance((attrs := log["attributes"]), dict): + otel_log["attributes"] = [ + format_attribute(k, v) for (k, v) in attrs.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) - return None def capture_session( self, session # type: Session diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 522073f5b3..5f61e689c5 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -15,7 +15,7 @@ from typing import List from typing import Iterator - from sentry_sdk._types import Event, EventDataCategory, Log + from sentry_sdk._types import Event, EventDataCategory def parse_json(data): @@ -103,7 +103,7 @@ def add_sessions( self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) def add_log( - self, log # type: Log + self, log # type: Any ): # type: (...) -> None self.add_item(Item(payload=PayloadRef(json=log), type="otel_log")) diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index 7abc202f5d..5f21de366c 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -1,15 +1,15 @@ # NOTE: this is the logger sentry exposes to users, not some generic logger. import functools -from typing import Any, Optional +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) -> Optional[str] + # type: (str, int, str, **Any) -> None client = get_client() scope = get_current_scope() - return client.capture_log(scope, severity_text, severity_number, template, **kwargs) + client.capture_log(scope, severity_text, severity_number, template, **kwargs) trace = functools.partial(capture_log, "trace", 1) From 26a1fe4946536c731c5f9ac4f1c8db9535057c37 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 16:20:19 -0400 Subject: [PATCH 04/28] mypy --- sentry_sdk/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e764aee7e0..e269cb52d0 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -57,7 +57,7 @@ from typing import Union from typing import TypeVar - from sentry_sdk._types import Event, Hint, SDKInfo, Log + from sentry_sdk._types import Event, Hint, SDKInfo from sentry_sdk.integrations import Integration from sentry_sdk.metrics import MetricsAggregator from sentry_sdk.scope import Scope @@ -874,7 +874,6 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) for k, v in kwargs.items(): attrs[f"sentry.message.parameters.{k}"] = v - # type: Log log = { "severity_text": severity_text, "severity_number": severity_number, @@ -890,14 +889,15 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) before_send_log = self.options.get("before_send_log") if before_send_log is not None: - log = before_send_log(log) + hint = {} + log = before_send_log(log, hint) if log is None: return def format_attribute(key, val): # type: (str, int | float | str | bool) -> Any if isinstance(val, int): - return {"key": key, "value": {"intValue": val}} + return {"key": key, "value": {"intValue": str(val)}} if isinstance(val, str): return {"key": key, "value": {"stringValue": val}} if isinstance(val, float): From ba7db76aa316c84cb1a3d1437fcf46b62d5620e8 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 16:24:07 -0400 Subject: [PATCH 05/28] mypy --- sentry_sdk/client.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e269cb52d0..5339042bd7 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -865,12 +865,12 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) attrs = { "sentry.message.template": template, } - if (extra_attrs := kwargs.get("attributes")) is not None: - attrs.update(extra_attrs) - if (env := self.options.get("environment")) is not None: - attrs["sentry.environment"] = env - if (release := self.options.get("release")) is not None: - attrs["sentry.release"] = release + if kwargs.get("attributes") is not None: + attrs.update(kwargs.get("attributes")) + if self.options.get("environment") is not None: + attrs["sentry.environment"] = self.options.get("environment") + if self.options.get("release") is not None: + attrs["sentry.release"] = self.options.get("release") for k, v in kwargs.items(): attrs[f"sentry.message.parameters.{k}"] = v @@ -882,9 +882,10 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) "time_unix_nano": time.time_ns(), } - if (ctx := scope.get_active_propagation_context()) is not None: - headers["trace_id"] = ctx.trace_id - log["trace_id"] = ctx.trace_id + 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_send_log = self.options.get("before_send_log") @@ -911,11 +912,10 @@ def format_attribute(key, val): "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 isinstance((attrs := log["attributes"]), dict): - otel_log["attributes"] = [ - format_attribute(k, v) for (k, v) in attrs.items() - ] if "trace_id" in log: otel_log["traceId"] = log["trace_id"] From 0da434045835c431f30418ead349791858ef4b24 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 16:25:03 -0400 Subject: [PATCH 06/28] move to experimental --- sentry_sdk/__init__.py | 2 +- sentry_sdk/{logger.py => _experimental_logger.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename sentry_sdk/{logger.py => _experimental_logger.py} (100%) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index b4859cc5d2..4a0d551e5a 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -45,7 +45,7 @@ "start_transaction", "trace", "monitor", - "logger", + "_experimental_logger.py", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/logger.py b/sentry_sdk/_experimental_logger.py similarity index 100% rename from sentry_sdk/logger.py rename to sentry_sdk/_experimental_logger.py From e8a1c0820a9e1ddf68bf1be4c03a1a00e06c4ddb Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 16:30:14 -0400 Subject: [PATCH 07/28] mypy --- sentry_sdk/client.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 5339042bd7..95cbf0eaa0 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -57,7 +57,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 @@ -865,12 +865,15 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) attrs = { "sentry.message.template": template, } - if kwargs.get("attributes") is not None: - attrs.update(kwargs.get("attributes")) - if self.options.get("environment") is not None: - attrs["sentry.environment"] = self.options.get("environment") - if self.options.get("release") is not None: - attrs["sentry.release"] = self.options.get("release") + 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 for k, v in kwargs.items(): attrs[f"sentry.message.parameters.{k}"] = v @@ -880,7 +883,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) "body": template.format(**kwargs), "attributes": attrs, "time_unix_nano": time.time_ns(), - } + } # type: Log propagation_context = scope.get_active_propagation_context() if propagation_context is not None: @@ -890,8 +893,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) before_send_log = self.options.get("before_send_log") if before_send_log is not None: - hint = {} - log = before_send_log(log, hint) + log = before_send_log(log, {}) if log is None: return From 24ae29a984f17826642379c922ea0df6625b28c0 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 16:38:02 -0400 Subject: [PATCH 08/28] mypy --- sentry_sdk/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 95cbf0eaa0..3b4ffd379f 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -883,6 +883,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) "body": template.format(**kwargs), "attributes": attrs, "time_unix_nano": time.time_ns(), + "trace_id": None, } # type: Log propagation_context = scope.get_active_propagation_context() From 41d39e97814fae656dff6a3d18aa681686f2f710 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Mon, 10 Mar 2025 20:47:22 -0400 Subject: [PATCH 09/28] mypu --- sentry_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 3b4ffd379f..755948b77c 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -864,7 +864,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) 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) From bef8fffe237921fecbd85ea11335f751eba30655 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Tue, 11 Mar 2025 12:22:59 -0400 Subject: [PATCH 10/28] rename before_send_log, handle parent_span_id --- sentry_sdk/client.py | 9 ++++++--- sentry_sdk/consts.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 755948b77c..61eee24f4b 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -874,6 +874,9 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) 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 @@ -892,9 +895,9 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) log["trace_id"] = propagation_context.trace_id envelope = Envelope(headers=headers) - before_send_log = self.options.get("before_send_log") - if before_send_log is not None: - log = before_send_log(log, {}) + before_emit_log = self.options.get("before_emit_log") + if before_emit_log is not None: + log = before_emit_log(log, {}) if log is None: return diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index cf826c25bb..c47bdcc53b 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -541,7 +541,7 @@ def __init__( instrumenter=INSTRUMENTER.SENTRY, # type: Optional[str] before_send_transaction=None, # type: Optional[TransactionProcessor] enable_sentry_logs=False, # type: bool - before_send_log=None, # type: Optional[LogProcessor] + before_emit_log=None, # type: Optional[LogProcessor] project_root=None, # type: Optional[str] enable_tracing=None, # type: Optional[bool] include_local_variables=True, # type: Optional[bool] From e2d39f09f57d7b90105dc178ff16b823126a4472 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 12:02:01 +0100 Subject: [PATCH 11/28] whitespace --- sentry_sdk/client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 61eee24f4b..c0bace6fc1 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -865,18 +865,23 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) 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 @@ -893,6 +898,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) 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.get("before_emit_log") @@ -922,10 +928,12 @@ def format_attribute(key, val): 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) From d1fce6c0b1406a952afa574f41da73f4034c0bfb Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:25:06 +0100 Subject: [PATCH 12/28] Added some basic unit tests --- tests/test_sentry_logs.py | 176 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 tests/test_sentry_logs.py diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py new file mode 100644 index 0000000000..f73a126ed7 --- /dev/null +++ b/tests/test_sentry_logs.py @@ -0,0 +1,176 @@ +import random +from unittest import mock + +from sentry_sdk import _experimental_logger as sentry_logger + + +def test_logs_disabled_by_default(sentry_init, capture_envelopes): + sentry_init() + envelopes = capture_envelopes() + + sentry_logger.trace("This is a 'trace' log...{rand}", rand=random.randint(0, 100)) + sentry_logger.debug("This is a 'debug' log... ({rand})", rand=random.randint(0, 100)) + sentry_logger.info("This is a 'info' log... ({rand})", rand=random.randint(0, 100)) + sentry_logger.warn("This is a 'warn' log... ({rand})", rand=random.randint(0, 100)) + sentry_logger.error("This is a 'error' log... ({rand})", rand=random.randint(0, 100)) + sentry_logger.fatal("This is a 'fatal' log... ({rand})", rand=random.randint(0, 100)) + + assert len(envelopes) == 0 + + +def test_logs_basics(sentry_init, capture_envelopes): + sentry_init(enable_sentry_logs=True) + envelopes = capture_envelopes() + + sentry_logger.trace("This is a 'trace' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.debug("This is a 'debug' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.info("This is a 'info' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.warn("This is a 'warn' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.error("This is a 'error' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.fatal("This is a 'fatal' log... {rand}", rand=random.randint(0, 100)) + + assert len(envelopes) == 6 # We will batch those log items into a single envelope at some point + assert envelopes[0].items[0].payload.json["severityText"] == "trace" + assert envelopes[0].items[0].payload.json["severityNumber"] == 1 + + assert envelopes[1].items[0].payload.json["severityText"] == "debug" + assert envelopes[1].items[0].payload.json["severityNumber"] == 5 + + assert envelopes[2].items[0].payload.json["severityText"] == "info" + assert envelopes[2].items[0].payload.json["severityNumber"] == 9 + + assert envelopes[3].items[0].payload.json["severityText"] == "warn" + assert envelopes[3].items[0].payload.json["severityNumber"] == 13 + + assert envelopes[4].items[0].payload.json["severityText"] == "error" + assert envelopes[4].items[0].payload.json["severityNumber"] == 17 + + assert envelopes[5].items[0].payload.json["severityText"] == "fatal" + assert envelopes[5].items[0].payload.json["severityNumber"] == 21 + + +def test_logs_before_emit_log(sentry_init, capture_envelopes): + def _before_log(record, hint): + assert list(record.keys()) == ['severity_text', 'severity_number', 'body', 'attributes', 'time_unix_nano', 'trace_id'] + + if record['severity_text'] in ['fatal', 'error']: + return None + + return record + + sentry_init( + enable_sentry_logs=True, + before_emit_log=_before_log, + ) + + envelopes = capture_envelopes() + + sentry_logger.trace("This is a 'trace' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.debug("This is a 'debug' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.info("This is a 'info' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.warn("This is a 'warn' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.error("This is a 'error' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.fatal("This is a 'fatal' log... {rand}", rand=random.randint(0, 100)) + + assert len(envelopes) == 4 + + assert envelopes[0].items[0].payload.json["severityText"] == "trace" + assert envelopes[1].items[0].payload.json["severityText"] == "debug" + assert envelopes[2].items[0].payload.json["severityText"] == "info" + assert envelopes[3].items[0].payload.json["severityText"] == "warn" + + +def test_logs_attributes(sentry_init, capture_envelopes): + sentry_init(enable_sentry_logs=True) + + envelopes = capture_envelopes() + + attrs = { + "attr_int": 1, + "attr_float": 2.0, + "attr_bool": True, + "attr_string": "string attribute", + } + + sentry_logger.warn("The recorded value was '{my_var}'", my_var="some value", attributes=attrs) + + log_item = envelopes[0].items[0].payload.json + assert log_item["body"]["stringValue"] == "The recorded value was 'some value'" + + assert log_item["attributes"][1] == {"key": "attr_int", "value": {"intValue": "1"}} # ??? + assert log_item["attributes"][2] == {"key": "attr_float", "value": {"doubleValue": 2.0}} + assert log_item["attributes"][3] == {"key": "attr_bool", "value": {"intValue": "True"}} # ??? + assert log_item["attributes"][4] == {"key": "attr_string", "value": {"stringValue": "string attribute"}} + assert log_item["attributes"][5] == {"key": "sentry.environment", "value": {"stringValue": "production"}} + assert log_item["attributes"][6] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} + assert log_item["attributes"][7] == {"key": "sentry.message.parameters.my_var", "value": {"stringValue": "some value"}} + + +def test_logs_message_params(sentry_init, capture_envelopes): + sentry_init(enable_sentry_logs=True) + + envelopes = capture_envelopes() + + sentry_logger.warn("The recorded value was '{int_var}'", int_var=1) + sentry_logger.warn("The recorded value was '{float_var}'", float_var=2.0) + sentry_logger.warn("The recorded value was '{bool_var}'", bool_var=False) + sentry_logger.warn("The recorded value was '{string_var}'", string_var="some string value") + + assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" + assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.int_var", "value": {'intValue': "1"}} # ??? + + assert envelopes[1].items[0].payload.json["body"]["stringValue"] == "The recorded value was '2.0'" + assert envelopes[1].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.float_var", "value": {'doubleValue': 2.0}} + + assert envelopes[2].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'False'" + assert envelopes[2].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.bool_var", "value": {'intValue': "False"}} # ??? + + assert envelopes[3].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'some string value'" + assert envelopes[3].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.string_var", "value": {'stringValue': "some string value"}} + + +def test_logs_message_old_style(sentry_init, capture_envelopes): + sentry_init(enable_sentry_logs=True) + + envelopes = capture_envelopes() + + sentry_logger.warn("The recorded value was '%s'" % 1) + + assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" + assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} # no parametrization! + + +def test_logs_message_format(sentry_init, capture_envelopes): + sentry_init(enable_sentry_logs=True) + + envelopes = capture_envelopes() + + sentry_logger.warn("The recorded value was '{int_var}'".format(int_var=1)) + + assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" + assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} # no parametrization! + + +def test_logs_message_f_string(sentry_init, capture_envelopes): + sentry_init(enable_sentry_logs=True) + + envelopes = capture_envelopes() + + int_var = 1 + sentry_logger.warn(f"The recorded value was '{int_var}'") + + assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" + assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} # no parametrization! + + +def test_logs_message_python_logging(sentry_init, capture_envelopes): + sentry_init(enable_sentry_logs=True) + + envelopes = capture_envelopes() + + try: + sentry_logger.warn(f"The recorded value was '%s'", 1) + except Exception as ex: + # This is when users just replace the existing call to Python logging method, with the new Sentry logging method. + # This is a very confusing error message to explain what is wrong here. + assert str(ex) == "capture_log() takes 3 positional arguments but 4 were given" From 5891202c09494ee6fede82f5213bd68a8eea068a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:31:13 +0100 Subject: [PATCH 13/28] explainational text --- tests/test_sentry_logs.py | 61 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py index f73a126ed7..70f89069f4 100644 --- a/tests/test_sentry_logs.py +++ b/tests/test_sentry_logs.py @@ -8,12 +8,12 @@ def test_logs_disabled_by_default(sentry_init, capture_envelopes): sentry_init() envelopes = capture_envelopes() - sentry_logger.trace("This is a 'trace' log...{rand}", rand=random.randint(0, 100)) - sentry_logger.debug("This is a 'debug' log... ({rand})", rand=random.randint(0, 100)) - sentry_logger.info("This is a 'info' log... ({rand})", rand=random.randint(0, 100)) - sentry_logger.warn("This is a 'warn' log... ({rand})", rand=random.randint(0, 100)) - sentry_logger.error("This is a 'error' log... ({rand})", rand=random.randint(0, 100)) - sentry_logger.fatal("This is a 'fatal' log... ({rand})", rand=random.randint(0, 100)) + sentry_logger.trace("This is a 'trace' log.") + sentry_logger.debug("This is a 'debug' log...") + sentry_logger.info("This is a 'info' log...") + sentry_logger.warn("This is a 'warn' log...") + sentry_logger.error("This is a 'error' log...") + sentry_logger.fatal("This is a 'fatal' log...") assert len(envelopes) == 0 @@ -22,14 +22,15 @@ def test_logs_basics(sentry_init, capture_envelopes): sentry_init(enable_sentry_logs=True) envelopes = capture_envelopes() - sentry_logger.trace("This is a 'trace' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.debug("This is a 'debug' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.info("This is a 'info' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.warn("This is a 'warn' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.error("This is a 'error' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.fatal("This is a 'fatal' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.trace("This is a 'trace' log...") + sentry_logger.debug("This is a 'debug' log...") + sentry_logger.info("This is a 'info' log...") + sentry_logger.warn("This is a 'warn' log...") + sentry_logger.error("This is a 'error' log...") + sentry_logger.fatal("This is a 'fatal' log...") assert len(envelopes) == 6 # We will batch those log items into a single envelope at some point + assert envelopes[0].items[0].payload.json["severityText"] == "trace" assert envelopes[0].items[0].payload.json["severityNumber"] == 1 @@ -62,15 +63,14 @@ def _before_log(record, hint): enable_sentry_logs=True, before_emit_log=_before_log, ) - envelopes = capture_envelopes() - sentry_logger.trace("This is a 'trace' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.debug("This is a 'debug' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.info("This is a 'info' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.warn("This is a 'warn' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.error("This is a 'error' log... {rand}", rand=random.randint(0, 100)) - sentry_logger.fatal("This is a 'fatal' log... {rand}", rand=random.randint(0, 100)) + sentry_logger.trace("This is a 'trace' log...") + sentry_logger.debug("This is a 'debug' log...") + sentry_logger.info("This is a 'info' log...") + sentry_logger.warn("This is a 'warn' log...") + sentry_logger.error("This is a 'error' log...") + sentry_logger.fatal("This is a 'fatal' log...") assert len(envelopes) == 4 @@ -81,8 +81,10 @@ def _before_log(record, hint): def test_logs_attributes(sentry_init, capture_envelopes): + """ + Passing arbitrary attributes to log messages. + """ sentry_init(enable_sentry_logs=True) - envelopes = capture_envelopes() attrs = { @@ -107,8 +109,10 @@ def test_logs_attributes(sentry_init, capture_envelopes): def test_logs_message_params(sentry_init, capture_envelopes): + """ + This is the official way of how to pass vars to log messages. + """ sentry_init(enable_sentry_logs=True) - envelopes = capture_envelopes() sentry_logger.warn("The recorded value was '{int_var}'", int_var=1) @@ -130,6 +134,9 @@ def test_logs_message_params(sentry_init, capture_envelopes): def test_logs_message_old_style(sentry_init, capture_envelopes): + """ + This is how vars are passed to strings in old Python projects. + """ sentry_init(enable_sentry_logs=True) envelopes = capture_envelopes() @@ -141,8 +148,10 @@ def test_logs_message_old_style(sentry_init, capture_envelopes): def test_logs_message_format(sentry_init, capture_envelopes): + """ + This is another popular war how vars are passed to strings in old Python projects. + """ sentry_init(enable_sentry_logs=True) - envelopes = capture_envelopes() sentry_logger.warn("The recorded value was '{int_var}'".format(int_var=1)) @@ -152,8 +161,10 @@ def test_logs_message_format(sentry_init, capture_envelopes): def test_logs_message_f_string(sentry_init, capture_envelopes): + """ + This is the preferred way how vars are passed to strings in old Python projects. + """ sentry_init(enable_sentry_logs=True) - envelopes = capture_envelopes() int_var = 1 @@ -164,8 +175,10 @@ def test_logs_message_f_string(sentry_init, capture_envelopes): def test_logs_message_python_logging(sentry_init, capture_envelopes): + """ + This is how vars are passed to log messages when using Python logging module. + """ sentry_init(enable_sentry_logs=True) - envelopes = capture_envelopes() try: From bad1974b547530e710d5a82a33627f21d682f85c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:33:23 +0100 Subject: [PATCH 14/28] the last assert --- tests/test_sentry_logs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py index 70f89069f4..c80ed7b6cf 100644 --- a/tests/test_sentry_logs.py +++ b/tests/test_sentry_logs.py @@ -187,3 +187,5 @@ def test_logs_message_python_logging(sentry_init, capture_envelopes): # This is when users just replace the existing call to Python logging method, with the new Sentry logging method. # This is a very confusing error message to explain what is wrong here. assert str(ex) == "capture_log() takes 3 positional arguments but 4 were given" + + assert len(envelopes) == 0 From 92b7e6dbc8c9578a806d0289581a076c7a8839ce Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:40:50 +0100 Subject: [PATCH 15/28] Logs tied to transactions/spans --- tests/test_sentry_logs.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py index c80ed7b6cf..f6a966aa11 100644 --- a/tests/test_sentry_logs.py +++ b/tests/test_sentry_logs.py @@ -1,6 +1,7 @@ import random from unittest import mock +import sentry_sdk from sentry_sdk import _experimental_logger as sentry_logger @@ -189,3 +190,32 @@ def test_logs_message_python_logging(sentry_init, capture_envelopes): assert str(ex) == "capture_log() takes 3 positional arguments but 4 were given" assert len(envelopes) == 0 + + +def test_logs_tied_to_transactions(sentry_init, capture_envelopes): + """ + Log messages are also tied to transactions. + """ + sentry_init(enable_sentry_logs=True) + envelopes = capture_envelopes() + + with sentry_sdk.start_transaction(name="test-transaction") as trx: + sentry_logger.warn("This is a log tied to a transaction") + + log_entry = envelopes[0].items[0].payload.json + assert log_entry["attributes"][-1] =={'key': 'sentry.trace.parent_span_id', 'value': {'stringValue': trx.span_id}} + + +def test_logs_tied_to_spans(sentry_init, capture_envelopes): + """ + Log messages are also tied to spans. + """ + sentry_init(enable_sentry_logs=True) + envelopes = capture_envelopes() + + with sentry_sdk.start_transaction(name="test-transaction") as trx: + with sentry_sdk.start_span(description="test-span") as span: + sentry_logger.warn("This is a log tied to a span") + + log_entry = envelopes[0].items[0].payload.json + assert log_entry["attributes"][-1] =={'key': 'sentry.trace.parent_span_id', 'value': {'stringValue': span.span_id}} From 176f45e42cb5def8be9c48664972f2a8e6f52d65 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:45:46 +0100 Subject: [PATCH 16/28] Added todos --- tests/test_sentry_logs.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py index f6a966aa11..c14df58162 100644 --- a/tests/test_sentry_logs.py +++ b/tests/test_sentry_logs.py @@ -100,9 +100,9 @@ def test_logs_attributes(sentry_init, capture_envelopes): log_item = envelopes[0].items[0].payload.json assert log_item["body"]["stringValue"] == "The recorded value was 'some value'" - assert log_item["attributes"][1] == {"key": "attr_int", "value": {"intValue": "1"}} # ??? + assert log_item["attributes"][1] == {"key": "attr_int", "value": {"intValue": "1"}} # TODO: this is strange. assert log_item["attributes"][2] == {"key": "attr_float", "value": {"doubleValue": 2.0}} - assert log_item["attributes"][3] == {"key": "attr_bool", "value": {"intValue": "True"}} # ??? + assert log_item["attributes"][3] == {"key": "attr_bool", "value": {"intValue": "True"}} # TODO: this is strange. assert log_item["attributes"][4] == {"key": "attr_string", "value": {"stringValue": "string attribute"}} assert log_item["attributes"][5] == {"key": "sentry.environment", "value": {"stringValue": "production"}} assert log_item["attributes"][6] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} @@ -122,13 +122,13 @@ def test_logs_message_params(sentry_init, capture_envelopes): sentry_logger.warn("The recorded value was '{string_var}'", string_var="some string value") assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" - assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.int_var", "value": {'intValue': "1"}} # ??? + assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.int_var", "value": {'intValue': "1"}} # TODO: this is strange. assert envelopes[1].items[0].payload.json["body"]["stringValue"] == "The recorded value was '2.0'" assert envelopes[1].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.float_var", "value": {'doubleValue': 2.0}} assert envelopes[2].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'False'" - assert envelopes[2].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.bool_var", "value": {'intValue': "False"}} # ??? + assert envelopes[2].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.bool_var", "value": {'intValue': "False"}} # TODO: this is strange. assert envelopes[3].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'some string value'" assert envelopes[3].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.string_var", "value": {'stringValue': "some string value"}} @@ -137,6 +137,7 @@ def test_logs_message_params(sentry_init, capture_envelopes): def test_logs_message_old_style(sentry_init, capture_envelopes): """ This is how vars are passed to strings in old Python projects. + TODO: Should we support this? """ sentry_init(enable_sentry_logs=True) @@ -151,6 +152,7 @@ def test_logs_message_old_style(sentry_init, capture_envelopes): def test_logs_message_format(sentry_init, capture_envelopes): """ This is another popular war how vars are passed to strings in old Python projects. + TODO: Should we support this? """ sentry_init(enable_sentry_logs=True) envelopes = capture_envelopes() @@ -164,6 +166,7 @@ def test_logs_message_format(sentry_init, capture_envelopes): def test_logs_message_f_string(sentry_init, capture_envelopes): """ This is the preferred way how vars are passed to strings in old Python projects. + TODO: This we should definitely support. """ sentry_init(enable_sentry_logs=True) envelopes = capture_envelopes() @@ -178,6 +181,7 @@ def test_logs_message_f_string(sentry_init, capture_envelopes): def test_logs_message_python_logging(sentry_init, capture_envelopes): """ This is how vars are passed to log messages when using Python logging module. + TODO: We probably should also support this, to make it easier to migrate from the old logging module to the Sentry one. """ sentry_init(enable_sentry_logs=True) envelopes = capture_envelopes() From 42f1fb82590aef0cdbd2ed33224f7f56f6263818 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:52:30 +0100 Subject: [PATCH 17/28] Fixed boolValue --- sentry_sdk/client.py | 8 ++++---- tests/test_sentry_logs.py | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index c0bace6fc1..3f845d4c29 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -909,14 +909,14 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) 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, str): - return {"key": key, "value": {"stringValue": val}} if isinstance(val, float): return {"key": key, "value": {"doubleValue": val}} - if isinstance(val, bool): - return {"key": key, "value": {"boolValue": val}} + if isinstance(val, str): + return {"key": key, "value": {"stringValue": val}} return {"key": key, "value": {"stringValue": json.dumps(val)}} otel_log = { diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py index c14df58162..fa11bc5ecb 100644 --- a/tests/test_sentry_logs.py +++ b/tests/test_sentry_logs.py @@ -1,4 +1,3 @@ -import random from unittest import mock import sentry_sdk @@ -102,7 +101,7 @@ def test_logs_attributes(sentry_init, capture_envelopes): assert log_item["attributes"][1] == {"key": "attr_int", "value": {"intValue": "1"}} # TODO: this is strange. assert log_item["attributes"][2] == {"key": "attr_float", "value": {"doubleValue": 2.0}} - assert log_item["attributes"][3] == {"key": "attr_bool", "value": {"intValue": "True"}} # TODO: this is strange. + assert log_item["attributes"][3] == {"key": "attr_bool", "value": {"boolValue": True}} assert log_item["attributes"][4] == {"key": "attr_string", "value": {"stringValue": "string attribute"}} assert log_item["attributes"][5] == {"key": "sentry.environment", "value": {"stringValue": "production"}} assert log_item["attributes"][6] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} @@ -128,7 +127,7 @@ def test_logs_message_params(sentry_init, capture_envelopes): assert envelopes[1].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.float_var", "value": {'doubleValue': 2.0}} assert envelopes[2].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'False'" - assert envelopes[2].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.bool_var", "value": {'intValue': "False"}} # TODO: this is strange. + assert envelopes[2].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.bool_var", "value": {'boolValue': False}} assert envelopes[3].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'some string value'" assert envelopes[3].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.string_var", "value": {'stringValue': "some string value"}} From 1be2ce3826305bb50136133f7c89d3d7f6219da5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:54:05 +0100 Subject: [PATCH 18/28] typo --- tests/test_sentry_logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py index fa11bc5ecb..8883bee762 100644 --- a/tests/test_sentry_logs.py +++ b/tests/test_sentry_logs.py @@ -150,7 +150,7 @@ def test_logs_message_old_style(sentry_init, capture_envelopes): def test_logs_message_format(sentry_init, capture_envelopes): """ - This is another popular war how vars are passed to strings in old Python projects. + This is another popular way how vars are passed to strings in old Python projects. TODO: Should we support this? """ sentry_init(enable_sentry_logs=True) From 688c77efccf071ad369f1911473a92eee8684040 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 13 Mar 2025 14:56:06 +0100 Subject: [PATCH 19/28] typo --- tests/test_sentry_logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sentry_logs.py b/tests/test_sentry_logs.py index 8883bee762..3319f256be 100644 --- a/tests/test_sentry_logs.py +++ b/tests/test_sentry_logs.py @@ -164,7 +164,7 @@ def test_logs_message_format(sentry_init, capture_envelopes): def test_logs_message_f_string(sentry_init, capture_envelopes): """ - This is the preferred way how vars are passed to strings in old Python projects. + This is the preferred way how vars are passed to strings in newer Python projects. TODO: This we should definitely support. """ sentry_init(enable_sentry_logs=True) From 88a7780261783bb1b88051290128818c091d3efe Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 11:59:16 +0100 Subject: [PATCH 20/28] renamed file --- tests/{test_sentry_logs.py => test_logs.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_sentry_logs.py => test_logs.py} (100%) diff --git a/tests/test_sentry_logs.py b/tests/test_logs.py similarity index 100% rename from tests/test_sentry_logs.py rename to tests/test_logs.py From d90a2dab3ab9a2c3ac621af7605dd1abd0e8a6e3 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 13:33:00 +0100 Subject: [PATCH 21/28] Mark capture_log as private --- sentry_sdk/_experimental_logger.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/_experimental_logger.py b/sentry_sdk/_experimental_logger.py index 5f21de366c..1f3cd5e443 100644 --- a/sentry_sdk/_experimental_logger.py +++ b/sentry_sdk/_experimental_logger.py @@ -5,16 +5,16 @@ from sentry_sdk import get_client, get_current_scope -def capture_log(severity_text, severity_number, template, **kwargs): +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) +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) From cccce0f14db0eaa699bfa3d73ced0598eb8fea7e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 13:33:40 +0100 Subject: [PATCH 22/28] Move config params into _experimental --- sentry_sdk/client.py | 5 +++-- sentry_sdk/consts.py | 2 -- tests/test_logs.py | 18 +++++++++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 3f845d4c29..934178660f 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -855,7 +855,8 @@ def capture_event( def capture_log(self, scope, severity_text, severity_number, template, **kwargs): # type: (Scope, str, int, str, **Any) -> None - if not self.options.get("enable_sentry_logs", False): + logs_enabled = self.options["_experiments"].get("enable_sentry_logs", False) + if not logs_enabled: return headers = { @@ -901,7 +902,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) envelope = Envelope(headers=headers) - before_emit_log = self.options.get("before_emit_log") + 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: diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index c47bdcc53b..ad399dfa30 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -540,8 +540,6 @@ def __init__( proxy_headers=None, # type: Optional[Dict[str, str]] instrumenter=INSTRUMENTER.SENTRY, # type: Optional[str] before_send_transaction=None, # type: Optional[TransactionProcessor] - enable_sentry_logs=False, # type: bool - before_emit_log=None, # type: Optional[LogProcessor] project_root=None, # type: Optional[str] enable_tracing=None, # type: Optional[bool] include_local_variables=True, # type: Optional[bool] diff --git a/tests/test_logs.py b/tests/test_logs.py index 3319f256be..4733a04d55 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -19,7 +19,7 @@ def test_logs_disabled_by_default(sentry_init, capture_envelopes): def test_logs_basics(sentry_init, capture_envelopes): - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() sentry_logger.trace("This is a 'trace' log...") @@ -84,7 +84,7 @@ def test_logs_attributes(sentry_init, capture_envelopes): """ Passing arbitrary attributes to log messages. """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() attrs = { @@ -112,7 +112,7 @@ def test_logs_message_params(sentry_init, capture_envelopes): """ This is the official way of how to pass vars to log messages. """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() sentry_logger.warn("The recorded value was '{int_var}'", int_var=1) @@ -138,7 +138,7 @@ def test_logs_message_old_style(sentry_init, capture_envelopes): This is how vars are passed to strings in old Python projects. TODO: Should we support this? """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() @@ -153,7 +153,7 @@ def test_logs_message_format(sentry_init, capture_envelopes): This is another popular way how vars are passed to strings in old Python projects. TODO: Should we support this? """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() sentry_logger.warn("The recorded value was '{int_var}'".format(int_var=1)) @@ -167,7 +167,7 @@ def test_logs_message_f_string(sentry_init, capture_envelopes): This is the preferred way how vars are passed to strings in newer Python projects. TODO: This we should definitely support. """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() int_var = 1 @@ -182,7 +182,7 @@ def test_logs_message_python_logging(sentry_init, capture_envelopes): This is how vars are passed to log messages when using Python logging module. TODO: We probably should also support this, to make it easier to migrate from the old logging module to the Sentry one. """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() try: @@ -199,7 +199,7 @@ def test_logs_tied_to_transactions(sentry_init, capture_envelopes): """ Log messages are also tied to transactions. """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() with sentry_sdk.start_transaction(name="test-transaction") as trx: @@ -213,7 +213,7 @@ def test_logs_tied_to_spans(sentry_init, capture_envelopes): """ Log messages are also tied to spans. """ - sentry_init(enable_sentry_logs=True) + sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() with sentry_sdk.start_transaction(name="test-transaction") as trx: From 7472d5fa31ef89158921a019cf71b9a557882e0e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 13:35:16 +0100 Subject: [PATCH 23/28] Cleaned up tests --- tests/test_logs.py | 68 +++------------------------------------------- 1 file changed, 4 insertions(+), 64 deletions(-) diff --git a/tests/test_logs.py b/tests/test_logs.py index 4733a04d55..f5b58ce55a 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -60,8 +60,10 @@ def _before_log(record, hint): return record sentry_init( - enable_sentry_logs=True, - before_emit_log=_before_log, + _experiments={ + "enable_sentry_logs": True, + "before_emit_log": _before_log, + } ) envelopes = capture_envelopes() @@ -133,68 +135,6 @@ def test_logs_message_params(sentry_init, capture_envelopes): assert envelopes[3].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.string_var", "value": {'stringValue': "some string value"}} -def test_logs_message_old_style(sentry_init, capture_envelopes): - """ - This is how vars are passed to strings in old Python projects. - TODO: Should we support this? - """ - sentry_init(_experiments={"enable_sentry_logs": True}) - - envelopes = capture_envelopes() - - sentry_logger.warn("The recorded value was '%s'" % 1) - - assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" - assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} # no parametrization! - - -def test_logs_message_format(sentry_init, capture_envelopes): - """ - This is another popular way how vars are passed to strings in old Python projects. - TODO: Should we support this? - """ - sentry_init(_experiments={"enable_sentry_logs": True}) - envelopes = capture_envelopes() - - sentry_logger.warn("The recorded value was '{int_var}'".format(int_var=1)) - - assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" - assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} # no parametrization! - - -def test_logs_message_f_string(sentry_init, capture_envelopes): - """ - This is the preferred way how vars are passed to strings in newer Python projects. - TODO: This we should definitely support. - """ - sentry_init(_experiments={"enable_sentry_logs": True}) - envelopes = capture_envelopes() - - int_var = 1 - sentry_logger.warn(f"The recorded value was '{int_var}'") - - assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" - assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} # no parametrization! - - -def test_logs_message_python_logging(sentry_init, capture_envelopes): - """ - This is how vars are passed to log messages when using Python logging module. - TODO: We probably should also support this, to make it easier to migrate from the old logging module to the Sentry one. - """ - sentry_init(_experiments={"enable_sentry_logs": True}) - envelopes = capture_envelopes() - - try: - sentry_logger.warn(f"The recorded value was '%s'", 1) - except Exception as ex: - # This is when users just replace the existing call to Python logging method, with the new Sentry logging method. - # This is a very confusing error message to explain what is wrong here. - assert str(ex) == "capture_log() takes 3 positional arguments but 4 were given" - - assert len(envelopes) == 0 - - def test_logs_tied_to_transactions(sentry_init, capture_envelopes): """ Log messages are also tied to transactions. From 712eb5ba6be2651cf6a9c6c12785b9166077417d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 13:40:32 +0100 Subject: [PATCH 24/28] Format tests --- tests/test_logs.py | 156 ++++++++++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 46 deletions(-) diff --git a/tests/test_logs.py b/tests/test_logs.py index f5b58ce55a..580f27876b 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -1,4 +1,4 @@ -from unittest import mock +from unittest import mock import sentry_sdk from sentry_sdk import _experimental_logger as sentry_logger @@ -8,12 +8,12 @@ def test_logs_disabled_by_default(sentry_init, capture_envelopes): sentry_init() envelopes = capture_envelopes() - sentry_logger.trace("This is a 'trace' log.") - sentry_logger.debug("This is a 'debug' log...") - sentry_logger.info("This is a 'info' log...") - sentry_logger.warn("This is a 'warn' log...") - sentry_logger.error("This is a 'error' log...") - sentry_logger.fatal("This is a 'fatal' log...") + sentry_logger.trace("This is a 'trace' log.") + sentry_logger.debug("This is a 'debug' log...") + sentry_logger.info("This is a 'info' log...") + sentry_logger.warn("This is a 'warn' log...") + sentry_logger.error("This is a 'error' log...") + sentry_logger.fatal("This is a 'fatal' log...") assert len(envelopes) == 0 @@ -29,39 +29,48 @@ def test_logs_basics(sentry_init, capture_envelopes): sentry_logger.error("This is a 'error' log...") sentry_logger.fatal("This is a 'fatal' log...") - assert len(envelopes) == 6 # We will batch those log items into a single envelope at some point - + assert ( + len(envelopes) == 6 + ) # We will batch those log items into a single envelope at some point + assert envelopes[0].items[0].payload.json["severityText"] == "trace" assert envelopes[0].items[0].payload.json["severityNumber"] == 1 - + assert envelopes[1].items[0].payload.json["severityText"] == "debug" assert envelopes[1].items[0].payload.json["severityNumber"] == 5 - + assert envelopes[2].items[0].payload.json["severityText"] == "info" assert envelopes[2].items[0].payload.json["severityNumber"] == 9 - + assert envelopes[3].items[0].payload.json["severityText"] == "warn" assert envelopes[3].items[0].payload.json["severityNumber"] == 13 - + assert envelopes[4].items[0].payload.json["severityText"] == "error" assert envelopes[4].items[0].payload.json["severityNumber"] == 17 - + assert envelopes[5].items[0].payload.json["severityText"] == "fatal" assert envelopes[5].items[0].payload.json["severityNumber"] == 21 - + def test_logs_before_emit_log(sentry_init, capture_envelopes): def _before_log(record, hint): - assert list(record.keys()) == ['severity_text', 'severity_number', 'body', 'attributes', 'time_unix_nano', 'trace_id'] - - if record['severity_text'] in ['fatal', 'error']: + assert list(record.keys()) == [ + "severity_text", + "severity_number", + "body", + "attributes", + "time_unix_nano", + "trace_id", + ] + + if record["severity_text"] in ["fatal", "error"]: return None - + return record - + sentry_init( _experiments={ - "enable_sentry_logs": True, + "enable_sentry_logs": True, "before_emit_log": _before_log, } ) @@ -84,7 +93,7 @@ def _before_log(record, hint): def test_logs_attributes(sentry_init, capture_envelopes): """ - Passing arbitrary attributes to log messages. + Passing arbitrary attributes to log messages. """ sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() @@ -96,43 +105,92 @@ def test_logs_attributes(sentry_init, capture_envelopes): "attr_string": "string attribute", } - sentry_logger.warn("The recorded value was '{my_var}'", my_var="some value", attributes=attrs) + sentry_logger.warn( + "The recorded value was '{my_var}'", my_var="some value", attributes=attrs + ) log_item = envelopes[0].items[0].payload.json assert log_item["body"]["stringValue"] == "The recorded value was 'some value'" - assert log_item["attributes"][1] == {"key": "attr_int", "value": {"intValue": "1"}} # TODO: this is strange. - assert log_item["attributes"][2] == {"key": "attr_float", "value": {"doubleValue": 2.0}} - assert log_item["attributes"][3] == {"key": "attr_bool", "value": {"boolValue": True}} - assert log_item["attributes"][4] == {"key": "attr_string", "value": {"stringValue": "string attribute"}} - assert log_item["attributes"][5] == {"key": "sentry.environment", "value": {"stringValue": "production"}} - assert log_item["attributes"][6] == {"key": "sentry.release", "value": {"stringValue": mock.ANY}} - assert log_item["attributes"][7] == {"key": "sentry.message.parameters.my_var", "value": {"stringValue": "some value"}} + assert log_item["attributes"][1] == { + "key": "attr_int", + "value": {"intValue": "1"}, + } # TODO: this is strange. + assert log_item["attributes"][2] == { + "key": "attr_float", + "value": {"doubleValue": 2.0}, + } + assert log_item["attributes"][3] == { + "key": "attr_bool", + "value": {"boolValue": True}, + } + assert log_item["attributes"][4] == { + "key": "attr_string", + "value": {"stringValue": "string attribute"}, + } + assert log_item["attributes"][5] == { + "key": "sentry.environment", + "value": {"stringValue": "production"}, + } + assert log_item["attributes"][6] == { + "key": "sentry.release", + "value": {"stringValue": mock.ANY}, + } + assert log_item["attributes"][7] == { + "key": "sentry.message.parameters.my_var", + "value": {"stringValue": "some value"}, + } def test_logs_message_params(sentry_init, capture_envelopes): """ - This is the official way of how to pass vars to log messages. + This is the official way of how to pass vars to log messages. """ sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() - sentry_logger.warn("The recorded value was '{int_var}'", int_var=1) - sentry_logger.warn("The recorded value was '{float_var}'", float_var=2.0) - sentry_logger.warn("The recorded value was '{bool_var}'", bool_var=False) - sentry_logger.warn("The recorded value was '{string_var}'", string_var="some string value") - - assert envelopes[0].items[0].payload.json["body"]["stringValue"] == "The recorded value was '1'" - assert envelopes[0].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.int_var", "value": {'intValue': "1"}} # TODO: this is strange. + sentry_logger.warn("The recorded value was '{int_var}'", int_var=1) + sentry_logger.warn("The recorded value was '{float_var}'", float_var=2.0) + sentry_logger.warn("The recorded value was '{bool_var}'", bool_var=False) + sentry_logger.warn( + "The recorded value was '{string_var}'", string_var="some string value" + ) - assert envelopes[1].items[0].payload.json["body"]["stringValue"] == "The recorded value was '2.0'" - assert envelopes[1].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.float_var", "value": {'doubleValue': 2.0}} + assert ( + envelopes[0].items[0].payload.json["body"]["stringValue"] + == "The recorded value was '1'" + ) + assert envelopes[0].items[0].payload.json["attributes"][-1] == { + "key": "sentry.message.parameters.int_var", + "value": {"intValue": "1"}, + } # TODO: this is strange. + + assert ( + envelopes[1].items[0].payload.json["body"]["stringValue"] + == "The recorded value was '2.0'" + ) + assert envelopes[1].items[0].payload.json["attributes"][-1] == { + "key": "sentry.message.parameters.float_var", + "value": {"doubleValue": 2.0}, + } - assert envelopes[2].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'False'" - assert envelopes[2].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.bool_var", "value": {'boolValue': False}} + assert ( + envelopes[2].items[0].payload.json["body"]["stringValue"] + == "The recorded value was 'False'" + ) + assert envelopes[2].items[0].payload.json["attributes"][-1] == { + "key": "sentry.message.parameters.bool_var", + "value": {"boolValue": False}, + } - assert envelopes[3].items[0].payload.json["body"]["stringValue"] == "The recorded value was 'some string value'" - assert envelopes[3].items[0].payload.json["attributes"][-1] == {"key": "sentry.message.parameters.string_var", "value": {'stringValue': "some string value"}} + assert ( + envelopes[3].items[0].payload.json["body"]["stringValue"] + == "The recorded value was 'some string value'" + ) + assert envelopes[3].items[0].payload.json["attributes"][-1] == { + "key": "sentry.message.parameters.string_var", + "value": {"stringValue": "some string value"}, + } def test_logs_tied_to_transactions(sentry_init, capture_envelopes): @@ -146,7 +204,10 @@ def test_logs_tied_to_transactions(sentry_init, capture_envelopes): sentry_logger.warn("This is a log tied to a transaction") log_entry = envelopes[0].items[0].payload.json - assert log_entry["attributes"][-1] =={'key': 'sentry.trace.parent_span_id', 'value': {'stringValue': trx.span_id}} + assert log_entry["attributes"][-1] == { + "key": "sentry.trace.parent_span_id", + "value": {"stringValue": trx.span_id}, + } def test_logs_tied_to_spans(sentry_init, capture_envelopes): @@ -161,4 +222,7 @@ def test_logs_tied_to_spans(sentry_init, capture_envelopes): sentry_logger.warn("This is a log tied to a span") log_entry = envelopes[0].items[0].payload.json - assert log_entry["attributes"][-1] =={'key': 'sentry.trace.parent_span_id', 'value': {'stringValue': span.span_id}} + assert log_entry["attributes"][-1] == { + "key": "sentry.trace.parent_span_id", + "value": {"stringValue": span.span_id}, + } From e8168fee3ab9c504bcd5d2d7057d8642cd288f24 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 13:44:40 +0100 Subject: [PATCH 25/28] Linting --- sentry_sdk/consts.py | 1 - tests/test_logs.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index ad399dfa30..20179e2231 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -53,7 +53,6 @@ class CompressionAlgo(Enum): TransactionProcessor, MetricTags, MetricValue, - LogProcessor, ) # Experiments are feature flags to enable and disable certain unstable SDK diff --git a/tests/test_logs.py b/tests/test_logs.py index 580f27876b..fe323c09e5 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -217,7 +217,7 @@ def test_logs_tied_to_spans(sentry_init, capture_envelopes): sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() - with sentry_sdk.start_transaction(name="test-transaction") as trx: + with sentry_sdk.start_transaction(name="test-transaction"): with sentry_sdk.start_span(description="test-span") as span: sentry_logger.warn("This is a log tied to a span") From 194b939cef70bbd7139eea7ec8a092e8635e2bdd Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 14:16:29 +0100 Subject: [PATCH 26/28] If debug is enabled, also print logs to console --- sentry_sdk/client.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 934178660f..517969f944 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -4,6 +4,7 @@ import uuid import random import socket +import logging from collections.abc import Mapping from datetime import datetime, timezone from importlib import import_module @@ -895,6 +896,21 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) "trace_id": None, } # type: Log + 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 From 96b3a13f17bd650e65956762ae7aec6648f094d4 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 14:28:22 +0100 Subject: [PATCH 27/28] Skip the tests below Python 3.7 --- sentry_sdk/client.py | 1 + tests/test_logs.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 517969f944..5bbf919c02 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -896,6 +896,7 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs) "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 = { diff --git a/tests/test_logs.py b/tests/test_logs.py index fe323c09e5..4a9a2efa84 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -1,8 +1,13 @@ +import sys from unittest import mock +import pytest import sentry_sdk from sentry_sdk import _experimental_logger as sentry_logger +if sys.version_info < (3, 7): + pytest.skip("requires python3.7 or higher") + def test_logs_disabled_by_default(sentry_init, capture_envelopes): sentry_init() From 1906ab4e0339ad03598db5f9abfc97b0b3e4b01c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 14 Mar 2025 14:34:47 +0100 Subject: [PATCH 28/28] pytest skipping tests --- tests/test_logs.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_logs.py b/tests/test_logs.py index 4a9a2efa84..173a4028d6 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -5,10 +5,13 @@ import sentry_sdk from sentry_sdk import _experimental_logger as sentry_logger -if sys.version_info < (3, 7): - pytest.skip("requires python3.7 or higher") +minimum_python_37 = pytest.mark.skipif( + sys.version_info < (3, 7), reason="Asyncio tests need Python >= 3.7" +) + +@minimum_python_37 def test_logs_disabled_by_default(sentry_init, capture_envelopes): sentry_init() envelopes = capture_envelopes() @@ -23,6 +26,7 @@ def test_logs_disabled_by_default(sentry_init, capture_envelopes): assert len(envelopes) == 0 +@minimum_python_37 def test_logs_basics(sentry_init, capture_envelopes): sentry_init(_experiments={"enable_sentry_logs": True}) envelopes = capture_envelopes() @@ -57,6 +61,7 @@ def test_logs_basics(sentry_init, capture_envelopes): assert envelopes[5].items[0].payload.json["severityNumber"] == 21 +@minimum_python_37 def test_logs_before_emit_log(sentry_init, capture_envelopes): def _before_log(record, hint): assert list(record.keys()) == [ @@ -96,6 +101,7 @@ def _before_log(record, hint): assert envelopes[3].items[0].payload.json["severityText"] == "warn" +@minimum_python_37 def test_logs_attributes(sentry_init, capture_envelopes): """ Passing arbitrary attributes to log messages. @@ -147,6 +153,7 @@ def test_logs_attributes(sentry_init, capture_envelopes): } +@minimum_python_37 def test_logs_message_params(sentry_init, capture_envelopes): """ This is the official way of how to pass vars to log messages. @@ -198,6 +205,7 @@ def test_logs_message_params(sentry_init, capture_envelopes): } +@minimum_python_37 def test_logs_tied_to_transactions(sentry_init, capture_envelopes): """ Log messages are also tied to transactions. @@ -215,6 +223,7 @@ def test_logs_tied_to_transactions(sentry_init, capture_envelopes): } +@minimum_python_37 def test_logs_tied_to_spans(sentry_init, capture_envelopes): """ Log messages are also tied to spans.