Skip to content

Commit 0b95779

Browse files
committed
Merge branch 'feature/counter' into 'master'
Feature/counter See merge request grafolean/grafolean-collector-snmp!7
2 parents 061e0ab + b5410fc commit 0b95779

File tree

6 files changed

+161
-45
lines changed

6 files changed

+161
-45
lines changed

.gitlab-ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pytest:
1515
image: python:3.6-slim-stretch
1616
before_script:
1717
- apt-get update
18-
- apt-get install --no-install-recommends -q -y libsnmp-dev build-essential
18+
- apt-get install --no-install-recommends -q -y libsnmp-dev build-essential git
1919
- pip install --no-cache-dir pipenv
2020
- pipenv install --dev
2121
script:

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ LABEL org.label-schema.vendor="Grafolean" \
3131
COPY --from=python-requirements /requirements.txt /requirements.txt
3232
RUN \
3333
apt-get update && \
34-
apt-get install --no-install-recommends -q -y libsnmp-dev build-essential&& \
34+
apt-get install --no-install-recommends -q -y libsnmp-dev build-essential git && \
3535
pip install --no-cache-dir -r /requirements.txt && \
3636
apt-get purge -y build-essential && \
3737
apt-get clean autoclean && \

Pipfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ requests = "*"
1212
python-dotenv = "*"
1313
apscheduler = "*"
1414
ansicolors = "*"
15-
easysnmp = "*"
15+
easysnmp = {editable = true,git = "http://github.com/grafolean/easysnmp"}
1616
mathjspy = "*"
1717
numpy = "*"
1818

Pipfile.lock

+7-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

snmpcollector.py

+67-12
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import dotenv
33
import logging
44
import json
5+
import time
56
from pytz import utc
67
from colors import color
78
import requests
89

9-
from easysnmp import Session
10+
from easysnmp import Session, SNMPVariable
1011
from mathjspy import MathJS
1112

1213
from collector import Collector
@@ -25,6 +26,50 @@ class NoValueForOid(Exception):
2526
pass
2627

2728

29+
previous_counter_values = {}
30+
31+
32+
def _get_previous_counter_value(counter_ident):
33+
prev_value = previous_counter_values.get(counter_ident)
34+
if prev_value is None:
35+
return None, None
36+
return prev_value
37+
38+
39+
def _save_current_counter_value(new_value, now, counter_ident):
40+
previous_counter_values[counter_ident] = (new_value, now)
41+
42+
43+
def _convert_counters_to_values(results, now, counter_ident_prefix):
44+
new_results = []
45+
for i, v in enumerate(results):
46+
if isinstance(v, list):
47+
new_results.append(_convert_counters_to_values(v, now, counter_ident_prefix + f'/{i}'))
48+
continue
49+
if v.snmp_type not in ['COUNTER', 'COUNTER64']:
50+
new_results.append(v)
51+
continue
52+
# counter - deal with it:
53+
counter_ident = counter_ident_prefix + f'/{i}/{v.oid}/{v.oid_index}'
54+
old_value, t = _get_previous_counter_value(counter_ident)
55+
new_value = float(v.value)
56+
_save_current_counter_value(new_value, now, counter_ident)
57+
if old_value is None:
58+
new_results.append(SNMPVariable(oid=v.oid, oid_index=v.oid_index, value=None, snmp_type='COUNTER_PER_S'))
59+
continue
60+
61+
# it seems like the counter overflow happened, discard result:
62+
if new_value < old_value:
63+
new_results.append(SNMPVariable(oid=v.oid, oid_index=v.oid_index, value=None, snmp_type='COUNTER_PER_S'))
64+
log.warning(f"Counter overflow detected for oid {v.oid}, oid index {v.oid_index}, discarding value - if this happens often, consider using OIDS with 64bit counters (if available) or decreasing polling interval.")
65+
continue
66+
67+
dt = now - t
68+
dv = (new_value - old_value) / dt
69+
new_results.append(SNMPVariable(oid=v.oid, oid_index=v.oid_index, value=dv, snmp_type='COUNTER_PER_S'))
70+
return new_results
71+
72+
2873
def _apply_expression_to_results(snmp_results, methods, expression, output_path):
2974
if 'walk' in methods:
3075
"""
@@ -48,7 +93,9 @@ def _apply_expression_to_results(snmp_results, methods, expression, output_path)
4893
mjs = MathJS()
4994
for i, r in enumerate(addressable_results):
5095
v = r.get(oid_index)
51-
if v is None:
96+
if v is None: # oid index wasn't present
97+
raise NoValueForOid()
98+
if v.value is None: # no value (probably the first time we're asking for a counter)
5299
raise NoValueForOid()
53100
mjs.set('${}'.format(i + 1), float(v.value))
54101
value = mjs.eval(expression)
@@ -61,13 +108,19 @@ def _apply_expression_to_results(snmp_results, methods, expression, output_path)
61108
return result
62109

63110
else:
64-
mjs = MathJS()
65-
for i, r in enumerate(snmp_results):
66-
mjs.set('${}'.format(i + 1), float(r.value))
67-
value = mjs.eval(expression)
68-
return [
69-
{'p': output_path, 'v': value},
70-
]
111+
try:
112+
mjs = MathJS()
113+
for i, v in enumerate(snmp_results):
114+
if v.value is None: # no value (probably the first time we're asking for a counter)
115+
raise NoValueForOid()
116+
mjs.set('${}'.format(i + 1), float(v.value))
117+
value = mjs.eval(expression)
118+
return [
119+
{'p': output_path, 'v': value},
120+
]
121+
except NoValueForOid:
122+
log.warning(f'Missing OID value (counter?)')
123+
return []
71124

72125

73126
def send_results_to_grafolean(backend_url, bot_token, account_id, values):
@@ -178,15 +231,17 @@ def do_snmp(*args, **job_info):
178231
# while we are at it, save the indexes of the results:
179232
if not walk_indexes:
180233
walk_indexes = [r.oid_index for r in result]
181-
oids_results = list(zip(oids, methods, results))
182-
log.info("Results: {}".format(oids_results))
234+
log.info("Results: {}".format(list(zip(oids, methods, results))))
235+
236+
counter_ident_prefix = f'{job_info["entity_id"]}/{sensor["sensor_details"]["id"]}'
237+
results_no_counters = _convert_counters_to_values(results, time.time(), counter_ident_prefix)
183238

184239
# We have SNMP results and expression - let's calculate value(s). The trick here is that
185240
# if some of the data is fetched via SNMP WALK, we will have many results; if only SNMP
186241
# GET was used, we get one.
187242
expression = sensor["sensor_details"]["expression"]
188243
output_path = f'entity.{job_info["entity_id"]}.snmp.{sensor["sensor_details"]["output_path"]}'
189-
values = _apply_expression_to_results(results, methods, expression, output_path)
244+
values = _apply_expression_to_results(results_no_counters, methods, expression, output_path)
190245
send_results_to_grafolean(job_info['backend_url'], job_info['bot_token'], job_info['account_id'], values)
191246

192247

test_snmpcollector.py

+84-21
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from easysnmp import SNMPVariable
22

3-
from snmpcollector import _apply_expression_to_results
3+
from snmpcollector import _apply_expression_to_results, _convert_counters_to_values
44

55

6-
def test_snmpget():
6+
def test_apply_expression_snmpget():
77
results = [
8-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='5', value=68000, snmp_type='GAUGE'),
8+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='5', value='68000', snmp_type='GAUGE'),
99
]
1010
methods = ['walk' if isinstance(x, list) else 'get' for x in results]
1111
expression = '$1'
@@ -15,10 +15,10 @@ def test_snmpget():
1515
]
1616
assert _apply_expression_to_results(results, methods, expression, output_path) == expected_result
1717

18-
def test_snmpget_add():
18+
def test_apply_expression_snmpget_add():
1919
results = [
20-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='5', value=68000, snmp_type='GAUGE'),
21-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value=200, snmp_type='GAUGE'),
20+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='5', value='68000', snmp_type='GAUGE'),
21+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value='200', snmp_type='GAUGE'),
2222
]
2323
methods = ['walk' if isinstance(x, list) else 'get' for x in results]
2424
expression = '$1 + $2'
@@ -28,12 +28,12 @@ def test_snmpget_add():
2828
]
2929
assert _apply_expression_to_results(results, methods, expression, output_path) == expected_result
3030

31-
def test_snmpwalk():
31+
def test_apply_expression_snmpwalk():
3232
results = [
3333
[
34-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value=60000, snmp_type='GAUGE'),
35-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='2', value=61000, snmp_type='GAUGE'),
36-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='3', value=62000, snmp_type='GAUGE'),
34+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value='60000', snmp_type='GAUGE'),
35+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='2', value='61000', snmp_type='GAUGE'),
36+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='3', value='62000', snmp_type='GAUGE'),
3737
],
3838
]
3939
methods = ['walk' if isinstance(x, list) else 'get' for x in results]
@@ -46,13 +46,13 @@ def test_snmpwalk():
4646
]
4747
assert _apply_expression_to_results(results, methods, expression, output_path) == expected_result
4848

49-
def test_expression_add():
49+
def test_apply_expression_expression_add():
5050
results = [
51-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.8', oid_index='23', value=500, snmp_type='GAUGE'),
51+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.8', oid_index='23', value='500', snmp_type='GAUGE'),
5252
[
53-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value=60000, snmp_type='GAUGE'),
54-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='2', value=61000, snmp_type='GAUGE'),
55-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='3', value=62000, snmp_type='GAUGE'),
53+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value='60000', snmp_type='GAUGE'),
54+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='2', value='61000', snmp_type='GAUGE'),
55+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='3', value='62000', snmp_type='GAUGE'),
5656
],
5757
]
5858
methods = ['walk' if isinstance(x, list) else 'get' for x in results]
@@ -65,16 +65,16 @@ def test_expression_add():
6565
]
6666
assert _apply_expression_to_results(results, methods, expression, output_path) == expected_result
6767

68-
def test_snmpwalk_missing_value():
68+
def test_apply_expression_snmpwalk_missing_value_walk():
6969
results = [
7070
[
71-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value=60000, snmp_type='GAUGE'),
72-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='2', value=61000, snmp_type='GAUGE'),
73-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='3', value=62000, snmp_type='GAUGE'),
71+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value='60000', snmp_type='GAUGE'),
72+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='2', value='61000', snmp_type='GAUGE'),
73+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='3', value='62000', snmp_type='GAUGE'),
7474
],
7575
[
76-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.2.2', oid_index='1', value=10, snmp_type='GAUGE'),
77-
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.2.2', oid_index='2', value=10, snmp_type='GAUGE'),
76+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.2.2', oid_index='1', value='10', snmp_type='GAUGE'),
77+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.2.2', oid_index='2', value='10', snmp_type='GAUGE'),
7878
],
7979
]
8080
methods = ['walk' if isinstance(x, list) else 'get' for x in results]
@@ -85,3 +85,66 @@ def test_snmpwalk_missing_value():
8585
{ 'p': 'snmp.test123.asdf.2', 'v': 6100.0 },
8686
]
8787
assert _apply_expression_to_results(results, methods, expression, output_path) == expected_result
88+
89+
def test_apply_expression_snmpwalk_missing_value_get():
90+
results = [
91+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value='60000', snmp_type='GAUGE'),
92+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.2.2', oid_index='1', value=None, snmp_type='GAUGE'),
93+
]
94+
methods = ['walk' if isinstance(x, list) else 'get' for x in results]
95+
expression = '$1 / $2'
96+
output_path = 'snmp.test123.asdf'
97+
expected_result = []
98+
assert _apply_expression_to_results(results, methods, expression, output_path) == expected_result
99+
100+
def test_convert_counters_no_counters_no_change():
101+
""" If there are no counters, nothing should change """
102+
now = 1234567890.123456
103+
results = [
104+
[
105+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='1', value='60000', snmp_type='GAUGE'),
106+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='2', value='61000', snmp_type='GAUGE'),
107+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.1.3', oid_index='3', value='62000', snmp_type='GAUGE'),
108+
],
109+
[
110+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.2.2', oid_index='1', value='10', snmp_type='GAUGE'),
111+
SNMPVariable(oid='1.3.6.1.4.1.2021.13.16.2.2.2', oid_index='2', value='10', snmp_type='GAUGE'),
112+
],
113+
]
114+
assert _convert_counters_to_values(results, now, "ASDF/1234") == results
115+
116+
def test_convert_counters_counter():
117+
""" First expression should be empty, next ones should work """
118+
now = 1234567890.123456
119+
120+
results_0 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.16', oid_index='1', value='1000', snmp_type='COUNTER')]
121+
expected_0 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.16', oid_index='1', value=None, snmp_type='COUNTER_PER_S')]
122+
assert _convert_counters_to_values(results_0, now, "ASDF/1234") == expected_0
123+
124+
results_1 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.16', oid_index='1', value='2000.0', snmp_type='COUNTER')]
125+
expected_1 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.16', oid_index='1', value='1000.0', snmp_type='COUNTER_PER_S')]
126+
assert _convert_counters_to_values(results_1, now + 1.0, "ASDF/1234") == expected_1
127+
128+
results_2 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.16', oid_index='1', value='2300.0', snmp_type='COUNTER')]
129+
expected_2 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.16', oid_index='1', value='100.0', snmp_type='COUNTER_PER_S')]
130+
assert _convert_counters_to_values(results_2, now + 1.0 + 3.0, "ASDF/1234") == expected_2
131+
132+
def test_convert_counters_overflow():
133+
""" First expression should be empty, next ones should work """
134+
now = 1234567890.123456
135+
136+
results_0 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value='123000.0', snmp_type='COUNTER')]
137+
expected_0 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value=None, snmp_type='COUNTER_PER_S')]
138+
assert _convert_counters_to_values(results_0, now, "ASDF/1234") == expected_0
139+
140+
results_1 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value='234000.0', snmp_type='COUNTER')]
141+
expected_1 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value='111000.0', snmp_type='COUNTER_PER_S')]
142+
assert _convert_counters_to_values(results_1, now + 1.0, "ASDF/1234") == expected_1
143+
144+
results_2 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value='1000.0', snmp_type='COUNTER')]
145+
expected_2 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value=None, snmp_type='COUNTER_PER_S')]
146+
assert _convert_counters_to_values(results_2, now + 1.0 + 3.0, "ASDF/1234") == expected_2
147+
148+
results_3 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value='2000.0', snmp_type='COUNTER')]
149+
expected_3 = [SNMPVariable(oid='.1.3.6.1.2.1.2.2.1.17', oid_index='1', value='500.0', snmp_type='COUNTER_PER_S')]
150+
assert _convert_counters_to_values(results_3, now + 1.0 + 3.0 + 2.0, "ASDF/1234") == expected_3

0 commit comments

Comments
 (0)