diff --git a/.gitignore b/.gitignore index b3d698dd73e02f9b13fe28dc1f94a02d8b7218ec..9cbcb6fada295dbd9f98d5dd3053dbe7b118386d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ *.swo __pycache__ *.pyc - +/venv/ +/.mypy_cache +/.idea diff --git a/plugins/collectd.py b/plugins/collectd.py index 7a33ec33acd4bfdc69a6b01e57cdc5b0ef1f2bc7..18ae0d6ad6cf145be473a74135908fe55f218f09 100644 --- a/plugins/collectd.py +++ b/plugins/collectd.py @@ -1,13 +1,16 @@ import asyncio import time + def init(monitor): return PluginCollectd(monitor) + class PluginCollectd: """ Implements a super simple collectd interface for only sending temperature data """ + def __init__(self, monitor): self.loop = asyncio.get_event_loop() self.config = monitor.config @@ -48,7 +51,7 @@ class PluginCollectd: interval, timestamp, value) - #print("Sending data:", data.strip()) + # print("Sending data:", data.strip()) self._writer.write(data.encode('utf-8')) await self._writer.drain() @@ -63,7 +66,7 @@ class PluginCollectd: await self.reconnect() else: pass - #print("recv:", line) + # print("recv:", line) async def send_sensor_values(self, sensor): """ diff --git a/plugins/mail.py b/plugins/mail.py index 1416fef6551d55373659ed843b1fc9ac5cef5618..68bf81a36a8c0afc4358d8e60547c4e13014d733 100644 --- a/plugins/mail.py +++ b/plugins/mail.py @@ -71,6 +71,22 @@ I will try to fix this issue by reconnecting... Regards, Temperature """ +SENSOR_TEMPERATURE_WARNING_SUBJECT = "Temperaturwarnung Serverraum" +SENSOR_TEMPERATURE_WARNING_BODY = """Hi Guys, + +Die Temperaturen im Serverraum werden langsam Bedenklich: + +{temperatures} + +Auslöser: {reason} + +Aktuelle Temperaturen: +{alltemperatures} + +Bitte haltet die Temperaturen im Auge und fahrt eventuell heiß laufende Server herunter + +with love, +Temperator""" def init(monitor): @@ -79,10 +95,12 @@ def init(monitor): """ return PluginMail(monitor) + class PluginMail: """ Handle all the mail sending stuff """ + def __init__(self, monitor): self.monitor = monitor self.config = self.monitor.config @@ -115,7 +133,7 @@ class PluginMail: self._mail_rate_limit[subject] = time.time() smtp = smtplib.SMTP("mail.stusta.mhn.de") - #smtp.sendmail(msg['From'], recipients, msg.as_string()) + # smtp.sendmail(msg['From'], recipients, msg.as_string()) smtp.quit() async def err_nodata(self, **kwargs): @@ -143,26 +161,7 @@ class PluginMail: SENSOR_MEASUREMENT_MISSED_SUBJECT, SENSOR_MEASUREMENT_MISSED_BODY.format(**kwargs)) - async def temperature_warning(self, source, urgent=False, **kwargs): - subject = "Temperaturwarnung Serverraum" - - body = """Hi Guys, - -Die Temperaturen im Serverraum werden langsam Bedenklich: - -{temperatures} - -Auslöser: {reason} - -Aktuelle Temperaturen: -{alltemperatures} - -Bitte haltet die Temperaturen im Auge und fahrt eventuell heiß laufende Server herunter - -with love, -Temperator""" - if source == "tempdiff": temperatures = "{name1}:{temp1}\n{name2}:{temp2}".format(**kwargs) reason = "Differenztemperatur: {tempdiff}".format(**kwargs) @@ -173,8 +172,13 @@ Temperator""" alltemperatures = '\n'.join([ "{}: {}".format(sensor.name, sensor.temperature) if sensor.valid else "{}: INVALID".format(sensor.name) - for sensor in self.monitor.sensors.values() ]) + for sensor in self.monitor.sensors.values()]) - await self.send_mail(subject, body.format( - temperatures=temperatures, reason=reason, alltemperatures=alltemperatures), - urgent=urgent) + await self.send_mail( + SENSOR_TEMPERATURE_WARNING_SUBJECT, + SENSOR_TEMPERATURE_WARNING_BODY.format( + temperatures=temperatures, + reason=reason, + alltemperatures=alltemperatures), + urgent=urgent + ) diff --git a/plugins/prometheus.py b/plugins/prometheus.py new file mode 100644 index 0000000000000000000000000000000000000000..4812187440c1c6f3adba62c5de160dee433ca18a --- /dev/null +++ b/plugins/prometheus.py @@ -0,0 +1,54 @@ +import asyncio + +from prometheus_client import start_http_server, Gauge + + +def init(monitor): + return PluginPrometheus(monitor) + + +class PluginPrometheus: + def __init__(self, monitor): + self.loop = asyncio.get_event_loop() + self.config = monitor.config + self.last_store = 0 + self.monitor = monitor + + self.sensor_metrics = Gauge( + name=self.config["prometheus"]["sensor_metric_name"], + documentation="Container Temperature Measurements", + labelnames=["sensor"] + ) + + self.aggregated_metrics = Gauge( + name=self.config["prometheus"]["aggregated_metric_name"], + documentation="Container Temperature Aggregations", + labelnames=["group", "type"] + ) + + start_http_server( + addr=self.config["prometheus"].get('address', 'localhost'), + port=self.config["prometheus"]["port"] + ) + + async def update_sensor_values(self, sensor): + """ + update + """ + self.sensor_metrics.labels(sensor=sensor.name).set(sensor.temperature) + + async def send_stats_graph(self, graph, stattype, stattime, statval): + """ + to be called as a plugin callback to export aggregated measurements + """ + label_group = stattype.split("-")[1] + label_type = stattype.split("-")[2] + self.aggregated_metrics.labels(group=label_group, type=label_type).set(statval) + + async def sensor_update(self): + """ + Receive sensor data to store them regularely into collectd + """ + for sensor in self.monitor.sensors.values(): + if sensor.valid: + await self.update_sensor_values(sensor) diff --git a/plugins/warnings.py b/plugins/warnings.py index 372a898a34fb4b23736eef071a10f0e39f070c72..591912bfaf96b70e9e5e34f6fdc7cbeb8dfc9be6 100644 --- a/plugins/warnings.py +++ b/plugins/warnings.py @@ -1,19 +1,22 @@ import time + def init(monitor): """ Plugin interface method """ return PluginWarning(monitor) + class PluginWarning: """ Generate all kind of warnings whenever needed and observe the sensor if they see a problematic situation in the container """ + def __init__(self, monitor): self.monitor = monitor self.revmapping = { - sensor.name : sensor + sensor.name: sensor for sensor in self.monitor.sensors.values() } @@ -44,7 +47,7 @@ class PluginWarning: return [], 0, 0, 0, 0 avg = sum(sensor.temperature for sensor in sensors) / len(sensors) - var = sum((sensor.temperature - avg)**2 for sensor in sensors) / len(sensors) + var = sum((sensor.temperature - avg) ** 2 for sensor in sensors) / len(sensors) sensormin = +9999 sensormax = -9999 @@ -56,7 +59,6 @@ class PluginWarning: return sensors, sensormin, sensormax, avg, var - async def sensor_update(self): """ First generate stats and relay them to the collectd module, then use these stats @@ -74,30 +76,30 @@ class PluginWarning: if floor_sensors: await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-floormin", stattime=now, statval=floor_min) + stattype="temperature-floor-min", stattime=now, statval=floor_min) await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-floormax", stattime=now, statval=floor_max) + stattype="temperature-floor-max", stattime=now, statval=floor_max) await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-flooravg", stattime=now, statval=floor_avg) + stattype="temperature-floor-avg", stattime=now, statval=floor_avg) await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-floorvar", stattime=now, statval=floor_var) + stattype="temperature-floor-var", stattime=now, statval=floor_var) if ceil_sensors: await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-ceilmin", stattime=now, statval=ceil_min) + stattype="temperature-ceil-min", stattime=now, statval=ceil_min) await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-ceilmax", stattime=now, statval=ceil_max) + stattype="temperature-ceil-max", stattime=now, statval=ceil_max) await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-ceilavg", stattime=now, statval=ceil_avg) + stattype="temperature-ceil-avg", stattime=now, statval=ceil_avg) await self.monitor.call_plugin( "send_stats_graph", graph="stats", - stattype="temperature-ceilvar", stattime=now, statval=ceil_var) + stattype="temperature-ceil-var", stattime=now, statval=ceil_var) if floor_sensors and ceil_sensors: # Else we already have sent warning messages for broken sensors @@ -112,7 +114,6 @@ class PluginWarning: print("ceil: min {:05.2f} max {:05.2f} avg {:05.2f} var {:05.2f}".format( ceil_min, ceil_max, ceil_avg, ceil_var)) - # Here comes the warning magic # Critical: ceiling temperature > threshold (sane default: 45) @@ -132,7 +133,7 @@ class PluginWarning: # Warning: temperature difference > threshold (sane default: 17) if ceil_max > int(self.warning_conf['min_ceiling_warning']): - if tempdiff > int(self.warning_conf['floor_ceiling_diff']): + if tempdiff > int(self.warning_conf['floor_ceiling_diff']): await self.monitor.call_plugin("temperature_warning", source="tempdiff", name1="floor", @@ -140,4 +141,3 @@ class PluginWarning: temp1=floor_avg, temp2=ceil_avg, tempdiff=tempdiff) - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..0ece5484f3e0dbee5d7a71d403f1fbb806aa6a39 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +prometheus-client==0.7.1 +pyserial==3.4 +pyserial-asyncio==0.4 \ No newline at end of file diff --git a/tempermon.ini b/tempermon.ini index 6e98a68c14c949cc041419f2e394c7526fcc923b..a86cfdf573b5a456f07bc9f58187a542208dd585 100644 --- a/tempermon.ini +++ b/tempermon.ini @@ -1,3 +1,6 @@ +[general] +plugins = prometheus,mail,warnings + [serial] #port=/dev/ttyUSB0 port=/tmp/temperature_pts @@ -10,6 +13,11 @@ socketpath=/tmp/collectd_sock hostname=hugin interval=1 +[prometheus] +sensor_metric_name=ssn_container_temperature +aggregated_metric_name=ssn_container_temperature_agg +port=9199 + [mail] from=Temperman <root@temperator.stusta.de> to=jw@stusta.de,markus.hefele@stusta.de @@ -24,7 +32,6 @@ min_ceiling_warning=35 floor_ceiling_diff=15 ceiling_warning_level=40 - [testsensor] name=Test calibration=5 diff --git a/tempermonitor.py b/tempermonitor.py index 3c26e86dcb789770f33da4fd26b7b7283394307e..f4e51f7c58e45d6ebbf466d4183f0ddc73d68fc6 100755 --- a/tempermonitor.py +++ b/tempermonitor.py @@ -9,6 +9,7 @@ under the terms as stated in the LICENSE.md file. Changelog: 2018-08 jotweh: reimplemented using a micropython-esp32 +2020-04 milo: added prometheus plugin Open issues: @@ -26,25 +27,28 @@ from pathlib import Path import serial_asyncio import serial + class Sensor: """ - One instance as sensor posing as measurementproxy + One instance as sensor posing as measurement proxy """ + def __init__(self, config, owid): self.temperature = None self.last_update = 0 self.calibration = 0 self.valid = True - try: - if owid in config: - self.name = config[owid]['name'] - self.calibration = config[owid]['calibration'] - else: - print("Invalid Config: missing section {}".format(owid)) - except KeyError as exc: - print("Invalid Config: for {}: {}".format(owid, exc)) - raise + if owid not in config: + print(f"Invalid Config: missing section {owid}") + return + + if 'name' not in config[owid] and 'calibration' not in config[owid]: + print(f"Invalid Config for: {owid}") + raise RuntimeError(f"Invalid Config for: {owid}") + + self.name = config[owid]['name'] + self.calibration = config[owid]['calibration'] def update(self, temperature): """ @@ -53,6 +57,7 @@ class Sensor: self.temperature = float(temperature) self.last_update = time.time() + class TempMonitor: """ Interact with the esp-one-wire interface that sends: @@ -131,7 +136,7 @@ class TempMonitor: # Wait for the next line if time.time() - last_valid_data_received > 1800: - self.call_plugin("err_no_valid_data", last_line=line) + await self.call_plugin("err_no_valid_data", last_line=line) try: line = await asyncio.wait_for( @@ -149,8 +154,7 @@ class TempMonitor: line = line.decode('ascii').strip() except UnicodeError: continue - #print("recv:", line) - + # print("recv:", line) if line == '': # Block has ended @@ -237,6 +241,7 @@ def setup_plugin(filename, plugin): if not getattr(plugin, "name", None): plugin.name = filename + def main(): """ Start the tempmonitor @@ -252,8 +257,11 @@ def main(): plugin_path = Path(__file__).resolve().parent / "plugins" print("Loading plugins from {}".format(plugin_path)) + active_plugins = monitor.config["general"]["plugins"].split(",") + print(f"Active plugins: {active_plugins}") + for filename in plugin_path.glob("*.py"): - if (plugin_path / filename).exists(): + if (plugin_path / filename).exists() and filename.name in active_plugins: print("loading {}".format(filename.name)) modname = "plugins." + filename.name.split('.')[0] module = importlib.import_module(modname) @@ -269,5 +277,6 @@ def main(): finally: loop.run_until_complete(monitor.teardown()) + if __name__ == "__main__": main()