From cfd5bfc098181f5d6419a36d1cfa33e41f7919b0 Mon Sep 17 00:00:00 2001 From: Gleb Tcivie Date: Mon, 24 Jun 2024 22:56:49 +0300 Subject: [PATCH] Added a bit more counters and metrics --- exporter/processors.py | 16 +++- exporter/registry.py | 206 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 204 insertions(+), 18 deletions(-) diff --git a/exporter/processors.py b/exporter/processors.py index f2d366e..679f5e5 100644 --- a/exporter/processors.py +++ b/exporter/processors.py @@ -1,7 +1,8 @@ import os import redis -from meshtastic.mesh_pb2 import MeshPacket +from meshtastic.config_pb2 import Config +from meshtastic.mesh_pb2 import MeshPacket, HardwareModel from prometheus_client import CollectorRegistry, Counter from exporter.registry import ProcessorRegistry, ClientDetails @@ -27,12 +28,12 @@ class MessageProcessor: source_client_details = self._get_client_details(getattr(mesh_packet, 'from')) if os.getenv('MESH_HIDE_SOURCE_DATA', 'false') == 'true': - source_client_details = ClientDetails(node_id=source_client_details['id'], short_name='Hidden', + source_client_details = ClientDetails(node_id=source_client_details.node_id, short_name='Hidden', long_name='Hidden') destination_client_details = self._get_client_details(getattr(mesh_packet, 'to')) if os.getenv('MESH_HIDE_DESTINATION_DATA', 'false') == 'true': - destination_client_details = ClientDetails(node_id=destination_client_details['id'], short_name='Hidden', + destination_client_details = ClientDetails(node_id=destination_client_details.node_id, short_name='Hidden', long_name='Hidden') if port_num in map(int, os.getenv('FILTERED_PORTS', '1').split(',')): # Filter out ports @@ -62,6 +63,11 @@ class MessageProcessor: def _get_client_details(self, node_id: str) -> ClientDetails: details = self.redis_client.hgetall(f"node:{node_id}") if details: - return ClientDetails(node_id=node_id, short_name=details['short_name'], long_name=details['long_name']) + return ClientDetails(node_id=node_id, + short_name=details.get('short_name', 'Unknown'), + long_name=details.get('long_name', 'Unknown'), + hardware_model=details.get('hardware_model', HardwareModel.UNSET), + role=details.get('role', Config.DeviceConfig.Role.ValueType.UNSET), + ) - return ClientDetails(node_id=node_id, short_name='Unknown', long_name='Unknown') + return ClientDetails(node_id=node_id) diff --git a/exporter/registry.py b/exporter/registry.py index dfdf874..63341ba 100644 --- a/exporter/registry.py +++ b/exporter/registry.py @@ -1,3 +1,4 @@ +import json import os from abc import ABC, abstractmethod from venv import logger @@ -5,27 +6,118 @@ from venv import logger import redis import unishox2 from meshtastic.admin_pb2 import AdminMessage -from meshtastic.mesh_pb2 import Position, User, Routing, Waypoint, RouteDiscovery, NeighborInfo +from meshtastic.config_pb2 import Config +from meshtastic.mesh_pb2 import Position, User, Routing, Waypoint, RouteDiscovery, NeighborInfo, HardwareModel from meshtastic.mqtt_pb2 import MapReport from meshtastic.paxcount_pb2 import Paxcount from meshtastic.portnums_pb2 import PortNum from meshtastic.remote_hardware_pb2 import HardwareMessage from meshtastic.storeforward_pb2 import StoreAndForward -from meshtastic.telemetry_pb2 import Telemetry -from prometheus_client import CollectorRegistry, Counter +from meshtastic.telemetry_pb2 import Telemetry, DeviceMetrics, EnvironmentMetrics, AirQualityMetrics, PowerMetrics +from prometheus_client import CollectorRegistry, Counter, Gauge + + +class _Metrics: + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(_Metrics, cls).__new__(cls) + return cls._instance + + def __init__(self, registry: CollectorRegistry): + if not hasattr(self, 'initialized'): # Ensuring __init__ runs only once + self._registry = registry + self._init_metrics() + self.initialized = True # Attribute to indicate initialization + + def _init_metrics(self): # TODO: Go over the metrics and rethink some of them to be more like the longtitute and + # latitude - The values should represent something and we shouldn't just label stuff. Also, the labels should + # be less used looked upon like keys for the data + self.message_counter = Counter( + 'text_message_app', + 'Text message app payload details', + ['client_id', 'short_name', 'long_name', 'message_content'], + registry=self._registry + ) + self.telemetry_counter_device_metrics = Counter( + 'telemetry_app_device_metrics', + 'Telemetry app payload details - Device metrics', + ['client_id', 'short_name', 'long_name' + 'battery_level', 'voltage', 'channel_utilization', 'air_until_tx', + 'uptime_seconds' + ], + registry=self._registry + ) + self.telemetry_counter_environment_metrics = Counter( + 'telemetry_app_environment_metrics', + 'Telemetry app payload details - Environment metrics', + ['client_id', 'short_name', 'long_name' + 'temperature', 'relative_humidity', 'barometric_pressure', 'gas_resistance', + 'voltage', 'current', 'iaq', 'distance', 'lux', 'white_lux', 'ir_lux', 'uv_lux', 'wind_direction', + 'wind_speed', 'weight' + ], + registry=self._registry + ) + self.telemetry_counter_air_quality_metrics = Counter( + 'telemetry_app_air_quality_metrics', + 'Telemetry app payload details - Air quality metrics', + ['client_id', 'short_name', 'long_name' + 'pm10_standard', 'pm25_standard', 'pm100_standard', 'pm10_environmental', + 'pm25_environmental', 'pm100_environmental', 'particles_03um', 'particles_05um', 'particles_10um', + 'particles_25um', 'particles_50um', 'particles_100um' + ], + registry=self._registry + ) + self.telemetry_counter_power_metrics = Counter( + 'telemetry_app_power_metrics', + 'Telemetry app payload details - Power metrics', + ['client_id', 'short_name', 'long_name' + 'ch1_voltage', 'ch1_current', 'ch2_voltage', 'ch2_current', 'ch3_voltage', + 'ch3_current' + ], + registry=self._registry + ) + self.device_latitude_gauge = Gauge( + 'device_latitude', + 'Device latitude', + ['client_id', 'short_name', 'long_name'], + registry=self._registry + ) + self.device_longitude_gauge = Gauge( + 'device_longitude', + 'Device longitude', + ['client_id', 'short_name', 'long_name'], + registry=self._registry + ) + self.device_altitude_gauge = Gauge( + 'device_altitude', + 'Device altitude', + ['client_id', 'short_name', 'long_name'], + registry=self._registry + ) + self.device_position_precision_gauge = Gauge( + 'device_position_precision', + 'Device position precision', + ['client_id', 'short_name', 'long_name'], + registry=self._registry + ) class ClientDetails: - def __init__(self, node_id, short_name, long_name): + def __init__(self, node_id, short_name='Unknown', long_name='Unknown', hardware_model=HardwareModel.UNSET, + role=None): self.node_id = node_id self.short_name = short_name self.long_name = long_name + self.hardware_model: HardwareModel = hardware_model + self.role: Config.DeviceConfig.Role = role class Processor(ABC): def __init__(self, registry: CollectorRegistry, redis_client: redis.Redis): - self.registry = registry self.redis_client = redis_client + self.metrics = _Metrics(registry) @abstractmethod def process(self, payload: bytes, client_details: ClientDetails): @@ -59,19 +151,13 @@ class UnknownAppProcessor(Processor): class TextMessageAppProcessor(Processor): def __init__(self, registry: CollectorRegistry, redis_client: redis.Redis): super().__init__(registry, redis_client) - self.message_counter = Counter( - 'text_message_app', - 'Text message app payload details', - ['client_id', 'short_name', 'long_name', 'message_content'], - registry=self.registry - ) def process(self, payload: bytes, client_details: ClientDetails): logger.debug("Received TEXT_MESSAGE_APP packet") message = payload.decode('utf-8') if os.getenv('HIDE_MESSAGE', 'true') == 'true': message = 'Hidden' - self.message_counter.labels( + self.metrics.message_counter.labels( client_id=client_details.node_id, short_name=client_details.short_name, long_name=client_details.long_name, @@ -94,6 +180,26 @@ class PositionAppProcessor(Processor): logger.debug("Received POSITION_APP packet") position = Position() position.ParseFromString(payload) + self.metrics.device_latitude_gauge.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name + ).set(position.latitude_i) + self.metrics.device_longitude_gauge.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name + ).set(position.longitude_i) + self.metrics.device_altitude_gauge.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name + ).set(position.altitude) + self.metrics.device_position_precision_gauge.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name + ).set(position.position_precision) pass @@ -103,6 +209,15 @@ class NodeInfoAppProcessor(Processor): logger.debug("Received NODEINFO_APP packet") user = User() user.ParseFromString(payload) + user_details = { + 'short_name': user.short_name, + 'long_name': user.long_name, + 'id': user.id, + 'hardware_model': user.hardware_model, + 'role': user.role, + } + user_details_json = json.dumps(user_details) + self.redis_client.set(f"node:{client_details.node_id}", user_details_json) pass @@ -207,7 +322,72 @@ class TelemetryAppProcessor(Processor): logger.debug("Received TELEMETRY_APP packet") telemetry = Telemetry() telemetry.ParseFromString(payload) - pass + if telemetry.HasField('device_metrics'): + device_metrics: DeviceMetrics = telemetry.device_metrics + self.metrics.telemetry_counter_device_metrics.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name, + battery_level=device_metrics.battery_level, + voltage=device_metrics.voltage, + channel_utilization=device_metrics.channel_utilization, + air_until_tx=device_metrics.air_until_tx, + uptime_seconds=device_metrics.uptime_seconds + ).inc() + if telemetry.HasField('environment_metrics'): + environment_metrics: EnvironmentMetrics = telemetry.environment_metrics + self.metrics.telemetry_counter_environment_metrics.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name, + temperature=environment_metrics.temperature, + relative_humidity=environment_metrics.relative_humidity, + barometric_pressure=environment_metrics.barometric_pressure, + gas_resistance=environment_metrics.gas_resistance, + voltage=environment_metrics.voltage, + current=environment_metrics.current, + iaq=environment_metrics.iaq, + distance=environment_metrics.distance, + lux=environment_metrics.lux, + white_lux=environment_metrics.white_lux, + ir_lux=environment_metrics.ir_lux, + uv_lux=environment_metrics.uv_lux, + wind_direction=environment_metrics.wind_direction, + wind_speed=environment_metrics.wind_speed, + weight=environment_metrics.weight + ).inc() + if telemetry.HasField('air_quality_metrics'): + air_quality_metrics: AirQualityMetrics = telemetry.air_quality_metrics + self.metrics.telemetry_counter_air_quality_metrics.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name, + pm10_standard=air_quality_metrics.pm10_standard, + pm25_standard=air_quality_metrics.pm25_standard, + pm100_standard=air_quality_metrics.pm100_standard, + pm10_environmental=air_quality_metrics.pm10_environmental, + pm25_environmental=air_quality_metrics.pm25_environmental, + pm100_environmental=air_quality_metrics.pm100_environmental, + particles_03um=air_quality_metrics.particles_03um, + particles_05um=air_quality_metrics.particles_05um, + particles_10um=air_quality_metrics.particles_10um, + particles_25um=air_quality_metrics.particles_25um, + particles_50um=air_quality_metrics.particles_50um, + particles_100um=air_quality_metrics.particles_100um + ).inc() + if telemetry.HasField('power_metrics'): + power_metrics: PowerMetrics = telemetry.power_metrics + self.metrics.telemetry_counter_power_metrics.labels( + client_id=client_details.node_id, + short_name=client_details.short_name, + long_name=client_details.long_name, + ch1_voltage=power_metrics.ch1_voltage, + ch1_current=power_metrics.ch1_current, + ch2_voltage=power_metrics.ch2_voltage, + ch2_current=power_metrics.ch2_current, + ch3_voltage=power_metrics.ch3_voltage, + ch3_current=power_metrics.ch3_current + ).inc() @ProcessorRegistry.register_processor(PortNum.ZPS_APP)