Merge lp:~lucio.torre/txstatsd/add-sli into lp:txstatsd

Proposed by Lucio Torre
Status: Merged
Approved by: Lucio Torre
Approved revision: 100
Merged at revision: 95
Proposed branch: lp:~lucio.torre/txstatsd/add-sli
Merge into: lp:txstatsd
Diff against target: 271 lines (+235/-0)
5 files modified
twisted/plugins/distinct_plugin.py (+1/-0)
twisted/plugins/sli_plugin.py (+75/-0)
txstatsd/metrics/slimetric.py (+61/-0)
txstatsd/server/router.py (+1/-0)
txstatsd/tests/metrics/test_sli.py (+97/-0)
To merge this branch: bzr merge lp:~lucio.torre/txstatsd/add-sli
Reviewer Review Type Date Requested Status
Sidnei da Silva Approve
Review via email: mp+108388@code.launchpad.net

Commit message

SLI metrics to measure service performance.

Description of the change

SLI metrics to measure service performance.

It will produce metrics with counts of values that match conditions, so you can then derive the service level based on that.

You should first use routing to redirect metrics that you are interested in to be of type sli and then add something like this to the configuration:

[plugin_sli]
rules =
   test => red IF below 5
   test => green IF between 0.1 3
   other => red IF above 4
   *t* => yellow IF above 3

To post a comment you must log in.
Revision history for this message
Sidnei da Silva (sidnei) wrote :

Not super happy about the count_* naming. What about:

 count -> count.total
 count_%s -> count.%s

The rules parsing also doesn't look great. Simply using split() like that is bound to give cryptic errors on badly formed rules. I'd replace it by a regex and some strict checking on each of the items with explicit errors raised if one is missing.

review: Needs Fixing
Revision history for this message
Lucio Torre (lucio.torre) wrote :

On 06/04/2012 02:05 PM, Sidnei da Silva wrote:
> Review: Needs Fixing
>
> Not super happy about the count_* naming. What about:
>
> count -> count.total
> count_%s -> count.%s
that makes a rule named total collide with the total.

> The rules parsing also doesn't look great. Simply using split() like that is bound to give cryptic errors on badly formed rules. I'd replace it by a regex and some strict checking on each of the items with explicit errors raised if one is missing.
i am not comfortable with regexps enough to do that.

Revision history for this message
Sidnei da Silva (sidnei) wrote :

Simply forbid a rule named total?

re.search('(?P<name>[\w]+)\s+IF\s+(?P<condition>(above|between|below))\s+(?P<value1>[\d\.]+)(\s+(?P<value2>[\d\.]+))?', 'green IF between 0.1 3').groupdict()

If re.search() is None: raise ValueError('invalid rule: %r' % rule)
If value2 is None and condition == between: raise ValueError('missing 2nd argument to between condition')
If name == 'total': raise ValueError('total is a reserved name')

What else?

Revision history for this message
Lucio Torre (lucio.torre) wrote :

ah, well, i did the regexp thing before seeing your other comment, got over the lazyness.

having total be a reserved word breaks uniformity and removes "count" from the name, that shows that what we are looking at is a count.

Revision history for this message
Sidnei da Silva (sidnei) :
review: Approve
Revision history for this message
Sidnei da Silva (sidnei) wrote :

Tested IRL, missing '_-' from the regex.

Revision history for this message
Sidnei da Silva (sidnei) wrote :

Also, make sure prefix contains a dot:

if prefix:
   prefix += "."

in build_metric.

lp:~lucio.torre/txstatsd/add-sli updated
99. By Lucio Torre

report rates not counts, will depend on proper carbon schema config for bigger buckets

100. By Lucio Torre

better regexp, make sure prefix has '.'

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'twisted/plugins/distinct_plugin.py'
2--- twisted/plugins/distinct_plugin.py 2011-11-18 19:42:56 +0000
3+++ twisted/plugins/distinct_plugin.py 2012-06-05 16:24:19 +0000
4@@ -4,6 +4,7 @@
5 from txstatsd.itxstatsd import IMetricFactory
6 from txstatsd.metrics.distinctmetric import DistinctMetricReporter
7
8+
9 class DistinctMetricFactory(object):
10 implements(IMetricFactory, IPlugin)
11
12
13=== added file 'twisted/plugins/sli_plugin.py'
14--- twisted/plugins/sli_plugin.py 1970-01-01 00:00:00 +0000
15+++ twisted/plugins/sli_plugin.py 2012-06-05 16:24:19 +0000
16@@ -0,0 +1,75 @@
17+# -*- coding: utf-8 *-*
18+import fnmatch
19+import re
20+
21+from zope.interface import implements
22+
23+from twisted.plugin import IPlugin
24+from txstatsd.itxstatsd import IMetricFactory
25+from txstatsd.metrics.slimetric import (
26+ SLIMetricReporter, BetweenCondition, AboveCondition, BelowCondition)
27+
28+
29+class SLIMetricFactory(object):
30+ implements(IMetricFactory, IPlugin)
31+
32+ name = "SLI"
33+ metric_type = "sli"
34+
35+ def __init__(self):
36+ self.config = {}
37+
38+ def build_metric(self, prefix, name, wall_time_func=None):
39+ if prefix:
40+ if not prefix[-1] == ".":
41+ prefix = prefix + "."
42+ path = prefix + name
43+ else:
44+ path = name
45+ result = {}
46+ for pattern, conditions in self.config.items():
47+ if fnmatch.fnmatch(path, pattern):
48+ result.update(conditions)
49+ return SLIMetricReporter(path, result)
50+
51+ def configure(self, options):
52+ self.section = dict(options.get("plugin_sli", {}))
53+
54+ rules = self.section.get("rules", None)
55+
56+ if rules is None:
57+ return
58+
59+ rules = rules.strip()
60+ regexp = "([\w\.\*\?\_\-]+) => (\w+) IF (\w+)(.*)"
61+ mo = re.compile(regexp)
62+ for line_no, rule in enumerate(rules.split("\n")):
63+
64+ result = mo.match(rule)
65+ if result is None:
66+ raise TypeError("Did not match rule spec: %s (rule %d: %s)"
67+ % (regexp, line_no, rule))
68+
69+ head, label, cname, cparams = result.groups()
70+ cparams = cparams[1:]
71+
72+ self.config.setdefault(head, {})
73+
74+ method = getattr(self, "build_" + cname, None)
75+ if method is None:
76+ raise TypeError("cannot build condition: %s %s" % (
77+ cname, cparams))
78+
79+ cobj = method(*cparams.split(" "))
80+ self.config[head][label] = cobj
81+
82+ def build_above(self, value):
83+ return AboveCondition(float(value))
84+
85+ def build_below(self, value):
86+ return BelowCondition(float(value))
87+
88+ def build_between(self, low, hi):
89+ return BetweenCondition(float(low), float(hi))
90+
91+sli_metric_factory = SLIMetricFactory()
92
93=== added file 'txstatsd/metrics/slimetric.py'
94--- txstatsd/metrics/slimetric.py 1970-01-01 00:00:00 +0000
95+++ txstatsd/metrics/slimetric.py 2012-06-05 16:24:19 +0000
96@@ -0,0 +1,61 @@
97+# -*- coding: utf-8 *-*
98+
99+
100+class BelowCondition(object):
101+
102+ def __init__(self, value):
103+ self.value = value
104+
105+ def __call__(self, value):
106+ return value < self.value
107+
108+
109+class AboveCondition(object):
110+
111+ def __init__(self, value):
112+ self.value = value
113+
114+ def __call__(self, value):
115+ return value > self.value
116+
117+class BetweenCondition(object):
118+
119+ def __init__(self, low, hi):
120+ self.low = low
121+ self.hi = hi
122+
123+ def __call__(self, value):
124+ return self.low < value < self.hi
125+
126+
127+class SLIMetricReporter(object):
128+ def __init__(self, name, conditions):
129+ self.name = name
130+ self.conditions = conditions
131+ self.conditions = conditions
132+ self.clear()
133+
134+ def clear(self):
135+ self.counts = dict((k, 0) for k in self.conditions)
136+ self.count = 0
137+
138+ def process(self, fields):
139+ self.update(float(fields[0]))
140+
141+ def update(self, value):
142+ self.count += 1
143+ for k, condition in self.conditions.items():
144+
145+ if condition(value):
146+ self.counts[k] += 1
147+
148+ def flush(self, interval, timestamp):
149+ metrics = []
150+ for item, value in self.counts.items():
151+ metrics.append((self.name + ".count_" + item,
152+ value, timestamp))
153+ metrics.append((self.name + ".count",
154+ self.count, timestamp))
155+
156+ self.clear()
157+ return metrics
158
159=== modified file 'txstatsd/server/router.py'
160--- txstatsd/server/router.py 2012-05-09 08:04:07 +0000
161+++ txstatsd/server/router.py 2012-06-05 16:24:19 +0000
162@@ -42,6 +42,7 @@
163 from txstatsd.server.processor import BaseMessageProcessor
164 from txstatsd.client import StatsDClientProtocol, TwistedStatsDClient
165
166+
167 class StopProcessingException(Exception):
168
169 pass
170
171=== added file 'txstatsd/tests/metrics/test_sli.py'
172--- txstatsd/tests/metrics/test_sli.py 1970-01-01 00:00:00 +0000
173+++ txstatsd/tests/metrics/test_sli.py 2012-06-05 16:24:19 +0000
174@@ -0,0 +1,97 @@
175+# -*- coding: utf-8 *-*
176+import ConfigParser
177+from cStringIO import StringIO
178+from twisted.trial.unittest import TestCase
179+
180+from twisted.plugins.sli_plugin import SLIMetricFactory
181+from txstatsd.metrics.slimetric import (
182+ SLIMetricReporter, BetweenCondition, AboveCondition, BelowCondition)
183+from txstatsd import service
184+
185+
186+class TestConditions(TestCase):
187+
188+ def test_below(self):
189+ c = BelowCondition(5)
190+ self.assertEquals(c(2), True)
191+ self.assertEquals(c(6), False)
192+
193+ def test_above(self):
194+ c = AboveCondition(5)
195+ self.assertEquals(c(2), False)
196+ self.assertEquals(c(6), True)
197+
198+ def test_between(self):
199+ c = BetweenCondition(2.5, 5)
200+ self.assertEquals(c(2), False)
201+ self.assertEquals(c(6), False)
202+ self.assertEquals(c(2.6), True)
203+
204+
205+class TestMetric(TestCase):
206+ def setUp(self):
207+ self.sli = SLIMetricReporter('test', {
208+ "red": BelowCondition(5),
209+ "yellow": BelowCondition(3)})
210+
211+ def test_count_all(self):
212+ self.sli.update(1)
213+ self.sli.update(1)
214+ self.assertEquals(self.sli.count, 2)
215+
216+ def test_count_threshold(self):
217+ self.assertEquals(self.sli.count, 0)
218+ self.assertEquals(self.sli.counts["red"], 0)
219+ self.assertEquals(self.sli.counts["yellow"], 0)
220+ for i in range(1, 7):
221+ self.sli.update(i)
222+ self.assertEquals(self.sli.count, 6)
223+ self.assertEquals(self.sli.counts["red"], 4)
224+ self.assertEquals(self.sli.counts["yellow"], 2)
225+
226+ def test_reports(self):
227+ self.test_count_threshold()
228+ rows = sorted(self.sli.flush(0, 0))
229+ self.assertEquals(
230+ [("test.count", 6, 0),
231+ ("test.count_red", 4, 0),
232+ ("test.count_yellow", 2, 0)],
233+ rows)
234+
235+ def test_clear(self):
236+ self.sli.update(1)
237+ self.sli.update(1)
238+ self.assertEquals(self.sli.count, 2)
239+ self.sli.flush(0, 0)
240+ self.sli.update(1)
241+ self.assertEquals(self.sli.count, 1)
242+
243+
244+class TestFactory(TestCase):
245+ def test_configure(self):
246+ class TestOptions(service.OptionsGlue):
247+ optParameters = [["test", "t", "default", "help"]]
248+ config_section = "statsd"
249+
250+ o = TestOptions()
251+ config_file = ConfigParser.RawConfigParser()
252+ config_file.readfp(StringIO("[statsd]\n\n[plugin_sli]\n"
253+ "rules = \n"
254+ " test_o-k => red IF below 5\n"
255+ " test_o-k => green IF between 0.1 3\n"
256+ " other* => red IF above 4\n"))
257+ o.configure(config_file)
258+ smf = SLIMetricFactory()
259+ smf.configure(o)
260+ smr = smf.build_metric("", "test_o-k")
261+ rc = smr.conditions["red"]
262+ self.assertTrue(isinstance(rc, BelowCondition))
263+ self.assertEquals(rc.value, 5)
264+ gc = smr.conditions["green"]
265+ self.assertTrue(isinstance(gc, BetweenCondition))
266+ self.assertEquals(gc.hi, 3)
267+ self.assertEquals(gc.low, 0.1)
268+ smr = smf.build_metric("", "otherXX")
269+ rc = smr.conditions["red"]
270+ self.assertTrue(isinstance(rc, AboveCondition))
271+ self.assertEquals(rc.value, 4)

Subscribers

People subscribed via source and target branches