# statsd.py
# Based on example from https://github.com/etsy/statsd/
# Key format: apps.<app>.<server>.<app-key>
# e.g.: apps.tetra-app.bacon.request
import random
import asyncio
import logging
from micro import di
log = logging.getLogger(__name__)
class StatsdProtocol(asyncio.DatagramProtocol):
def __init__(self, prefix):
self.transport = None
""" :type : asyncio.DatagramTransport """
self.prefix = prefix
def connection_made(self, transport):
self.transport = transport
def connection_lost(self, exc):
self.transport = False
def send(self, data, sample_rate=1):
"""Squirt the metrics over UDP"""
sampled_data = {}
if sample_rate < 1:
if random.random() <= sample_rate:
for stat in data.keys():
value = data[stat]
sampled_data[stat] = '%s|@%s' % (value, sample_rate)
else:
sampled_data = data
try:
for stat in sampled_data.keys():
value = data[stat]
send_data = '%s.%s:%s' % (self.prefix, stat, value)
self.transport.sendto(send_data.encode('utf-8'))
except Exception as e:
log.error('Statsd error %r', e)
[docs]class StatsD(metaclass=di.Component):
"""Sends statistics to the stats daemon over UDP"""
def __init__(self, loop, hostname, host, port, prefix, app_prefix="app"):
self.loop = loop
self.host = host
self.port = port
self.prefix = 'apps.%s.%s' % (prefix, hostname)
self.protocol = None
""" :type protocol: StatsdProtocol """
self.app_prefix = app_prefix
@asyncio.coroutine
def start(self):
log.info('Creating StastD connection to %s:%s with prefix=%s', self.host, self.port, self.prefix)
_, self.protocol = yield from self.loop.create_datagram_endpoint(lambda: StatsdProtocol(self.prefix),
remote_addr=(self.host, self.port))
def stop(self):
if self.protocol:
self.protocol.transport.close()
[docs] def timing(self, stats, time, sample_rate=1):
"""Log timing information
>>> # noinspection PyUnresolvedReferences
>>> timing('some.time', '500')
"""
self.update_stats(stats, time, sample_rate, 'ms')
[docs] def increment(self, stats, sample_rate=1):
""" Increments one or more stats counters
>>> # noinspection PyUnresolvedReferences
>>> increment('some.int')
>>> # noinspection PyUnresolvedReferences
>>> increment('some.int', 0.5)
"""
self.update_stats(stats, 1, sample_rate)
[docs] def decrement(self, stats, sample_rate=1):
"""Decrements one or more stats counters
>>> # noinspection PyUnresolvedReferences
>>> decrement('some.int')
"""
self.update_stats(stats, -1, sample_rate)
[docs] def gauge(self, stats, value, sample_rate=1):
"""Sets one or more gauges to a value
>>> # noinspection PyUnresolvedReferences
>>> gauge('some.int', 'some_value')
"""
self.update_stats(stats, value, sample_rate, 'g')
[docs] def update_stats(self, stats, delta=1, sample_rate=1, metric='c'):
"""Updates one or more stats counters by arbitrary amounts
>>> # noinspection PyUnresolvedReferences
>>> update_stats('some.int', 10)
"""
if type(stats) is not list:
stats = [stats]
data = {}
for stat in stats:
data[stat] = '%s|%s' % (delta, metric)
self.protocol.send(data, sample_rate)
@di.has_deps
[docs]class WrappedStatsD:
"""Sends statistics to the stats daemon over UDP"""
delegate = di.Dep(StatsD)
def __init__(self, key_pattern):
"""
:param key_pattern: The key pattern. Must contain {stats}
"""
self.key = key_pattern
pass
[docs] def timing(self, stats, time, sample_rate=1):
"""Log timing information
>>> # noinspection PyUnresolvedReferences
>>> timing('some.time', '500')
"""
self.update_stats(stats, time, sample_rate, 'ms')
[docs] def increment(self, stats, sample_rate=1):
""" Increments one or more stats counters
>>> # noinspection PyUnresolvedReferences
>>> increment('some.int')
>>> # noinspection PyUnresolvedReferences
>>> increment('some.int', 0.5)
"""
self.update_stats(stats, 1, sample_rate)
[docs] def update_stats(self, stats, delta=1, sample_rate=1, metric='c'):
"""Updates one or more stats counters by arbitrary amounts
>>> # noinspection PyUnresolvedReferences
>>> update_stats('some.int', 10)
"""
key = self.key.format(stats=stats)
if self.delegate:
self.delegate.update_stats(key, delta, sample_rate, metric)
else:
log.debug("Stats: {key} {delta}".format(key=key, delta=delta)) # pragma: no cover