Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add median and percentile graphite methods #122

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ The default options are:
// Default loglevel
"logging": "info",

// Default method (average, last_value, sum, minimum, maximum).
// Default method (average, last_value, sum, minimum, maximum, median, percentile k [eg. percentile 25]).
// Can be redefined for each alert.
"method": "average",

Expand Down Expand Up @@ -225,7 +225,7 @@ Currently two types of alerts are supported:
// (optional) Default values format (none, bytes, s, ms, short)
"format": "bytes",

// (optional) Alert method (average, last_value, sum, minimum, maximum)
// (optional) Alert method (average, last_value, sum, minimum, maximum, median, percentile k [eg. percentile 25])
"method": "average",

// (optional) Alert interval [eg. 15second, 30minute, 2hour, 1day, 3month, 1year]
Expand Down
20 changes: 17 additions & 3 deletions graphite_beacon/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


LOGGER = log.gen_log
METHODS = "average", "last_value", "sum", "minimum", "maximum"
METHODS = "average", "last_value", "sum", "minimum", "maximum", "median", "percentile"
LEVELS = {
'critical': 0,
'warning': 10,
Expand Down Expand Up @@ -241,7 +241,14 @@ def configure(self, **options):
self.default_nan_value = options.get(
'default_nan_value', self.reactor.options['default_nan_value'])
self.ignore_nan = options.get('ignore_nan', self.reactor.options['ignore_nan'])
assert self.method in METHODS, "Method is invalid"
method_tokens = self.method.split(' ', 1)
assert method_tokens[0] in METHODS, "Method is invalid"
if method_tokens[0] == 'percentile':
try:
rank = float(method_tokens[1])
except ValueError:
raise ValueError('Invalid percentile: %s' % method_tokens[1])
assert 0 <= rank <= 100, 'Percentile must be in the range [0,100]'

self.auth_username = self.reactor.options.get('auth_username')
self.auth_password = self.reactor.options.get('auth_password')
Expand All @@ -267,7 +274,7 @@ def load(self):
GraphiteRecord(line.decode('utf-8'), self.default_nan_value, self.ignore_nan)
for line in response.buffer)
data = [
(None if record.empty else getattr(record, self.method), record.target)
(None if record.empty else self._get_record_attr(record), record.target)
for record in records]
if len(data) == 0:
raise ValueError('No data')
Expand All @@ -293,6 +300,13 @@ def _graphite_url(self, query, raw_data=False, graphite_url=None):
url = "{0}&rawData=true".format(url)
return url

def _get_record_attr(self, record):
method_tokens = self.method.split(' ', 1)
if method_tokens[0] == 'percentile':
return record.percentile(float(method_tokens[1]))
else:
return getattr(record, self.method)


class URLAlert(BaseAlert):

Expand Down
12 changes: 12 additions & 0 deletions graphite_beacon/graphite.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,15 @@ def minimum(self):
@property
def maximum(self):
return max(self.values)

@property
def median(self):
return self.percentile(50)

def percentile(self, rank):
values = sorted(self.values)
if rank == 100 or len(self.values) == 1:
return values[-1]
k = int(len(values) * rank / 100.0)
return values[k]

35 changes: 35 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,38 @@ def test_html_template(reactor):
assert message

assert len(message._payload) == 2


def test_graphite_record():
from random import choice, randint
from graphite_beacon.graphite import GraphiteRecord

def record(data):
meta = ','.join('0' * 4)
data = ','.join(map(str, data))
return GraphiteRecord('|'.join([meta, data]))

cases = [
([0], 0, 0),
([0], 50, 0),
([0], 100, 0),
([1, 0], 0, 0),
([1, 0], 49.9, 0),
([1, 0], 50, 1),
([1, 0], 99.9, 1),
([1, 0], 100, 1),
([2, 1, 0], 0, 0),
([2, 1, 0], 33.3, 0),
([2, 1, 0], 33.4, 1),
([2, 1, 0], 66.6, 1),
([2, 1, 0], 66.7, 2),
([2, 1, 0], 99.9, 2),
([2, 1, 0], 100, 2),
]

for (data, rank, result) in cases:
assert record(data).percentile(rank) == result

for _ in range(100):
data = [choice(range(100)) for _ in range(randint(50, 150))]
assert record(data).median == sorted(data)[int(len(data) / 2.0)]