Files
pretix_original/src/pretix/base/metrics.py
2017-03-27 22:31:20 +02:00

241 lines
7.6 KiB
Python
Executable File

import math
from collections import defaultdict
from django.apps import apps
from django.conf import settings
if settings.HAS_REDIS:
import django_redis
redis = django_redis.get_redis_connection("redis")
REDIS_KEY = "pretix_metrics"
_INF = float("inf")
_MINUS_INF = float("-inf")
def _float_to_go_string(d):
# inspired by https://github.com/prometheus/client_python/blob/master/prometheus_client/core.py
if d == _INF:
return '+Inf'
elif d == _MINUS_INF:
return '-Inf'
elif math.isnan(d):
return 'NaN'
else:
return repr(float(d))
class Metric(object):
"""
Base Metrics Object
"""
def __init__(self, name, helpstring, labelnames=None):
self.name = name
self.helpstring = helpstring
self.labelnames = labelnames or []
def __repr__(self):
return self.name + "{" + ",".join(self.labelnames) + "}"
def _check_label_consistency(self, labels):
"""
Checks if the given labels provides exactly the labels that are required.
"""
# test if every required label is provided
for labelname in self.labelnames:
if labelname not in labels:
raise ValueError("Label {0} not specified.".format(labelname))
# now test if no further labels are required
if len(labels) != len(self.labelnames):
raise ValueError("Unknown labels used: {}".format(", ".join(set(labels) - set(self.labelnames))))
def _construct_metric_identifier(self, metricname, labels=None, labelnames=None):
"""
Constructs the scrapable metricname usable in the output format.
"""
if not labels:
return metricname
else:
named_labels = []
for labelname in (labelnames or self.labelnames):
named_labels.append('{}="{}"'.format(labelname, labels[labelname]))
return metricname + "{" + ",".join(named_labels) + "}"
def _inc_in_redis(self, key, amount, pipeline=None):
"""
Increments given key in Redis.
"""
if settings.HAS_REDIS:
if not pipeline:
pipeline = redis
pipeline.hincrbyfloat(REDIS_KEY, key, amount)
def _set_in_redis(self, key, value, pipeline=None):
"""
Sets given key in Redis.
"""
if settings.HAS_REDIS:
if not pipeline:
pipeline = redis
pipeline.hset(REDIS_KEY, key, value)
def _get_redis_pipeline(self):
if settings.HAS_REDIS:
return redis.pipeline()
def _execute_redis_pipeline(self, pipeline):
if settings.HAS_REDIS:
return pipeline.execute()
class Counter(Metric):
"""
Counter Metric Object
Counters can only be increased, they can neither be set to a specific value
nor decreased.
"""
def inc(self, amount=1, **kwargs):
"""
Increments Counter by given amount for the labels specified in kwargs.
"""
if amount < 0:
raise ValueError("Counter cannot be increased by negative values.")
self._check_label_consistency(kwargs)
fullmetric = self._construct_metric_identifier(self.name, kwargs)
self._inc_in_redis(fullmetric, amount)
class Gauge(Metric):
"""
Gauge Metric Object
Gauges can be set to a specific value, increased and decreased.
"""
def set(self, value=1, **kwargs):
"""
Sets Gauge to a specific value for the labels specified in kwargs.
"""
self._check_label_consistency(kwargs)
fullmetric = self._construct_metric_identifier(self.name, kwargs)
self._set_in_redis(fullmetric, value)
def inc(self, amount=1, **kwargs):
"""
Increments Gauge by given amount for the labels specified in kwargs.
"""
if amount < 0:
raise ValueError("Amount must be greater than zero. Otherwise use dec().")
self._check_label_consistency(kwargs)
fullmetric = self._construct_metric_identifier(self.name, kwargs)
self._inc_in_redis(fullmetric, amount)
def dec(self, amount=1, **kwargs):
"""
Decrements Gauge by given amount for the labels specified in kwargs.
"""
if amount < 0:
raise ValueError("Amount must be greater than zero. Otherwise use inc().")
self._check_label_consistency(kwargs)
fullmetric = self._construct_metric_identifier(self.name, kwargs)
self._inc_in_redis(fullmetric, amount * -1)
class Histogram(Metric):
"""
Histogram Metric Object
"""
def __init__(self, name, helpstring, labelnames=None,
buckets=(.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, 30.0, _INF)):
if list(buckets) != sorted(buckets):
# This is probably an error on the part of the user,
# so raise rather than sorting for them.
raise ValueError('Buckets not in sorted order')
if buckets and buckets[-1] != _INF:
buckets.append(_INF)
if len(buckets) < 2:
raise ValueError('Must have at least two buckets')
self.buckets = buckets
super().__init__(name, helpstring, labelnames)
def observe(self, amount, **kwargs):
"""
Stores a value in the histogram for the labels specified in kwargs.
"""
if amount < 0:
raise ValueError("Amount must be greater than zero. Otherwise use inc().")
self._check_label_consistency(kwargs)
pipe = self._get_redis_pipeline()
countmetric = self._construct_metric_identifier(self.name + '_count', kwargs)
self._inc_in_redis(countmetric, 1, pipeline=pipe)
summetric = self._construct_metric_identifier(self.name + '_sum', kwargs)
self._inc_in_redis(summetric, amount, pipeline=pipe)
kwargs_le = dict(kwargs.items())
for i, bound in enumerate(self.buckets):
if amount <= bound:
kwargs_le['le'] = _float_to_go_string(bound)
bmetric = self._construct_metric_identifier(self.name + '_bucket', kwargs_le,
labelnames=self.labelnames + ["le"])
self._inc_in_redis(bmetric, 1, pipeline=pipe)
self._execute_redis_pipeline(pipe)
def metric_values():
"""
Produces the the values to be presented to the monitoring system
"""
metrics = defaultdict(dict)
# Metrics from redis
if settings.HAS_REDIS:
for key, value in redis.hscan_iter(REDIS_KEY):
dkey = key.decode("utf-8")
splitted = dkey.split("{", 2)
value = float(value.decode("utf-8"))
metrics[splitted[0]]["{" + splitted[1]] = value
# Aliases
aliases = {
'pretix_view_requests_total': 'pretix_view_duration_seconds_count'
}
for a, atarget in aliases.items():
metrics[a] = metrics[atarget]
# Throwaway metrics
for m in apps.get_models(): # Count all models
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
return metrics
"""
Provided metrics
"""
pretix_view_duration_seconds = Histogram("pretix_view_duration_seconds", "Return time of views.",
["status_code", "method", "url_name"])
pretix_task_runs_total = Counter("pretix_task_runs_total", "Total calls to a celery task",
["task_name", "status"])
pretix_task_duration_seconds = Histogram("pretix_task_duration_seconds", "Call time of a celery task",
["task_name"])