From 36c2650ccc6edcd300e2d207d7123b12c8b77b27 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 8 Nov 2023 16:51:12 +0100 Subject: [PATCH] feat(metrics): Unify datetime format (#2409) This somewhat unifies the APIs with regards to timestamps. The span system uses datetime objects, this now also permits these values in metrics and vice versa.* feat(metrics): Allow metrics emission for spans --------- Co-authored-by: Anton Pirker --- sentry_sdk/metrics.py | 24 ++++++++++++++---------- sentry_sdk/tracing.py | 20 +++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/metrics.py b/sentry_sdk/metrics.py index fe8e86b345..0b0abee51b 100644 --- a/sentry_sdk/metrics.py +++ b/sentry_sdk/metrics.py @@ -5,13 +5,14 @@ import random import time import zlib +from datetime import datetime from functools import wraps, partial from threading import Event, Lock, Thread from contextlib import contextmanager +import sentry_sdk from sentry_sdk._compat import text_type -from sentry_sdk.hub import Hub -from sentry_sdk.utils import now, nanosecond_time +from sentry_sdk.utils import now, nanosecond_time, to_timestamp from sentry_sdk.envelope import Envelope, Item from sentry_sdk.tracing import ( TRANSACTION_SOURCE_ROUTE, @@ -29,6 +30,7 @@ from typing import Optional from typing import Generator from typing import Tuple + from typing import Union from sentry_sdk._types import BucketKey from sentry_sdk._types import DurationUnit @@ -406,7 +408,7 @@ def add( value, # type: MetricValue unit, # type: MeasurementUnit tags, # type: Optional[MetricTags] - timestamp=None, # type: Optional[float] + timestamp=None, # type: Optional[Union[float, datetime]] ): # type: (...) -> None if not self._ensure_thread() or self._flusher is None: @@ -414,6 +416,8 @@ def add( if timestamp is None: timestamp = time.time() + elif isinstance(timestamp, datetime): + timestamp = to_timestamp(timestamp) bucket_timestamp = int( (timestamp // self.ROLLUP_IN_SECONDS) * self.ROLLUP_IN_SECONDS @@ -500,7 +504,7 @@ def _serialize_tags( def _get_aggregator_and_update_tags(key, tags): # type: (str, Optional[MetricTags]) -> Tuple[Optional[MetricsAggregator], Optional[MetricTags]] """Returns the current metrics aggregator if there is one.""" - hub = Hub.current + hub = sentry_sdk.Hub.current client = hub.client if client is None or client.metrics_aggregator is None: return None, tags @@ -531,7 +535,7 @@ def incr( value=1.0, # type: float unit="none", # type: MeasurementUnit tags=None, # type: Optional[MetricTags] - timestamp=None, # type: Optional[float] + timestamp=None, # type: Optional[Union[float, datetime]] ): # type: (...) -> None """Increments a counter.""" @@ -545,7 +549,7 @@ def __init__( self, key, # type: str tags, # type: Optional[MetricTags] - timestamp, # type: Optional[float] + timestamp, # type: Optional[Union[float, datetime]] value, # type: Optional[float] unit, # type: DurationUnit ): @@ -597,7 +601,7 @@ def timing( value=None, # type: Optional[float] unit="second", # type: DurationUnit tags=None, # type: Optional[MetricTags] - timestamp=None, # type: Optional[float] + timestamp=None, # type: Optional[Union[float, datetime]] ): # type: (...) -> _Timing """Emits a distribution with the time it takes to run the given code block. @@ -620,7 +624,7 @@ def distribution( value, # type: float unit="none", # type: MeasurementUnit tags=None, # type: Optional[MetricTags] - timestamp=None, # type: Optional[float] + timestamp=None, # type: Optional[Union[float, datetime]] ): # type: (...) -> None """Emits a distribution.""" @@ -634,7 +638,7 @@ def set( value, # type: MetricValue unit="none", # type: MeasurementUnit tags=None, # type: Optional[MetricTags] - timestamp=None, # type: Optional[float] + timestamp=None, # type: Optional[Union[float, datetime]] ): # type: (...) -> None """Emits a set.""" @@ -648,7 +652,7 @@ def gauge( value, # type: float unit="none", # type: MetricValue tags=None, # type: Optional[MetricTags] - timestamp=None, # type: Optional[float] + timestamp=None, # type: Optional[Union[float, datetime]] ): # type: (...) -> None """Emits a gauge.""" diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 704339286f..3bdb46f6f6 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,7 +1,7 @@ import uuid import random -from datetime import timedelta +from datetime import datetime, timedelta import sentry_sdk from sentry_sdk.consts import INSTRUMENTER @@ -14,13 +14,13 @@ if TYPE_CHECKING: import typing - from datetime import datetime from typing import Any from typing import Dict from typing import Iterator from typing import List from typing import Optional from typing import Tuple + from typing import Union import sentry_sdk.profiler from sentry_sdk._types import Event, MeasurementUnit, SamplingContext @@ -131,7 +131,7 @@ def __init__( status=None, # type: Optional[str] transaction=None, # type: Optional[str] # deprecated containing_transaction=None, # type: Optional[Transaction] - start_timestamp=None, # type: Optional[datetime] + start_timestamp=None, # type: Optional[Union[datetime, float]] ): # type: (...) -> None self.trace_id = trace_id or uuid.uuid4().hex @@ -146,7 +146,11 @@ def __init__( self._tags = {} # type: Dict[str, str] self._data = {} # type: Dict[str, Any] self._containing_transaction = containing_transaction - self.start_timestamp = start_timestamp or datetime_utcnow() + if start_timestamp is None: + start_timestamp = datetime.utcnow() + elif isinstance(start_timestamp, float): + start_timestamp = datetime.utcfromtimestamp(start_timestamp) + self.start_timestamp = start_timestamp try: # profiling depends on this value and requires that # it is measured in nanoseconds @@ -439,7 +443,7 @@ def is_success(self): return self.status == "ok" def finish(self, hub=None, end_timestamp=None): - # type: (Optional[sentry_sdk.Hub], Optional[datetime]) -> Optional[str] + # type: (Optional[sentry_sdk.Hub], Optional[Union[float, datetime]]) -> Optional[str] # Note: would be type: (Optional[sentry_sdk.Hub]) -> None, but that leads # to incompatible return types for Span.finish and Transaction.finish. """Sets the end timestamp of the span. @@ -463,6 +467,8 @@ def finish(self, hub=None, end_timestamp=None): try: if end_timestamp: + if isinstance(end_timestamp, float): + end_timestamp = datetime.utcfromtimestamp(end_timestamp) self.timestamp = end_timestamp else: elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns @@ -627,7 +633,7 @@ def containing_transaction(self): return self def finish(self, hub=None, end_timestamp=None): - # type: (Optional[sentry_sdk.Hub], Optional[datetime]) -> Optional[str] + # type: (Optional[sentry_sdk.Hub], Optional[Union[float, datetime]]) -> Optional[str] """Finishes the transaction and sends it to Sentry. All finished spans in the transaction will also be sent to Sentry. @@ -935,7 +941,7 @@ def get_trace_context(self): return {} def finish(self, hub=None, end_timestamp=None): - # type: (Optional[sentry_sdk.Hub], Optional[datetime]) -> Optional[str] + # type: (Optional[sentry_sdk.Hub], Optional[Union[float, datetime]]) -> Optional[str] pass def set_measurement(self, name, value, unit=""):