Skip to content

Commit

Permalink
ref: Add typing to sentry.utils.metrics, fix bug where metric tags we…
Browse files Browse the repository at this point in the history
…re lost (#48512)
  • Loading branch information
untitaker authored May 4, 2023
1 parent 6cad520 commit 3b3b9d7
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 79 deletions.
6 changes: 6 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ files = fixtures/mypy-stubs,
src/sentry/killswitches.py,
src/sentry/lang/native/appconnect.py,
src/sentry/mail/notifications.py,
src/sentry/metrics/,
src/sentry/models/debugfile.py,
src/sentry/models/groupsubscription.py,
src/sentry/models/options/,
Expand Down Expand Up @@ -163,6 +164,7 @@ files = fixtures/mypy-stubs,
src/sentry/utils/audit.py,
src/sentry/utils/auth.py,
src/sentry/utils/avatar.py,
src/sentry/utils/cache.py,
src/sentry/utils/codecs.py,
src/sentry/utils/committers.py,
src/sentry/utils/cursors.py,
Expand Down Expand Up @@ -259,6 +261,10 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-rapidjson]
ignore_missing_imports = True
[mypy-statsd]
ignore_missing_imports = True
[mypy-datadog.*]
ignore_missing_imports = True

# TODO: these cause type errors when followed
[mypy-snuba_sdk.*]
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/auth/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def __hash__(self) -> int:
def is_expired(self) -> bool:
return False

@memoize # type: ignore[misc]
@memoize
def user(self) -> AnonymousUser:
user = AnonymousUser()
user.is_active = True
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/integrations/slack/requests/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class SlackActionRequest(SlackRequest):
def type(self) -> str:
return str(self.data.get("type"))

@memoize # type: ignore
@memoize
def callback_data(self) -> JSONData:
"""
We store certain data in ``callback_id`` as JSON. It's a bit hacky, but
Expand Down
36 changes: 30 additions & 6 deletions src/sentry/metrics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,53 @@

from random import random
from threading import local
from typing import Any, Mapping, Optional, Union

from django.conf import settings

Tags = Mapping[str, Any]


class MetricsBackend(local):
def __init__(self, prefix=None):
def __init__(self, prefix: Optional[str] = None) -> None:
if prefix is None:
prefix = settings.SENTRY_METRICS_PREFIX
self.prefix = prefix

def _get_key(self, key):
def _get_key(self, key: str) -> str:
if self.prefix:
return f"{self.prefix}{key}"
return key

def _should_sample(self, sample_rate):
def _should_sample(self, sample_rate: float) -> bool:
return sample_rate >= 1 or random() >= 1 - sample_rate

def incr(self, key, instance=None, tags=None, amount=1, sample_rate=1):
def incr(
self,
key: str,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
amount: Union[float, int] = 1,
sample_rate: float = 1,
) -> None:
raise NotImplementedError

def timing(self, key, value, instance=None, tags=None, sample_rate=1):
def timing(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
raise NotImplementedError

def gauge(self, key, value, instance=None, tags=None, sample_rate=1):
def gauge(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
raise NotImplementedError
67 changes: 45 additions & 22 deletions src/sentry/metrics/datadog.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
__all__ = ["DatadogMetricsBackend"]

from typing import Any, Optional, Union

from datadog import ThreadStats, initialize
from datadog.util.hostname import get_hostname

from sentry.utils.cache import memoize

from .base import MetricsBackend
from .base import MetricsBackend, Tags


class DatadogMetricsBackend(MetricsBackend):
def __init__(self, prefix=None, **kwargs):
def __init__(self, prefix: Optional[str] = None, **kwargs: Any) -> None:
# TODO(dcramer): it'd be nice if the initialize call wasn't a global
self.tags = kwargs.pop("tags", None)
if "host" in kwargs:
Expand All @@ -19,54 +21,75 @@ def __init__(self, prefix=None, **kwargs):
initialize(**kwargs)
super().__init__(prefix=prefix)

def __del__(self):
def __del__(self) -> None:
try:
self.stats.stop()
except TypeError:
# TypeError: 'NoneType' object is not callable
pass

@memoize
def stats(self):
def stats(self) -> ThreadStats:
instance = ThreadStats()
instance.start()
return instance

def incr(self, key, instance=None, tags=None, amount=1, sample_rate=1):
if tags is None:
tags = {}
def incr(
self,
key: str,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
amount: Union[float, int] = 1,
sample_rate: float = 1,
) -> None:
tags = dict(tags or ())

if self.tags:
tags.update(self.tags)
if instance:
tags["instance"] = instance
if tags:
tags = [f"{k}:{v}" for k, v in tags.items()]

tags_list = [f"{k}:{v}" for k, v in tags.items()]
self.stats.increment(
self._get_key(key), amount, sample_rate=sample_rate, tags=tags, host=self.host
self._get_key(key), amount, sample_rate=sample_rate, tags=tags_list, host=self.host
)

def timing(self, key, value, instance=None, tags=None, sample_rate=1):
if tags is None:
tags = {}
def timing(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
tags = dict(tags or ())

if self.tags:
tags.update(self.tags)
if instance:
tags["instance"] = instance
if tags:
tags = [f"{k}:{v}" for k, v in tags.items()]

tags_list = [f"{k}:{v}" for k, v in tags.items()]
self.stats.timing(
self._get_key(key), value, sample_rate=sample_rate, tags=tags, host=self.host
self._get_key(key), value, sample_rate=sample_rate, tags=tags_list, host=self.host
)

def gauge(self, key, value, instance=None, tags=None, sample_rate=1):
if tags is None:
tags = {}
def gauge(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
tags = dict(tags or ())

if self.tags:
tags.update(self.tags)
if instance:
tags["instance"] = instance
if tags:
tags = [f"{k}:{v}" for k, v in tags.items()]

tags_list = [f"{k}:{v}" for k, v in tags.items()]
self.stats.gauge(
self._get_key(key), value, sample_rate=sample_rate, tags=tags, host=self.host
self._get_key(key), value, sample_rate=sample_rate, tags=tags_list, host=self.host
)
65 changes: 44 additions & 21 deletions src/sentry/metrics/dogstatsd.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,69 @@
__all__ = ["DogStatsdMetricsBackend"]
from typing import Any, Optional, Union

from datadog import initialize, statsd

from .base import MetricsBackend
from .base import MetricsBackend, Tags

__all__ = ["DogStatsdMetricsBackend"]


class DogStatsdMetricsBackend(MetricsBackend):
def __init__(self, prefix=None, **kwargs):
def __init__(self, prefix: Optional[str] = None, **kwargs: Any) -> None:
# TODO(dcramer): it'd be nice if the initialize call wasn't a global
self.tags = kwargs.pop("tags", None)
initialize(**kwargs)
super().__init__(prefix=prefix)

def incr(self, key, instance=None, tags=None, amount=1, sample_rate=1):
if tags is None:
tags = {}
def incr(
self,
key: str,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
amount: Union[float, int] = 1,
sample_rate: float = 1,
) -> None:
tags = dict(tags or ())

if self.tags:
tags.update(self.tags)
if instance:
tags["instance"] = instance
if tags:
tags = [f"{k}:{v}" for k, v in tags.items()]
statsd.increment(self._get_key(key), amount, sample_rate=sample_rate, tags=tags)

def timing(self, key, value, instance=None, tags=None, sample_rate=1):
if tags is None:
tags = {}
tags_list = [f"{k}:{v}" for k, v in tags.items()]
statsd.increment(self._get_key(key), amount, sample_rate=sample_rate, tags=tags_list)

def timing(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
tags = dict(tags or ())

if self.tags:
tags.update(self.tags)
if instance:
tags["instance"] = instance
if tags:
tags = [f"{k}:{v}" for k, v in tags.items()]
statsd.timing(self._get_key(key), value, sample_rate=sample_rate, tags=tags)

def gauge(self, key, value, instance=None, tags=None, sample_rate=1):
if tags is None:
tags = {}
tags_list = [f"{k}:{v}" for k, v in tags.items()]
statsd.timing(self._get_key(key), value, sample_rate=sample_rate, tags=tags_list)

def gauge(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
tags = dict(tags or ())

if self.tags:
tags.update(self.tags)
if instance:
tags["instance"] = instance
if tags:
tags = [f"{k}:{v}" for k, v in tags.items()]
statsd.gauge(self._get_key(key), value, sample_rate=sample_rate, tags=tags)

tags_list = [f"{k}:{v}" for k, v in tags.items()]
statsd.gauge(self._get_key(key), value, sample_rate=sample_rate, tags=tags_list)
33 changes: 28 additions & 5 deletions src/sentry/metrics/dummy.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
__all__ = ["DummyMetricsBackend"]
from typing import Optional, Union

from .base import MetricsBackend, Tags

from .base import MetricsBackend
__all__ = ["DummyMetricsBackend"]


class DummyMetricsBackend(MetricsBackend):
def incr(self, key, instance=None, tags=None, amount=1, sample_rate=1):
def incr(
self,
key: str,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
amount: Union[float, int] = 1,
sample_rate: float = 1,
) -> None:
pass

def timing(self, key, value, instance=None, tags=None, sample_rate=1):
def timing(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
pass

def gauge(self, key, value, instance=None, tags=None, sample_rate=1):
def gauge(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
pass
30 changes: 26 additions & 4 deletions src/sentry/metrics/logging.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import logging
from typing import Optional, Union

from .base import MetricsBackend
from .base import MetricsBackend, Tags

logger = logging.getLogger("sentry.metrics")


class LoggingBackend(MetricsBackend):
def incr(self, key, instance=None, tags=None, amount=1, sample_rate=1):
def incr(
self,
key: str,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
amount: Union[float, int] = 1,
sample_rate: float = 1,
) -> None:
logger.debug("%r: %+g", key, amount, extra={"instance": instance, "tags": tags or {}})

def timing(self, key, value, instance=None, tags=None, sample_rate=1):
def timing(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
logger.debug(
"%r: %g ms", key, value * 1000, extra={"instance": instance, "tags": tags or {}}
)

def gauge(self, key, value, instance=None, tags=None, sample_rate=1):
def gauge(
self,
key: str,
value: float,
instance: Optional[str] = None,
tags: Optional[Tags] = None,
sample_rate: float = 1,
) -> None:
logger.debug("%r: %+g", key, value, extra={"instance": instance, "tags": tags or {}})
Loading

0 comments on commit 3b3b9d7

Please sign in to comment.