From 270be5985f049d444fc161dd1fdfea674aaf647a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 10 Dec 2025 16:42:27 +0100 Subject: [PATCH] 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."""