This repository has been archived by the owner on Dec 15, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add StatsReporter for reporting of runtime monitoring stats
Summary: For monitoring/alerting/debugging, we should be able to report runtime stats cheaply; this adds a simple Stats class for reporting to statsd, along with StatsReporter for app-specific setup. I've also included a few sample uses; not necessarily high value, but illustrative. Happy to remove those if we think they should be added later or not at all. Test Plan: Manual; open to suggestions. Reviewers: ar, vishal Reviewed By: ar, vishal Subscribers: cf, wwu Projects: #changes, #changes-stability Differential Revision: https://tails.corp.dropbox.com/D84470
- Loading branch information
Showing
6 changed files
with
126 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import statsd | ||
import re | ||
import logging | ||
|
||
logger = logging.getLogger('statsreporter') | ||
|
||
|
||
def swallow_exceptions(exn_logger): | ||
"""Decorator to catch, log, and discard any Exceptions raised in a method. | ||
:param exn_logger: logging.Logger to use for logging any exceptions. | ||
""" | ||
def decor(func): | ||
def wrapper(*args, **kwargs): | ||
try: | ||
return func(*args, **kwargs) | ||
except Exception as e: | ||
exn_logger.exception(e) | ||
return wrapper | ||
return decor | ||
|
||
|
||
class StatsReporter(object): | ||
"""StatsReporter is responsible for maintaining an app-specific Stats instance. | ||
The app config should specify: | ||
STATSD_HOST (address of statsd host as a string) | ||
STATSD_PORT (port statsd is listening on as an int) | ||
STATSD_PREFIX (string to be automatically prepended to all reported stats for namespacing) | ||
If STATSD_HOST isn't specified, none of the others will be used and this app will | ||
get a no-op Stats instance. | ||
""" | ||
def __init__(self, app=None): | ||
self.app = app | ||
self._stats = None | ||
if app: | ||
self.init_app(app) | ||
|
||
def init_app(self, app): | ||
if not self._stats and app.config.get('STATSD_HOST'): | ||
sd = statsd.StatsClient(host=app.config['STATSD_HOST'], | ||
prefix=app.config['STATSD_PREFIX'], | ||
port=app.config['STATSD_PORT']) | ||
self._stats = Stats(client=sd) | ||
|
||
def stats(self): | ||
"""Returns a Stats object. | ||
If no statsd config has been provided, | ||
the Stats won't do anything but validate.""" | ||
if self._stats: | ||
return self._stats | ||
return Stats(client=None) | ||
|
||
|
||
class Stats(object): | ||
""" Minimalistic class for sending stats/monitoring values.""" | ||
|
||
def __init__(self, client): | ||
""" | ||
@param client - A statsd.StatsClient instance, or None for a no-op Stats. | ||
""" | ||
# A thin wrapper around Statsd rather than just Statsd so we | ||
# can pick which features to support and how to encode the data. | ||
self._client = client | ||
|
||
@swallow_exceptions(logger) | ||
def set_gauge(self, key, value): | ||
""" Set a gauge, typically a sampled instantaneous value. | ||
@param key - the name of the gauge. | ||
@param value - current value of the gauge. | ||
""" | ||
assert isinstance(value, (int, float, long)) | ||
Stats._check_key(key) | ||
if self._client: | ||
self._client.gauge(key, value) | ||
|
||
@swallow_exceptions(logger) | ||
def incr(self, key, delta=1): | ||
""" Increment a count. | ||
@param key - the name of the stat. | ||
@param delta - amount to increment the stat by. Must be positive. | ||
""" | ||
assert isinstance(delta, (int, float, long)) | ||
assert delta >= 0 | ||
Stats._check_key(key) | ||
if self._client: | ||
self._client.incr(key, delta) | ||
|
||
@swallow_exceptions(logger) | ||
def log_timing(self, key, duration_ms): | ||
""" Record a millisecond timing. """ | ||
assert isinstance(duration_ms, (int, float, long)) | ||
Stats._check_key(key) | ||
if self._client: | ||
self._client.timing(key, duration_ms) | ||
|
||
_KEY_RE = re.compile(r'^[A-Za-z0-9_-]+$') | ||
|
||
@classmethod | ||
def _check_key(cls, key): | ||
""" This is probably overly strict, but we have little use for | ||
interestingly named keys and this avoids unintentionally using them.""" | ||
if not cls._KEY_RE.match(key): | ||
raise Exception("Invalid key: {}".format(repr(key))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters