diff --git a/mypy.ini b/mypy.ini index cc2a7513bf66a0..63eb3794f2ee2e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -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/, @@ -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, @@ -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.*] diff --git a/src/sentry/auth/system.py b/src/sentry/auth/system.py index 514367915042b6..69f351cb595d61 100644 --- a/src/sentry/auth/system.py +++ b/src/sentry/auth/system.py @@ -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 diff --git a/src/sentry/integrations/slack/requests/action.py b/src/sentry/integrations/slack/requests/action.py index 0c2946f891bde5..88f77e39354b46 100644 --- a/src/sentry/integrations/slack/requests/action.py +++ b/src/sentry/integrations/slack/requests/action.py @@ -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 diff --git a/src/sentry/metrics/base.py b/src/sentry/metrics/base.py index fccba2a991da87..68e7e9840b59e5 100644 --- a/src/sentry/metrics/base.py +++ b/src/sentry/metrics/base.py @@ -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 diff --git a/src/sentry/metrics/datadog.py b/src/sentry/metrics/datadog.py index 34ecd20746b162..fad51d14056a80 100644 --- a/src/sentry/metrics/datadog.py +++ b/src/sentry/metrics/datadog.py @@ -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: @@ -19,7 +21,7 @@ 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: @@ -27,46 +29,67 @@ def __del__(self): 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 ) diff --git a/src/sentry/metrics/dogstatsd.py b/src/sentry/metrics/dogstatsd.py index 16e8c559207ff4..917e6ee2f02cc0 100644 --- a/src/sentry/metrics/dogstatsd.py +++ b/src/sentry/metrics/dogstatsd.py @@ -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) diff --git a/src/sentry/metrics/dummy.py b/src/sentry/metrics/dummy.py index 2b16d8ed29dfe4..f9324190b2d073 100644 --- a/src/sentry/metrics/dummy.py +++ b/src/sentry/metrics/dummy.py @@ -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 diff --git a/src/sentry/metrics/logging.py b/src/sentry/metrics/logging.py index ca09f2d95b56be..9af76ef68937dd 100644 --- a/src/sentry/metrics/logging.py +++ b/src/sentry/metrics/logging.py @@ -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 {}}) diff --git a/src/sentry/metrics/statsd.py b/src/sentry/metrics/statsd.py index 19c0e537163ce4..80d68d91773544 100644 --- a/src/sentry/metrics/statsd.py +++ b/src/sentry/metrics/statsd.py @@ -1,25 +1,48 @@ __all__ = ["StatsdMetricsBackend"] +from typing import Any, Optional, Union + import statsd -from .base import MetricsBackend +from .base import MetricsBackend, Tags class StatsdMetricsBackend(MetricsBackend): - def __init__(self, host="127.0.0.1", port=8125, **kwargs): + def __init__(self, host: str = "127.0.0.1", port: int = 8125, **kwargs: Any) -> None: self.client = statsd.StatsClient(host=host, port=port) super().__init__(**kwargs) - def _full_key(self, key, instance=None): + def _full_key(self, key: str, instance: Optional[str] = None) -> str: if instance: return f"{key}.{instance}" return key - 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: self.client.incr(self._full_key(self._get_key(key)), amount, sample_rate) - 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: self.client.timing(self._full_key(self._get_key(key)), value, sample_rate) - 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: self.client.gauge(self._full_key(self._get_key(key)), value, sample_rate) diff --git a/src/sentry/utils/arroyo.py b/src/sentry/utils/arroyo.py index be505f380f3a3a..4334f30c567879 100644 --- a/src/sentry/utils/arroyo.py +++ b/src/sentry/utils/arroyo.py @@ -42,10 +42,12 @@ def increment( self, name: str, value: Union[int, float] = 1, tags: Optional[Tags] = None ) -> None: # sentry metrics backend uses `incr` instead of `increment` - self.__backend.incr(self.__merge_name(name), value, self.__merge_tags(tags)) + self.__backend.incr(key=self.__merge_name(name), amount=value, tags=self.__merge_tags(tags)) def gauge(self, name: str, value: Union[int, float], tags: Optional[Tags] = None) -> None: - self.__backend.gauge(self.__merge_name(name), value, self.__merge_tags(tags)) + self.__backend.gauge(key=self.__merge_name(name), value=value, tags=self.__merge_tags(tags)) def timing(self, name: str, value: Union[int, float], tags: Optional[Tags] = None) -> None: - self.__backend.timing(self.__merge_name(name), value, self.__merge_tags(tags)) + self.__backend.timing( + key=self.__merge_name(name), value=value, tags=self.__merge_tags(tags) + ) diff --git a/src/sentry/utils/cache.py b/src/sentry/utils/cache.py index f2e10d874359ec..28f4701d136ea9 100644 --- a/src/sentry/utils/cache.py +++ b/src/sentry/utils/cache.py @@ -1,9 +1,15 @@ +from typing import Any, Callable, Generic, Mapping, TypeVar + from django.core.cache import cache +__all__ = ["cache", "memoize", "default_cache", "cache_key_for_event"] + default_cache = cache +T = TypeVar("T") + -class memoize: +class memoize(Generic[T]): """ Memoize the result of a property call. @@ -13,18 +19,18 @@ class memoize: >>> return 'foo' """ - def __init__(self, func): + def __init__(self, func: Callable[[Any], T]) -> None: if isinstance(func, classmethod) or isinstance(func, staticmethod): - func = func.__func__ + func = func.__func__ # type: ignore self.__name__ = func.__name__ self.__module__ = func.__module__ self.__doc__ = func.__doc__ self.func = func - def __get__(self, obj, type=None): + def __get__(self, obj: Any, type: Any = None) -> T: if obj is None: - return self + return self # type: ignore d, n = vars(obj), self.__name__ if n not in d: value = self.func(obj) @@ -33,5 +39,5 @@ def __get__(self, obj, type=None): return value -def cache_key_for_event(data) -> str: +def cache_key_for_event(data: Mapping[str, Any]) -> str: return "e:{}:{}".format(data["event_id"], data["project"]) diff --git a/src/sentry/utils/metrics.py b/src/sentry/utils/metrics.py index 43156279525b2d..8ae43b0f649dae 100644 --- a/src/sentry/utils/metrics.py +++ b/src/sentry/utils/metrics.py @@ -17,6 +17,7 @@ MutableMapping, Optional, Tuple, + Type, TypeVar, Union, ) @@ -141,7 +142,7 @@ def _get_current_global_tags() -> MutableTags: def get_default_backend() -> MetricsBackend: from sentry.utils.imports import import_string - cls = import_string(settings.SENTRY_METRICS_BACKEND) + cls: Type[MetricsBackend] = import_string(settings.SENTRY_METRICS_BACKEND) return cls(**settings.SENTRY_METRICS_OPTIONS) diff --git a/src/sentry/utils/redis_metrics.py b/src/sentry/utils/redis_metrics.py index 0a8e74cf1d6f4c..7a64e26faa0233 100644 --- a/src/sentry/utils/redis_metrics.py +++ b/src/sentry/utils/redis_metrics.py @@ -33,10 +33,12 @@ def __merge_tags(self, tags: Optional[Tags]) -> Optional[Tags]: def increment( self, name: str, value: Union[int, float] = 1, tags: Optional[Tags] = None ) -> None: - self.__backend.incr(self.__merge_name(name), value, self.__merge_tags(tags)) + self.__backend.incr(key=self.__merge_name(name), amount=value, tags=self.__merge_tags(tags)) def gauge(self, name: str, value: Union[int, float], tags: Optional[Tags] = None) -> None: - self.__backend.gauge(self.__merge_name(name), value, self.__merge_tags(tags)) + self.__backend.gauge(key=self.__merge_name(name), value=value, tags=self.__merge_tags(tags)) def timing(self, name: str, value: Union[int, float], tags: Optional[Tags] = None) -> None: - self.__backend.timing(self.__merge_name(name), value, self.__merge_tags(tags)) + self.__backend.timing( + key=self.__merge_name(name), value=value, tags=self.__merge_tags(tags) + )