Skip to content

Commit 3d90499

Browse files
authored
Metrics Improvements (Part 1) (#72)
* Refactor the Metrics Classes * Update Metrics Documentation * Add basic metrics tests * Add InfluxDB metrics class
1 parent e6c772c commit 3d90499

File tree

6 files changed

+213
-46
lines changed

6 files changed

+213
-46
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44
This CHANGELOG follows the format listed [here](https://github.com/sensu-plugins/community/blob/master/HOW_WE_CHANGELOG.md)
55

66
# [Unreleased]
7+
### Added
8+
9+
- Add new class SensuPluginMetricsGeneric, this will be extended in future to act in a similar way to its Ruby counterpart. (@borourke)
10+
- Added a new class SensuPluginMetricInfluxdb, which outputs the results in influxdb line format. (@borourke)
11+
- Add basic tests for the Metrics classes. (@borourke)
12+
713
### Fixed
814

15+
- Refactor metrics classes, they should now function properly when passed an exception, empty status or a status message. (@borourke)
916
- Update tests so that they run with pytest > 4.0. (@borourke)
1017

1118
# [0.7.1]

README.md

+31-17
Original file line numberDiff line numberDiff line change
@@ -72,42 +72,56 @@ The check submits the check in json format. Arbitrary extra fields can be added
7272

7373
## Metrics
7474

75-
### JSON
75+
For a metric you can subclass one of the following;
7676

77-
from sensu_plugin import SensuPluginMetricJSON
77+
* SensuPluginMetricGraphite
78+
* SensuPluginMetricInfluxdb
79+
* SensuPluginMetricJSON
80+
* SensuPluginMetricStatsd
81+
82+
### Graphite
7883

79-
class FooBarBazMetricJSON(SensuPluginMetricJSON):
84+
from sensu_plugin import SensuPluginMetricGraphite
85+
86+
class MyGraphiteMetric (SensuPluginMetricGraphite):
8087
def run(self):
81-
self.ok({'foo': 1, 'bar': { 'baz': 'stuff' } })
88+
self.ok('sensu', 1)
8289

8390
if __name__ == "__main__":
84-
f = FooBarBazMetricJSON()
91+
metric = MyGraphiteMetric()
8592

86-
### Graphite
93+
### Influxdb
8794

88-
from sensu_plugin import SensuPluginMetricGraphite
95+
from sensu_plugin import SensuPluginMetricInfluxdb
96+
97+
class MyInfluxdbMetric (SensuPluginMetricInfluxdb):
98+
def run(self):
99+
self.ok('sensu', 'baz=42', 'env=prod,location=us-midwest')
100+
101+
if __name__ == "__main__":
102+
metric = MyInfluxdbMetric()
103+
104+
### JSON
105+
106+
from sensu_plugin import SensuPluginMetricJSON
89107

90-
class FooBarBazMetricGraphite(SensuPluginMetricGraphite):
108+
class MyJSONMetric(OLDSensuPluginMetricJSON):
91109
def run(self):
92-
self.output('a', 1)
93-
self.output('b', 2)
94-
self.ok()
110+
self.ok({'foo': 1, 'bar': 'anything'})
95111

96112
if __name__ == "__main__":
97-
f = FooBarBazMetricGraphite()
113+
metric = MyJSONMetric()
98114

99115
### StatsD
100116

101117
from sensu_plugin import SensuPluginMetricStatsd
102118

103-
class FooBarBazMetricStatsd(SensuPluginMetricStatsd):
119+
class MyStatsdMetric(SensuPluginMetricStatsd):
104120
def run(self):
105-
self.output('a', 1, 'ms')
106-
self.output('b.c.d', 15)
107-
self.ok()
121+
self.ok('sensu.baz', 42, 'g')
108122

109123
if __name__ == "__main__":
110-
f = FooBarBazMetricStatsd()
124+
metric = MyStatsdMetric()
111125

112126
## License
113127

pylint.rc

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ load-plugins=
4242
# W0221: arguments-differ
4343
# BOR-TODO: look into W0211 more
4444
# BOR-TODO: remove R0205 when we drop python 2.7 support
45-
disable=E1101,R0201,W0212,C0111,R0903,E0602,R0204,W0221,R0205,R0912,R0801
45+
disable=E1101,R0201,W0212,C0111,R0903,E0602,R0204,W0221,R0205,R0912,R0801,inconsistent-return-statements
4646

4747

4848

sensu_plugin/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""This module provides helpers for writing Sensu plugins"""
22
from sensu_plugin.plugin import SensuPlugin
33
from sensu_plugin.check import SensuPluginCheck
4-
from sensu_plugin.metric import SensuPluginMetricJSON
4+
from sensu_plugin.metric import SensuPluginMetricGeneric
55
from sensu_plugin.metric import SensuPluginMetricGraphite
6+
from sensu_plugin.metric import SensuPluginMetricInfluxdb
7+
from sensu_plugin.metric import SensuPluginMetricJSON
68
from sensu_plugin.metric import SensuPluginMetricStatsd
79
from sensu_plugin.handler import SensuHandler
810
import sensu_plugin.pushevent

sensu_plugin/metric.py

+67-27
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,80 @@
1313
from sensu_plugin.compat import compat_basestring
1414

1515

16-
class SensuPluginMetricJSON(SensuPlugin):
16+
class SensuPluginMetricGeneric(SensuPlugin):
17+
def sanitise_arguments(self, args):
18+
# check whether the arguments have been passed by a dynamic status code
19+
# or if the output method is being called directly
20+
# extract the required tuple if called using dynamic function
21+
if len(args) == 1 and isinstance(args[0], tuple):
22+
args = args[0]
23+
# check to see whether output is running after being called by an empty
24+
# dynamic function.
25+
if args[0] is None:
26+
pass
27+
# check to see whether output is running after being called by a
28+
# dynamic whilst containing a message.
29+
elif isinstance(args[0], Exception) or len(args) == 1:
30+
print(args[0])
31+
else:
32+
return args
33+
34+
35+
class SensuPluginMetricGraphite(SensuPluginMetricGeneric):
36+
def output(self, *args):
37+
# sanitise the arguments
38+
args = self.sanitise_arguments(args)
39+
if args:
40+
# convert the arguments to a list
41+
args = list(args)
42+
# add the timestamp if required
43+
if len(args) < 3:
44+
args.append(None)
45+
if args[2] is None:
46+
args[2] = (int(time.time()))
47+
# produce the output
48+
print(" ".join(str(s) for s in args[0:3]))
49+
50+
51+
class SensuPluginMetricInfluxdb(SensuPluginMetricGeneric):
52+
def output(self, *args):
53+
# sanitise the arguments
54+
args = self.sanitise_arguments(args)
55+
if args:
56+
# determine whether a single value has been passed
57+
# as fields and if so give it a name.
58+
fields = args[1]
59+
if fields.isdigit():
60+
fields = "value={}".format(args[1])
61+
# append tags on to the measurement name if they exist
62+
measurement = args[0]
63+
if len(args) > 2:
64+
measurement = "{},{}".format(args[0], args[2])
65+
# create a timestamp
66+
timestamp = int(time.time())
67+
# produce the output
68+
print("{} {} {}".format(measurement, fields, timestamp))
69+
70+
71+
class SensuPluginMetricJSON(SensuPluginMetricGeneric):
1772
def output(self, args):
1873
obj = args[0]
1974
if isinstance(obj, (Exception, compat_basestring)):
20-
print(obj)
75+
print(obj[0])
2176
elif isinstance(obj, (dict, list)):
2277
print(json.dumps(obj))
2378

2479

25-
class SensuPluginMetricGraphite(SensuPlugin):
26-
def output(self, *args):
27-
if args[0] is None:
28-
print()
29-
elif isinstance(args[0], Exception) or args[1] is None:
30-
print(args[0])
31-
else:
32-
l_args = list(args)
33-
if len(l_args) < 3:
34-
l_args.append(None)
35-
if l_args[2] is None:
36-
l_args[2] = int(time.time())
37-
print("\t".join(str(s) for s in l_args[0:3]))
38-
39-
40-
class SensuPluginMetricStatsd(SensuPlugin):
80+
class SensuPluginMetricStatsd(SensuPluginMetricGeneric):
4181
def output(self, *args):
42-
if args[0] is None:
43-
print()
44-
elif isinstance(args[0], Exception) or args[1] is None:
45-
print(args[0])
46-
else:
47-
l_args = list(args)
48-
if len(l_args) < 3 or l_args[2] is None:
82+
# sanitise the arguments
83+
args = self.sanitise_arguments(args)
84+
if args:
85+
# convert the arguments to a list
86+
args = list(args)
87+
if len(args) < 3 or args[2] is None:
4988
stype = 'kv'
5089
else:
51-
stype = l_args[2]
52-
print("|".join([":".join(str(s) for s in l_args[0:2]), stype]))
90+
stype = args[2]
91+
# produce the output
92+
print("|".join([":".join(str(s) for s in args[0:2]), stype]))

sensu_plugin/tests/test_metric.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import json
2+
3+
from sensu_plugin import SensuPluginMetricGraphite
4+
from sensu_plugin import SensuPluginMetricInfluxdb
5+
from sensu_plugin import SensuPluginMetricJSON
6+
from sensu_plugin import SensuPluginMetricStatsd
7+
8+
try:
9+
from unittest.mock import Mock, patch
10+
except ImportError:
11+
from mock import Mock, patch
12+
13+
try:
14+
from StringIO import StringIO
15+
except ImportError:
16+
from io import StringIO
17+
18+
19+
class TestSensuPluginMetricGraphite(object):
20+
def __init__(self):
21+
self.sensu_plugin_metric = None
22+
23+
def setup(self):
24+
'''
25+
Instantiate a fresh SensuPluginMetricGraphite before each test.
26+
'''
27+
self.sensu_plugin_metric = SensuPluginMetricGraphite(autorun=False)
28+
29+
@patch('sensu_plugin.plugin.sys.exit', Mock())
30+
@patch('sys.stdout', new_callable=StringIO)
31+
def test_output_ok(self, out):
32+
self.sensu_plugin_metric.ok('sensu', 1)
33+
output = out.getvalue().split()
34+
assert output[0] == 'sensu'
35+
assert output[1] == '1'
36+
37+
38+
class TestSensuPluginMetricInfluxdb(object):
39+
def __init__(self):
40+
self.sensu_plugin_metric = None
41+
42+
def setup(self):
43+
'''
44+
Instantiate a fresh SensuPluginMetricInfluxdb before each test.
45+
'''
46+
self.sensu_plugin_metric = SensuPluginMetricInfluxdb(autorun=False)
47+
48+
@patch('sensu_plugin.plugin.sys.exit', Mock())
49+
@patch('sys.stdout', new_callable=StringIO)
50+
def test_output_ok(self, out):
51+
self.sensu_plugin_metric.ok('sensu', 'baz=42',
52+
'env=prod,location=us-midwest')
53+
output = out.getvalue().split()
54+
assert output[0] == 'sensu,env=prod,location=us-midwest'
55+
assert output[1] == 'baz=42'
56+
57+
@patch('sensu_plugin.plugin.sys.exit', Mock())
58+
@patch('sys.stdout', new_callable=StringIO)
59+
def test_output_ok_no_key(self, out):
60+
self.sensu_plugin_metric.ok('sensu', '42',
61+
'env=prod,location=us-midwest')
62+
output = out.getvalue().split()
63+
assert output[0] == 'sensu,env=prod,location=us-midwest'
64+
assert output[1] == 'value=42'
65+
66+
67+
class TestSensuPluginMetricJSON(object):
68+
def __init__(self):
69+
self.sensu_plugin_metric = None
70+
71+
def setup(self):
72+
'''
73+
Instantiate a fresh SensuPluginMetricJSON before each test.
74+
'''
75+
self.sensu_plugin_metric = SensuPluginMetricJSON(autorun=False)
76+
77+
@patch('sensu_plugin.plugin.sys.exit', Mock())
78+
@patch('sys.stdout', new_callable=StringIO)
79+
def test_output_ok(self, out):
80+
self.sensu_plugin_metric.ok({'foo': 1, 'bar': 'anything'})
81+
assert json.loads(out.getvalue())
82+
83+
84+
class TestSensuPluginMetricStatsd(object):
85+
def __init__(self):
86+
self.sensu_plugin_metric = None
87+
88+
def setup(self):
89+
'''
90+
Instantiate a fresh SensuPluginMetricStatsd before each test.
91+
'''
92+
self.sensu_plugin_metric = SensuPluginMetricStatsd(autorun=False)
93+
94+
@patch('sensu_plugin.plugin.sys.exit', Mock())
95+
@patch('sys.stdout', new_callable=StringIO)
96+
def test_output_ok(self, out):
97+
self.sensu_plugin_metric.ok('sensu.baz', 42, 'g')
98+
assert out.getvalue() == "sensu.baz:42|g\n"
99+
100+
@patch('sensu_plugin.plugin.sys.exit', Mock())
101+
@patch('sys.stdout', new_callable=StringIO)
102+
def test_output_ok_two(self, out):
103+
self.sensu_plugin_metric.ok('sensu.baz', 42)
104+
assert out.getvalue() == "sensu.baz:42|kv\n"

0 commit comments

Comments
 (0)