From 270be5985f049d444fc161dd1fdfea674aaf647a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 10 Dec 2025 16:42:27 +0100 Subject: [PATCH 01/21] ref: Make logs, metrics go via scope --- sentry_sdk/_types.py | 25 +++-- sentry_sdk/client.py | 150 +++++------------------------ sentry_sdk/integrations/logging.py | 2 +- sentry_sdk/integrations/loguru.py | 2 +- sentry_sdk/logger.py | 6 +- sentry_sdk/metrics.py | 4 +- sentry_sdk/scope.py | 118 ++++++++++++++++++++++- 7 files changed, 163 insertions(+), 144 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 0426bf7a93..7c2f17a06a 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -222,13 +222,26 @@ class SDKInfo(TypedDict): # TODO: Make a proper type definition for this (PRs welcome!) Hint = Dict[str, Any] + AttributeValue = ( + str | bool | float | int | list[str] | list[bool] | list[float] | list[int] + ) + Attributes = dict[str, AttributeValue] + + SerializedAttributeValue = TypedDict( + "SerializedAttributeValue", + { + "type": Literal["string", "boolean", "double", "integer"], + "value": AttributeValue, + }, + ) + Log = TypedDict( "Log", { "severity_text": str, "severity_number": int, "body": str, - "attributes": dict[str, str | bool | float | int], + "attributes": Attributes, "time_unix_nano": int, "trace_id": Optional[str], }, @@ -236,14 +249,6 @@ class SDKInfo(TypedDict): MetricType = Literal["counter", "gauge", "distribution"] - MetricAttributeValue = TypedDict( - "MetricAttributeValue", - { - "value": Union[str, bool, float, int], - "type": Literal["string", "boolean", "double", "integer"], - }, - ) - Metric = TypedDict( "Metric", { @@ -254,7 +259,7 @@ class SDKInfo(TypedDict): "type": MetricType, "value": float, "unit": Optional[str], - "attributes": dict[str, str | bool | float | int], + "attributes": Attributes, }, ) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ad682b1979..209d5cdd0d 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -932,137 +932,39 @@ def capture_event( return return_value - def _capture_log(self, log): - # type: (Optional[Log]) -> None - if not has_logs_enabled(self.options) or log is None: + def _capture_telemetry(self, telemetry, type_, scope): + # type: (Telemetry, str, Scope) -> None + # Capture attributes-based telemetry (logs, metrics, spansV2) + before_send_getter = { + "log": lambda: get_before_send_log(self.options), + "metric": lambda: get_before_send_metric(self.options), + }.get(type_) + + if before_send_getter is not None: + before_send = before_send_getter() + if before_send is not None: + telemetry = before_send(telemetry, {}) + + if telemetry is None: return - current_scope = sentry_sdk.get_current_scope() - isolation_scope = sentry_sdk.get_isolation_scope() - - log["attributes"]["sentry.sdk.name"] = SDK_INFO["name"] - log["attributes"]["sentry.sdk.version"] = SDK_INFO["version"] - - server_name = self.options.get("server_name") - if server_name is not None and SPANDATA.SERVER_ADDRESS not in log["attributes"]: - log["attributes"][SPANDATA.SERVER_ADDRESS] = server_name - - environment = self.options.get("environment") - if environment is not None and "sentry.environment" not in log["attributes"]: - log["attributes"]["sentry.environment"] = environment - - release = self.options.get("release") - if release is not None and "sentry.release" not in log["attributes"]: - log["attributes"]["sentry.release"] = release - - trace_context = current_scope.get_trace_context() - trace_id = trace_context.get("trace_id") - span_id = trace_context.get("span_id") - - if trace_id is not None and log.get("trace_id") is None: - log["trace_id"] = trace_id - - if ( - span_id is not None - and "sentry.trace.parent_span_id" not in log["attributes"] - ): - log["attributes"]["sentry.trace.parent_span_id"] = span_id - - # The user, if present, is always set on the isolation scope. - if isolation_scope._user is not None: - for log_attribute, user_attribute in ( - ("user.id", "id"), - ("user.name", "username"), - ("user.email", "email"), - ): - if ( - user_attribute in isolation_scope._user - and log_attribute not in log["attributes"] - ): - log["attributes"][log_attribute] = isolation_scope._user[ - user_attribute - ] - - # If debug is enabled, log the log to the console - debug = self.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" - ) + scope.apply_to_telemetry(telemetry) - before_send_log = get_before_send_log(self.options) - if before_send_log is not None: - log = before_send_log(log, {}) + batcher = { + "log": self.log_batcher, + "metric": self.metrics_batcher, + }.get(type_) # type: Optional[LogBatcher, MetricsBatcher] - if log is None: - return + if batcher: + batcher.add(telemetry) - if self.log_batcher: - self.log_batcher.add(log) + def _capture_log(self, log, scope): + # type: (Optional[Log], Scope) -> None + self._capture_telemetry(log, "log", scope) - def _capture_metric(self, metric): + def _capture_metric(self, metric, scope): # type: (Optional[Metric]) -> None - if not has_metrics_enabled(self.options) or metric is None: - return - - current_scope = sentry_sdk.get_current_scope() - isolation_scope = sentry_sdk.get_isolation_scope() - - metric["attributes"]["sentry.sdk.name"] = SDK_INFO["name"] - metric["attributes"]["sentry.sdk.version"] = SDK_INFO["version"] - - server_name = self.options.get("server_name") - if ( - server_name is not None - and SPANDATA.SERVER_ADDRESS not in metric["attributes"] - ): - metric["attributes"][SPANDATA.SERVER_ADDRESS] = server_name - - environment = self.options.get("environment") - if environment is not None and "sentry.environment" not in metric["attributes"]: - metric["attributes"]["sentry.environment"] = environment - - release = self.options.get("release") - if release is not None and "sentry.release" not in metric["attributes"]: - metric["attributes"]["sentry.release"] = release - - trace_context = current_scope.get_trace_context() - trace_id = trace_context.get("trace_id") - span_id = trace_context.get("span_id") - - metric["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000" - if span_id is not None: - metric["span_id"] = span_id - - if isolation_scope._user is not None: - for metric_attribute, user_attribute in ( - ("user.id", "id"), - ("user.name", "username"), - ("user.email", "email"), - ): - if ( - user_attribute in isolation_scope._user - and metric_attribute not in metric["attributes"] - ): - metric["attributes"][metric_attribute] = isolation_scope._user[ - user_attribute - ] - - debug = self.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" - ) - - before_send_metric = get_before_send_metric(self.options) - if before_send_metric is not None: - metric = before_send_metric(metric, {}) - - if metric is None: - return - - if self.metrics_batcher: - self.metrics_batcher.add(metric) + self._capture_telemetry(metric, "metric", scope) def capture_session( self, diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 9c68596be8..e40086c065 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -409,7 +409,7 @@ def _capture_log_from_record(self, client, record): attrs["logger.name"] = record.name # noinspection PyProtectedMember - client._capture_log( + sentry_sdk.get_current_scope()._capture_log( { "severity_text": otel_severity_text, "severity_number": otel_severity_number, diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 96d2b6a7ae..193bc82e4a 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -201,7 +201,7 @@ def loguru_sentry_logs_handler(message): else: attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) - client._capture_log( + sentry_sdk.get_current_scope()._capture_log( { "severity_text": otel_severity_text, "severity_number": otel_severity_number, diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index b90ac034bb..4903bf5a35 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -3,7 +3,7 @@ import time from typing import Any -from sentry_sdk import get_client +import sentry_sdk from sentry_sdk.utils import safe_repr, capture_internal_exceptions OTEL_RANGES = [ @@ -28,8 +28,6 @@ def __missing__(self, key): def _capture_log(severity_text, severity_number, template, **kwargs): # type: (str, int, str, **Any) -> None - client = get_client() - body = template attrs = {} # type: dict[str, str | bool | float | int] if "attributes" in kwargs: @@ -58,7 +56,7 @@ def _capture_log(severity_text, severity_number, template, **kwargs): } # noinspection PyProtectedMember - client._capture_log( + sentry_sdk.get_current_scope()._capture_log( { "severity_text": severity_text, "severity_number": severity_number, diff --git a/sentry_sdk/metrics.py b/sentry_sdk/metrics.py index 03bde137bd..63c681264f 100644 --- a/sentry_sdk/metrics.py +++ b/sentry_sdk/metrics.py @@ -21,8 +21,6 @@ def _capture_metric( attributes=None, # type: Optional[dict[str, Any]] ): # type: (...) -> None - client = sentry_sdk.get_client() - attrs = {} # type: dict[str, Union[str, bool, float, int]] if attributes: for k, v in attributes.items(): @@ -48,7 +46,7 @@ def _capture_metric( "attributes": attrs, } # type: Metric - client._capture_metric(metric) + sentry_sdk.get_current_scope()._capture_metric(metric) def count( diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 466e1b5b12..a8cb1af790 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -11,7 +11,12 @@ from sentry_sdk._types import AnnotatedValue from sentry_sdk.attachments import Attachment -from sentry_sdk.consts import DEFAULT_MAX_BREADCRUMBS, FALSE_VALUES, INSTRUMENTER +from sentry_sdk.consts import ( + DEFAULT_MAX_BREADCRUMBS, + FALSE_VALUES, + INSTRUMENTER, + SPANDATA, +) from sentry_sdk.feature_flags import FlagBuffer, DEFAULT_FLAG_CAPACITY from sentry_sdk.profiler.continuous_profiler import ( get_profiler_id, @@ -42,6 +47,8 @@ event_from_exception, exc_info_from_error, logger, + has_logs_enabled, + has_metrics_enabled, ) import typing @@ -1239,6 +1246,57 @@ def capture_event(self, event, hint=None, scope=None, **scope_kwargs): return event_id + def _capture_log(self, log, scope=None, **scope_kwargs): + # type: (Optional[Log], Optional[Scope], Any) -> None + if log is None: + return + + client = self.get_client() + if not has_logs_enabled(client.options) or log is None: + return + + scope = self._merge_scopes(scope, scope_kwargs) + + trace_context = scope.get_trace_context() + trace_id = trace_context.get("trace_id") + if trace_id is not None and log.get("trace_id") is None: + log["trace_id"] = trace_id + + # If debug is enabled, log the log to the console + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" + ) + + client._capture_log(log, scope=scope) + + def _capture_metric(self, metric, scope=None, **scope_kwargs): + # type: (Optional[Metric], Optional[Scope], Any) -> None + if metric is None: + return + + client = self.get_client() + if not has_metrics_enabled(client.options): + return + + scope = self._merge_scopes(scope, scope_kwargs) + + trace_context = scope.get_trace_context() + trace_id = trace_context.get("trace_id") + span_id = trace_context.get("span_id") + metric["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000" + if span_id is not None: + metric["span_id"] = span_id + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" + ) + + client._capture_metric(metric, scope=scope) + def capture_message(self, message, level=None, scope=None, **scope_kwargs): # type: (str, Optional[LogLevelStr], Optional[Scope], Any) -> Optional[str] """ @@ -1470,6 +1528,54 @@ def _apply_flags_to_event(self, event, hint, options): {"values": flags} ) + def _apply_global_attributes_to_telemetry(self, telemetry, options): + # TODO: Global stuff like this should just be retrieved at init time and + # put onto the global scope's attributes + # TODO: These attrs will actually be saved on and retrieved from + # the global scope directly in a later step instead of constructing + # them anew + from sentry_sdk.client import SDK_INFO + + attributes = telemetry["attributes"] + + attributes["sentry.sdk.name"] = SDK_INFO["name"] + attributes["sentry.sdk.version"] = SDK_INFO["version"] + + server_name = options.get("server_name") + if server_name is not None: + attributes[SPANDATA.SERVER_ADDRESS] = server_name + + environment = options.get("environment") + if environment is not None: + attributes["sentry.environment"] = environment + + release = options.get("release") + if release is not None: + attributes["sentry.release"] = release + + def _apply_tracing_attributes_to_telemetry(self, telemetry): + attributes = telemetry["attributes"] + + trace_context = self.get_trace_context() + span_id = trace_context.get("span_id") + + if span_id is not None and "sentry.trace_parent_span_id" not in attributes: + attributes["sentry.trace.parent_span_id"] = span_id + + def _apply_user_attributes_to_telemetry(self, telemetry): + attributes = telemetry["attributes"] + + if self._user is None: + return + + for attribute_name, user_attribute in ( + ("user.id", "id"), + ("user.name", "username"), + ("user.email", "email"), + ): + if user_attribute in self._user and attribute_name not in attributes: + attributes[attribute_name] = self._user[user_attribute] + def _drop(self, cause, ty): # type: (Any, str) -> Optional[Any] logger.info("%s (%s) dropped event", ty, cause) @@ -1580,6 +1686,16 @@ def apply_to_event( return event + @_disable_capture + def apply_to_telemetry(self, telemetry): + # Attributes-based events and telemetry go through here (logs, metrics, + # spansV2) + options = self.get_client().options + + self._apply_global_attributes_to_telemetry(telemetry, options) + self._apply_tracing_attributes_to_telemetry(telemetry) + self._apply_user_attributes_to_telemetry(telemetry) + def update_from_scope(self, scope): # type: (Scope) -> None """Update the scope with another scope's data.""" From 329ea2c1f61946684722df155c3182c860a6004c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 14:51:01 +0100 Subject: [PATCH 02/21] typing fixes --- sentry_sdk/_types.py | 3 ++- sentry_sdk/client.py | 12 +++++++----- sentry_sdk/metrics.py | 2 +- sentry_sdk/scope.py | 15 ++++++++++----- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 323db1fbcc..31a57e04f6 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -216,7 +216,8 @@ class SDKInfo(TypedDict): Hint = Dict[str, Any] AttributeValue = ( - str | bool | float | int | list[str] | list[bool] | list[float] | list[int] + str | bool | float | int + # TODO: relay support coming soon for: list[str] | list[bool] | list[float] | list[int] ) Attributes = dict[str, AttributeValue] diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index bf6b4cb2ce..471a5b8059 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -68,7 +68,6 @@ SDKInfo, Log, Metric, - Telemetry, EventDataCategory, ) from sentry_sdk.integrations import Integration @@ -225,10 +224,10 @@ def is_active(self) -> bool: def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": return None - def _capture_log(self, log: "Log") -> None: + def _capture_log(self, log: "Log", scope: "Scope") -> None: pass - def _capture_metric(self, metric: "Metric") -> None: + def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: pass def capture_session(self, *args: "Any", **kwargs: "Any") -> None: @@ -907,9 +906,12 @@ def capture_event( return return_value def _capture_telemetry( - self, telemetry: "Telemetry", ty: str, scope: "Scope" + self, telemetry: "Optional[Union[Log, Metric]]", ty: str, scope: "Scope" ) -> None: # Capture attributes-based telemetry (logs, metrics, spansV2) + if telemetry is None: + return + before_send_getter = { "log": lambda: get_before_send_log(self.options), "metric": lambda: get_before_send_metric(self.options), @@ -925,7 +927,7 @@ def _capture_telemetry( scope.apply_to_telemetry(telemetry) - batcher: "Optional[LogBatcher, MetricsBatcher]" = { + batcher: "Optional[Union[LogBatcher, MetricsBatcher]]" = { "log": self.log_batcher, "metric": self.metrics_batcher, }.get(ty) diff --git a/sentry_sdk/metrics.py b/sentry_sdk/metrics.py index 24841c1dbd..e7f7a3bea7 100644 --- a/sentry_sdk/metrics.py +++ b/sentry_sdk/metrics.py @@ -18,7 +18,7 @@ def _capture_metric( metric_type: "MetricType", value: float, unit: "Optional[str]" = None, - attributes: "Attributes" = None, + attributes: "Optional[Attributes]" = None, ) -> None: attrs: "Attributes" = {} diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 0e9053a72b..194dc69943 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -84,7 +84,6 @@ LogLevelStr, Metric, SamplingContext, - Telemetry, Type, ) @@ -1487,7 +1486,9 @@ def _apply_flags_to_event( {"values": flags} ) - def _apply_global_attributes_to_telemetry(self, telemetry, options): + def _apply_global_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric]", options: "dict[str, Any]" + ) -> None: # TODO: Global stuff like this should just be retrieved at init time and # put onto the global scope's attributes and then applied to events # from there @@ -1510,7 +1511,9 @@ def _apply_global_attributes_to_telemetry(self, telemetry, options): if release is not None: attributes["sentry.release"] = release - def _apply_tracing_attributes_to_telemetry(self, telemetry): + def _apply_tracing_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric]" + ) -> None: attributes = telemetry["attributes"] trace_context = self.get_trace_context() @@ -1519,7 +1522,9 @@ def _apply_tracing_attributes_to_telemetry(self, telemetry): if span_id is not None and "sentry.trace_parent_span_id" not in attributes: attributes["sentry.trace.parent_span_id"] = span_id - def _apply_user_attributes_to_telemetry(self, telemetry): + def _apply_user_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric]" + ) -> None: attributes = telemetry["attributes"] if not should_send_default_pii() or self._user is None: @@ -1640,7 +1645,7 @@ def apply_to_event( return event @_disable_capture - def apply_to_telemetry(self, telemetry: "Telemetry") -> None: + def apply_to_telemetry(self, telemetry: "Union[Log, Metric]") -> None: # Attributes-based events and telemetry go through here (logs, metrics, # spansV2) options = self.get_client().options From 6c1897a99a912d2dc97ca9952a289d77db7c74f0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 14:57:28 +0100 Subject: [PATCH 03/21] giving up on typing dispatches --- sentry_sdk/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 471a5b8059..ef8a21fbe8 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -920,7 +920,7 @@ def _capture_telemetry( if before_send_getter is not None: before_send = before_send_getter() if before_send is not None: - telemetry = before_send(telemetry, {}) + telemetry = before_send(telemetry, {}) # type: ignore[arg-type] if telemetry is None: return @@ -933,7 +933,7 @@ def _capture_telemetry( }.get(ty) if batcher: - batcher.add(telemetry) + batcher.add(telemetry) # type: ignore[arg-type] def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: self._capture_telemetry(log, "log", scope) From 8ffc78ad805134d9cc63caa37b12db6ef244e3d7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 15:01:19 +0100 Subject: [PATCH 04/21] span_id is not an attribute anymore --- sentry_sdk/scope.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 194dc69943..6620afccd1 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1511,17 +1511,6 @@ def _apply_global_attributes_to_telemetry( if release is not None: attributes["sentry.release"] = release - def _apply_tracing_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric]" - ) -> None: - attributes = telemetry["attributes"] - - trace_context = self.get_trace_context() - span_id = trace_context.get("span_id") - - if span_id is not None and "sentry.trace_parent_span_id" not in attributes: - attributes["sentry.trace.parent_span_id"] = span_id - def _apply_user_attributes_to_telemetry( self, telemetry: "Union[Log, Metric]" ) -> None: @@ -1650,8 +1639,15 @@ def apply_to_telemetry(self, telemetry: "Union[Log, Metric]") -> None: # spansV2) options = self.get_client().options + trace_context = self.get_trace_context() + trace_id = trace_context.get("span_id") + span_id = trace_context.get("span_id") + if telemetry.get("trace_id") is None: + telemetry["trace_id"] = trace_id + if telemetry.get("span_id") is None: + telemetry["span_id"] = span_id + self._apply_global_attributes_to_telemetry(telemetry, options) - self._apply_tracing_attributes_to_telemetry(telemetry) self._apply_user_attributes_to_telemetry(telemetry) def update_from_scope(self, scope: "Scope") -> None: From 2405282583f8539adc4054cfb74e40443fb7c84d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 15:04:15 +0100 Subject: [PATCH 05/21] move format_attributes to utils --- sentry_sdk/_log_batcher.py | 13 +------------ sentry_sdk/_metrics_batcher.py | 13 +------------ sentry_sdk/utils.py | 22 +++++++++++++++++++++- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/sentry_sdk/_log_batcher.py b/sentry_sdk/_log_batcher.py index aee9b1db6f..d8e1590523 100644 --- a/sentry_sdk/_log_batcher.py +++ b/sentry_sdk/_log_batcher.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from typing import Optional, List, Callable, TYPE_CHECKING, Any -from sentry_sdk.utils import format_timestamp, safe_repr +from sentry_sdk.utils import format_attribute, format_timestamp, safe_repr from sentry_sdk.envelope import Envelope, Item, PayloadRef if TYPE_CHECKING: @@ -115,17 +115,6 @@ def flush(self) -> None: @staticmethod def _log_to_transport_format(log: "Log") -> "Any": - def format_attribute(val: "int | float | str | bool") -> "Any": - if isinstance(val, bool): - return {"value": val, "type": "boolean"} - if isinstance(val, int): - return {"value": val, "type": "integer"} - if isinstance(val, float): - return {"value": val, "type": "double"} - if isinstance(val, str): - return {"value": val, "type": "string"} - return {"value": safe_repr(val), "type": "string"} - if "sentry.severity_number" not in log["attributes"]: log["attributes"]["sentry.severity_number"] = log["severity_number"] if "sentry.severity_text" not in log["attributes"]: diff --git a/sentry_sdk/_metrics_batcher.py b/sentry_sdk/_metrics_batcher.py index 3bac514ed2..36526e8aa9 100644 --- a/sentry_sdk/_metrics_batcher.py +++ b/sentry_sdk/_metrics_batcher.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from typing import Optional, List, Callable, TYPE_CHECKING, Any, Union -from sentry_sdk.utils import format_timestamp, safe_repr +from sentry_sdk.utils import format_attribute, format_timestamp, safe_repr from sentry_sdk.envelope import Envelope, Item, PayloadRef if TYPE_CHECKING: @@ -96,17 +96,6 @@ def flush(self) -> None: @staticmethod def _metric_to_transport_format(metric: "Metric") -> "Any": - def format_attribute(val: "Union[int, float, str, bool]") -> "Any": - if isinstance(val, bool): - return {"value": val, "type": "boolean"} - if isinstance(val, int): - return {"value": val, "type": "integer"} - if isinstance(val, float): - return {"value": val, "type": "double"} - if isinstance(val, str): - return {"value": val, "type": "string"} - return {"value": safe_repr(val), "type": "string"} - res = { "timestamp": metric["timestamp"], "trace_id": metric["trace_id"], diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 551504fa8a..8f2d01bdce 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -59,7 +59,15 @@ from gevent.hub import Hub - from sentry_sdk._types import Event, ExcInfo, Hint, Log, Metric + from sentry_sdk._types import ( + AttributeValue, + SerializedAttributeValue, + Event, + ExcInfo, + Hint, + Log, + Metric, + ) P = ParamSpec("P") R = TypeVar("R") @@ -2036,3 +2044,15 @@ def get_before_send_metric( return options.get("before_send_metric") or options["_experiments"].get( "before_send_metric" ) + + +def format_attribute(val: "AttributeValue") -> "SerializedAttributeValue": + if isinstance(val, bool): + return {"value": val, "type": "boolean"} + if isinstance(val, int): + return {"value": val, "type": "integer"} + if isinstance(val, float): + return {"value": val, "type": "double"} + if isinstance(val, str): + return {"value": val, "type": "string"} + return {"value": safe_repr(val), "type": "string"} From a0a603bae572404e4acd4389b6808b1bb2a34229 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 15:19:37 +0100 Subject: [PATCH 06/21] attr list values --- sentry_sdk/_types.py | 14 +++++++++++--- sentry_sdk/utils.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 31a57e04f6..4115ee2723 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -216,15 +216,23 @@ class SDKInfo(TypedDict): Hint = Dict[str, Any] AttributeValue = ( - str | bool | float | int - # TODO: relay support coming soon for: list[str] | list[bool] | list[float] | list[int] + str | bool | float | int | list[str] | list[bool] | list[float] | list[int] ) Attributes = dict[str, AttributeValue] SerializedAttributeValue = TypedDict( "SerializedAttributeValue", { - "type": Literal["string", "boolean", "double", "integer"], + "type": Literal[ + "string", + "boolean", + "double", + "integer", + "string[]", + "boolean[]", + "double[]", + "integer[]", + ], "value": AttributeValue, }, ) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 8f2d01bdce..8635f607fb 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -2055,4 +2055,21 @@ def format_attribute(val: "AttributeValue") -> "SerializedAttributeValue": return {"value": val, "type": "double"} if isinstance(val, str): return {"value": val, "type": "string"} + + if isinstance(val, list): + if not val: + return {"value": val, "type": str} + + # Only lists of elements of a single type are supported + list_types = { + str: "string[]", + int: "integer[]", + float: "double[]", + bool: "boolean[]", + } + + ty = type(val[0]) + if ty in list_types and all(isinstance(v, ty) for v in val): + return {"value": val, "type": list_types[ty]} + return {"value": safe_repr(val), "type": "string"} From a2fee7c4f9cf61e2301ce7450cfacb7255a70977 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 15:27:30 +0100 Subject: [PATCH 07/21] . --- sentry_sdk/logger.py | 26 ++++++++------------------ sentry_sdk/utils.py | 6 ++++-- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index 4bf7d5c4bd..5187283ce9 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -1,11 +1,15 @@ # NOTE: this is the logger sentry exposes to users, not some generic logger. import functools import time -from typing import Any +from typing import Any, TYPE_CHECKING import sentry_sdk from sentry_sdk.utils import safe_repr, capture_internal_exceptions +if TYPE_CHECKING: + from sentry_sdk._types import Attributes, Log + + OTEL_RANGES = [ # ((severity level range), severity text) # https://opentelemetry.io/docs/specs/otel/logs/data-model @@ -29,7 +33,8 @@ def _capture_log( severity_text: str, severity_number: int, template: str, **kwargs: "Any" ) -> None: body = template - attrs: "dict[str, str | bool | float | int]" = {} + + attrs: "Attributes" = {} if "attributes" in kwargs: attrs.update(kwargs.pop("attributes")) for k, v in kwargs.items(): @@ -41,21 +46,6 @@ def _capture_log( with capture_internal_exceptions(): body = template.format_map(_dict_default_key(kwargs)) - attrs = { - k: ( - v - if ( - isinstance(v, str) - or isinstance(v, int) - or isinstance(v, bool) - or isinstance(v, float) - ) - else safe_repr(v) - ) - for (k, v) in attrs.items() - } - - # noinspection PyProtectedMember sentry_sdk.get_current_scope()._capture_log( { "severity_text": severity_text, @@ -65,7 +55,7 @@ def _capture_log( "time_unix_nano": time.time_ns(), "trace_id": None, "span_id": None, - }, + } ) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 8635f607fb..0840a04356 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -45,6 +45,7 @@ Dict, Iterator, List, + Literal, NoReturn, Optional, ParamSpec, @@ -2058,10 +2059,10 @@ def format_attribute(val: "AttributeValue") -> "SerializedAttributeValue": if isinstance(val, list): if not val: - return {"value": val, "type": str} + return {"value": val, "type": "string[]"} # Only lists of elements of a single type are supported - list_types = { + list_types: 'dict[type, Literal["string[]", "integer[]", "double[]", "boolean[]"]]' = { str: "string[]", int: "integer[]", float: "double[]", @@ -2072,4 +2073,5 @@ def format_attribute(val: "AttributeValue") -> "SerializedAttributeValue": if ty in list_types and all(isinstance(v, ty) for v in val): return {"value": val, "type": list_types[ty]} + # Coerce to string if we don't know what to do with the value return {"value": safe_repr(val), "type": "string"} From 87537f05e48abdc5530b4f933e0b0baa8a596126 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 16:49:08 +0100 Subject: [PATCH 08/21] add link --- sentry_sdk/_types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 4115ee2723..daeb87a6ab 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -221,6 +221,7 @@ class SDKInfo(TypedDict): Attributes = dict[str, AttributeValue] SerializedAttributeValue = TypedDict( + # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types "SerializedAttributeValue", { "type": Literal[ From 8c3283316b6b8eed495c4377a1348ab0525fca79 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 16 Dec 2025 16:53:33 +0100 Subject: [PATCH 09/21] remove custom trace_id, span_id setting --- sentry_sdk/scope.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 6620afccd1..cd7db829ce 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1196,14 +1196,6 @@ def _capture_log( scope = self._merge_scopes(scope, scope_kwargs) - trace_context = scope.get_trace_context() - trace_id = trace_context.get("trace_id") - span_id = trace_context.get("span_id") - if trace_id is not None and log.get("trace_id") is None: - log["trace_id"] = trace_id - if span_id is not None and log.get("span_id") is None: - log["span_id"] = span_id - # If debug is enabled, log the log to the console debug = client.options.get("debug", False) if debug: @@ -1228,13 +1220,6 @@ def _capture_metric( scope = self._merge_scopes(scope, scope_kwargs) - trace_context = scope.get_trace_context() - trace_id = trace_context.get("trace_id") - span_id = trace_context.get("span_id") - metric["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000" - if span_id is not None: - metric["span_id"] = span_id - debug = client.options.get("debug", False) if debug: logger.debug( From 3747c5f24e34aec7be6df8bd15fa49c280cf93cb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Dec 2025 09:14:53 +0100 Subject: [PATCH 10/21] rename, fix --- sentry_sdk/_log_batcher.py | 4 ++-- sentry_sdk/_metrics_batcher.py | 4 ++-- sentry_sdk/scope.py | 8 ++++---- sentry_sdk/utils.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/_log_batcher.py b/sentry_sdk/_log_batcher.py index d8e1590523..51886f48f9 100644 --- a/sentry_sdk/_log_batcher.py +++ b/sentry_sdk/_log_batcher.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from typing import Optional, List, Callable, TYPE_CHECKING, Any -from sentry_sdk.utils import format_attribute, format_timestamp, safe_repr +from sentry_sdk.utils import format_timestamp, safe_repr, serialize_attribute from sentry_sdk.envelope import Envelope, Item, PayloadRef if TYPE_CHECKING: @@ -127,7 +127,7 @@ def _log_to_transport_format(log: "Log") -> "Any": "level": str(log["severity_text"]), "body": str(log["body"]), "attributes": { - k: format_attribute(v) for (k, v) in log["attributes"].items() + k: serialize_attribute(v) for (k, v) in log["attributes"].items() }, } diff --git a/sentry_sdk/_metrics_batcher.py b/sentry_sdk/_metrics_batcher.py index 36526e8aa9..6cbac0cbce 100644 --- a/sentry_sdk/_metrics_batcher.py +++ b/sentry_sdk/_metrics_batcher.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from typing import Optional, List, Callable, TYPE_CHECKING, Any, Union -from sentry_sdk.utils import format_attribute, format_timestamp, safe_repr +from sentry_sdk.utils import format_timestamp, safe_repr, serialize_attribute from sentry_sdk.envelope import Envelope, Item, PayloadRef if TYPE_CHECKING: @@ -103,7 +103,7 @@ def _metric_to_transport_format(metric: "Metric") -> "Any": "type": metric["type"], "value": metric["value"], "attributes": { - k: format_attribute(v) for (k, v) in metric["attributes"].items() + k: serialize_attribute(v) for (k, v) in metric["attributes"].items() }, } diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index cd7db829ce..fcaae9a7f3 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1625,11 +1625,11 @@ def apply_to_telemetry(self, telemetry: "Union[Log, Metric]") -> None: options = self.get_client().options trace_context = self.get_trace_context() - trace_id = trace_context.get("span_id") - span_id = trace_context.get("span_id") + trace_id = trace_context.get("trace_id") if telemetry.get("trace_id") is None: - telemetry["trace_id"] = trace_id - if telemetry.get("span_id") is None: + telemetry["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000" + span_id = trace_context.get("span_id") + if telemetry.get("span_id") is None and span_id: telemetry["span_id"] = span_id self._apply_global_attributes_to_telemetry(telemetry, options) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 0840a04356..78da303b29 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -2047,7 +2047,7 @@ def get_before_send_metric( ) -def format_attribute(val: "AttributeValue") -> "SerializedAttributeValue": +def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue": if isinstance(val, bool): return {"value": val, "type": "boolean"} if isinstance(val, int): From 4dc5dd876c0741adf639340c4dbabd76fa918a75 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Dec 2025 09:26:05 +0100 Subject: [PATCH 11/21] . --- sentry_sdk/client.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ef8a21fbe8..08161eac6b 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -62,14 +62,7 @@ from typing import Union from typing import TypeVar - from sentry_sdk._types import ( - Event, - Hint, - SDKInfo, - Log, - Metric, - EventDataCategory, - ) + from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory from sentry_sdk.integrations import Integration from sentry_sdk.scope import Scope from sentry_sdk.session import Session From 6acb51007a496cbad17ec37b01773a45d4ee58ab Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Dec 2025 09:29:21 +0100 Subject: [PATCH 12/21] simplify --- sentry_sdk/scope.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index fcaae9a7f3..c6747745c6 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1181,12 +1181,7 @@ def capture_event( return event_id - def _capture_log( - self, - log: "Optional[Log]", - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> None: + def _capture_log(self, log: "Optional[Log]") -> None: if log is None: return @@ -1194,7 +1189,7 @@ def _capture_log( if not has_logs_enabled(client.options) or log is None: return - scope = self._merge_scopes(scope, scope_kwargs) + scope = self._merge_scopes() # If debug is enabled, log the log to the console debug = client.options.get("debug", False) @@ -1205,12 +1200,7 @@ def _capture_log( client._capture_log(log, scope=scope) - def _capture_metric( - self, - metric: "Optional[Metric]", - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> None: + def _capture_metric(self, metric: "Optional[Metric]") -> None: if metric is None: return @@ -1218,7 +1208,7 @@ def _capture_metric( if not has_metrics_enabled(client.options): return - scope = self._merge_scopes(scope, scope_kwargs) + scope = self._merge_scopes() debug = client.options.get("debug", False) if debug: From 2bf0f3a27d0f3c5faa8b1c26697e3f41bb6563c1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Dec 2025 09:32:40 +0100 Subject: [PATCH 13/21] . --- sentry_sdk/scope.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c6747745c6..0eb3b9505e 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1186,19 +1186,18 @@ def _capture_log(self, log: "Optional[Log]") -> None: return client = self.get_client() - if not has_logs_enabled(client.options) or log is None: + if not has_logs_enabled(client.options): return - scope = self._merge_scopes() + merged_scope = self._merge_scopes() - # If debug is enabled, log the log to the console debug = client.options.get("debug", False) if debug: logger.debug( f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" ) - client._capture_log(log, scope=scope) + client._capture_log(log, scope=merged_scope) def _capture_metric(self, metric: "Optional[Metric]") -> None: if metric is None: @@ -1208,7 +1207,7 @@ def _capture_metric(self, metric: "Optional[Metric]") -> None: if not has_metrics_enabled(client.options): return - scope = self._merge_scopes() + merged_scope = self._merge_scopes() debug = client.options.get("debug", False) if debug: @@ -1216,7 +1215,7 @@ def _capture_metric(self, metric: "Optional[Metric]") -> None: f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" ) - client._capture_metric(metric, scope=scope) + client._capture_metric(metric, scope=merged_scope) def capture_message( self, From c83d76c64c3963c6f7c6954da30ef1789142eb73 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Dec 2025 09:59:31 +0100 Subject: [PATCH 14/21] . --- sentry_sdk/scope.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 0eb3b9505e..d4bb99fb57 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1474,15 +1474,15 @@ def _apply_global_attributes_to_telemetry( attributes["sentry.sdk.version"] = SDK_INFO["version"] server_name = options.get("server_name") - if server_name is not None: + if server_name is not None and SPANDATA.SERVER_ADDRESS not in attributes: attributes[SPANDATA.SERVER_ADDRESS] = server_name environment = options.get("environment") - if environment is not None: + if environment is not None and "sentry.environment" not in attributes: attributes["sentry.environment"] = environment release = options.get("release") - if release is not None: + if release is not None and "sentry.release" not in attributes: attributes["sentry.release"] = release def _apply_user_attributes_to_telemetry( From a62d90b897a3683803750b03c396af9fe642107d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Dec 2025 10:42:24 +0100 Subject: [PATCH 15/21] first attrs, then before_send --- sentry_sdk/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 08161eac6b..bb9dd0b117 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -905,6 +905,8 @@ def _capture_telemetry( if telemetry is None: return + scope.apply_to_telemetry(telemetry) + before_send_getter = { "log": lambda: get_before_send_log(self.options), "metric": lambda: get_before_send_metric(self.options), @@ -918,8 +920,6 @@ def _capture_telemetry( if telemetry is None: return - scope.apply_to_telemetry(telemetry) - batcher: "Optional[Union[LogBatcher, MetricsBatcher]]" = { "log": self.log_batcher, "metric": self.metrics_batcher, From 649c3add024996018c211671ec06c32862c03c01 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 17 Dec 2025 10:47:06 +0100 Subject: [PATCH 16/21] dont pass opts around --- sentry_sdk/scope.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index d4bb99fb57..a933159919 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1461,7 +1461,7 @@ def _apply_flags_to_event( ) def _apply_global_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric]", options: "dict[str, Any]" + self, telemetry: "Union[Log, Metric]" ) -> None: # TODO: Global stuff like this should just be retrieved at init time and # put onto the global scope's attributes and then applied to events @@ -1473,6 +1473,8 @@ def _apply_global_attributes_to_telemetry( attributes["sentry.sdk.name"] = SDK_INFO["name"] attributes["sentry.sdk.version"] = SDK_INFO["version"] + options = self.get_client().options + server_name = options.get("server_name") if server_name is not None and SPANDATA.SERVER_ADDRESS not in attributes: attributes[SPANDATA.SERVER_ADDRESS] = server_name @@ -1611,8 +1613,6 @@ def apply_to_event( def apply_to_telemetry(self, telemetry: "Union[Log, Metric]") -> None: # Attributes-based events and telemetry go through here (logs, metrics, # spansV2) - options = self.get_client().options - trace_context = self.get_trace_context() trace_id = trace_context.get("trace_id") if telemetry.get("trace_id") is None: @@ -1621,7 +1621,7 @@ def apply_to_telemetry(self, telemetry: "Union[Log, Metric]") -> None: if telemetry.get("span_id") is None and span_id: telemetry["span_id"] = span_id - self._apply_global_attributes_to_telemetry(telemetry, options) + self._apply_global_attributes_to_telemetry(telemetry) self._apply_user_attributes_to_telemetry(telemetry) def update_from_scope(self, scope: "Scope") -> None: From 3fffa7fc502f805a526f6bcee6e3a312e5422f0e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Dec 2025 08:20:31 +0100 Subject: [PATCH 17/21] simplify dispatcher --- sentry_sdk/client.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index bb9dd0b117..259196d1c6 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -907,26 +907,26 @@ def _capture_telemetry( scope.apply_to_telemetry(telemetry) - before_send_getter = { - "log": lambda: get_before_send_log(self.options), - "metric": lambda: get_before_send_metric(self.options), - }.get(ty) + before_send = None + if ty == "log": + before_send = get_before_send_log(self.options) + elif ty == "metric": + before_send = get_before_send_metric(self.options) # type: ignore - if before_send_getter is not None: - before_send = before_send_getter() - if before_send is not None: - telemetry = before_send(telemetry, {}) # type: ignore[arg-type] + if before_send is not None: + telemetry = before_send(telemetry, {}) # type: ignore if telemetry is None: return - batcher: "Optional[Union[LogBatcher, MetricsBatcher]]" = { - "log": self.log_batcher, - "metric": self.metrics_batcher, - }.get(ty) + batcher = None + if ty == "log": + batcher = self.log_batcher + elif ty == "metric": + batcher = self.metrics_batcher # type: ignore - if batcher: - batcher.add(telemetry) # type: ignore[arg-type] + if batcher is not None: + batcher.add(telemetry) # type: ignore def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: self._capture_telemetry(log, "log", scope) From 7ccbd5a89a01539e1603c19f5cdb1886647c5954 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Dec 2025 09:43:09 +0100 Subject: [PATCH 18/21] no support for array attributes yet --- sentry_sdk/_types.py | 13 ++++++++----- sentry_sdk/utils.py | 16 ---------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index daeb87a6ab..5a8cb936ac 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -216,7 +216,9 @@ class SDKInfo(TypedDict): Hint = Dict[str, Any] AttributeValue = ( - str | bool | float | int | list[str] | list[bool] | list[float] | list[int] + str | bool | float | int + # TODO: relay support coming soon for + # | list[str] | list[bool] | list[float] | list[int] ) Attributes = dict[str, AttributeValue] @@ -229,10 +231,11 @@ class SDKInfo(TypedDict): "boolean", "double", "integer", - "string[]", - "boolean[]", - "double[]", - "integer[]", + # TODO: relay support coming soon for: + # "string[]", + # "boolean[]", + # "double[]", + # "integer[]", ], "value": AttributeValue, }, diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 78da303b29..4965a13c0a 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -2057,21 +2057,5 @@ def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue": if isinstance(val, str): return {"value": val, "type": "string"} - if isinstance(val, list): - if not val: - return {"value": val, "type": "string[]"} - - # Only lists of elements of a single type are supported - list_types: 'dict[type, Literal["string[]", "integer[]", "double[]", "boolean[]"]]' = { - str: "string[]", - int: "integer[]", - float: "double[]", - bool: "boolean[]", - } - - ty = type(val[0]) - if ty in list_types and all(isinstance(v, ty) for v in val): - return {"value": val, "type": list_types[ty]} - # Coerce to string if we don't know what to do with the value return {"value": safe_repr(val), "type": "string"} From 9d8b3d2f938f67dcb22ce16396293d0ccfe8e7f1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Dec 2025 11:05:28 +0100 Subject: [PATCH 19/21] put preserialization back --- sentry_sdk/logger.py | 7 +++++-- sentry_sdk/metrics.py | 11 +---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index 5187283ce9..bc5f39224f 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -35,8 +35,11 @@ def _capture_log( body = template attrs: "Attributes" = {} - if "attributes" in kwargs: - attrs.update(kwargs.pop("attributes")) + + if kwargs.get("attributes"): + for k, v in kwargs.pop("attributes").items(): + attrs[k] = v if isinstance(v, (str, int, bool, float)) else safe_repr(v) + for k, v in kwargs.items(): attrs[f"sentry.message.parameter.{k}"] = v if kwargs: diff --git a/sentry_sdk/metrics.py b/sentry_sdk/metrics.py index e7f7a3bea7..de40136590 100644 --- a/sentry_sdk/metrics.py +++ b/sentry_sdk/metrics.py @@ -24,16 +24,7 @@ def _capture_metric( if attributes: for k, v in attributes.items(): - attrs[k] = ( - v - if ( - isinstance(v, str) - or isinstance(v, int) - or isinstance(v, bool) - or isinstance(v, float) - ) - else safe_repr(v) - ) + attrs[k] = v if isinstance(v, (str, int, bool, float)) else safe_repr(v) metric: "Metric" = { "timestamp": time.time(), From 0b25f4654ffb1e55e3059ed871b698b16599232e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Dec 2025 11:15:53 +0100 Subject: [PATCH 20/21] preserialize after template, parameters --- sentry_sdk/logger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index bc5f39224f..82178ecf89 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -37,11 +37,11 @@ def _capture_log( attrs: "Attributes" = {} if kwargs.get("attributes"): - for k, v in kwargs.pop("attributes").items(): - attrs[k] = v if isinstance(v, (str, int, bool, float)) else safe_repr(v) + attrs.update(kwargs.pop("attributes")) for k, v in kwargs.items(): attrs[f"sentry.message.parameter.{k}"] = v + if kwargs: # only attach template if there are parameters attrs["sentry.message.template"] = template @@ -49,6 +49,9 @@ def _capture_log( with capture_internal_exceptions(): body = template.format_map(_dict_default_key(kwargs)) + for k, v in attrs.items(): + attrs[k] = v if isinstance(v, (str, int, bool, float)) else safe_repr(v) + sentry_sdk.get_current_scope()._capture_log( { "severity_text": severity_text, From ef5f9fb59dc6ce75310ec21b56af206265727525 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 18 Dec 2025 11:17:37 +0100 Subject: [PATCH 21/21] fix --- sentry_sdk/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index 82178ecf89..b446ec7893 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -36,7 +36,7 @@ def _capture_log( attrs: "Attributes" = {} - if kwargs.get("attributes"): + if "attributes" in kwargs: attrs.update(kwargs.pop("attributes")) for k, v in kwargs.items():