meshtastic-metrics-exporter/exporter/processors.py

253 lines
10 KiB
Python

import base64
import json
import os
import redis
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from meshtastic.config_pb2 import Config
from meshtastic.mesh_pb2 import MeshPacket, HardwareModel, Data
from meshtastic.portnums_pb2 import PortNum
from prometheus_client import CollectorRegistry, Counter, Histogram, Gauge
from exporter.registry import ProcessorRegistry, ClientDetails
class MessageProcessor:
def __init__(self, registry: CollectorRegistry, redis_client: redis.Redis):
self.rx_rssi_gauge = None
self.channel_counter = None
self.packet_id_counter = None
self.hop_start_gauge = None
self.via_mqtt_counter = None
self.want_ack_counter = None
self.hop_limit_counter = None
self.rx_snr_gauge = None
self.rx_time_histogram = None
self.total_packets_counter = None
self.destination_message_type_counter = None
self.source_message_type_counter = None
self.registry = registry
self.redis_client = redis_client
self.init_metrics()
self.processor_registry = ProcessorRegistry()
def init_metrics(self):
# Source-related counters
self.source_message_type_counter = Counter(
'mesh_packet_source_types',
'Types of mesh packets processed by source',
['source_id', 'portnum'],
registry=self.registry
)
# Destination-related counters
self.destination_message_type_counter = Counter(
'mesh_packet_destination_types',
'Types of mesh packets processed by destination',
['destination_id', 'portnum'],
registry=self.registry
)
# Counters for the total number of packets
self.total_packets_counter = Counter(
'mesh_packet_total',
'Total number of mesh packets processed',
['source_id', 'destination_id'],
registry=self.registry
)
# Histogram for the rx_time (time in seconds)
self.rx_time_histogram = Histogram(
'mesh_packet_rx_time',
'Receive time of mesh packets (seconds since 1970)',
['source_id', 'destination_id'],
registry=self.registry
)
# Gauge for the rx_snr (signal-to-noise ratio)
self.rx_snr_gauge = Gauge(
'mesh_packet_rx_snr',
'Receive SNR of mesh packets',
['source_id', 'destination_id'],
registry=self.registry
)
# Counter for hop_limit
self.hop_limit_counter = Counter(
'mesh_packet_hop_limit',
'Hop limit of mesh packets',
['source_id', 'destination_id'],
registry=self.registry
)
# Counter for want_ack (occurrences of want_ack set to true)
self.want_ack_counter = Counter(
'mesh_packet_want_ack',
'Occurrences of want ACK for mesh packets',
['source_id', 'destination_id'],
registry=self.registry
)
# Counter for via_mqtt (occurrences of via_mqtt set to true)
self.via_mqtt_counter = Counter(
'mesh_packet_via_mqtt',
'Occurrences of mesh packets sent via MQTT',
['source_id', 'destination_id'],
registry=self.registry
)
# Gauge for hop_start
self.hop_start_gauge = Gauge(
'mesh_packet_hop_start',
'Hop start of mesh packets',
['source_id', 'destination_id'],
registry=self.registry
)
# Counter for unique packet IDs
self.packet_id_counter = Counter(
'mesh_packet_ids',
'Unique IDs for mesh packets',
['source_id', 'destination_id', 'packet_id'],
registry=self.registry
)
# Counter for the channel used
self.channel_counter = Counter(
'mesh_packet_channel',
'Channel used for mesh packets',
['source_id', 'destination_id', 'channel'],
registry=self.registry
)
# Gauge for the rx_rssi (received signal strength indicator)
self.rx_rssi_gauge = Gauge(
'mesh_packet_rx_rssi',
'Receive RSSI of mesh packets',
['source_id', 'destination_id'],
registry=self.registry
)
def process(self, mesh_packet: MeshPacket):
if getattr(mesh_packet, 'encrypted'):
key_bytes = base64.b64decode(os.getenv('MQTT_SERVER_KEY', '1PG7OiApB1nwvP+rz05pAQ==').encode('ascii'))
nonce_packet_id = getattr(mesh_packet, "id").to_bytes(8, "little")
nonce_from_node = getattr(mesh_packet, "from").to_bytes(8, "little")
# Put both parts into a single byte array.
nonce = nonce_packet_id + nonce_from_node
cipher = Cipher(algorithms.AES(key_bytes), modes.CTR(nonce), backend=default_backend())
decryptor = cipher.decryptor()
decrypted_bytes = decryptor.update(getattr(mesh_packet, "encrypted")) + decryptor.finalize()
data = Data()
data.ParseFromString(decrypted_bytes)
mesh_packet.decoded.CopyFrom(data)
port_num = int(mesh_packet.decoded.portnum)
payload = mesh_packet.decoded.payload
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.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.node_id, short_name='Hidden',
long_name='Hidden')
if port_num in map(int, os.getenv('FILTERED_PORTS', '1').split(',')): # Filter out ports
return None # Ignore this packet
self.process_simple_packet_details(destination_client_details, mesh_packet, port_num, source_client_details)
processor = ProcessorRegistry.get_processor(port_num)(self.registry, self.redis_client)
processor.process(payload, client_details=source_client_details)
def get_port_name_from_portnum(self, port_num):
descriptor = PortNum.DESCRIPTOR
for enum_value in descriptor.values:
if enum_value.number == port_num:
return enum_value.name
return 'UNKNOWN_PORT'
def process_simple_packet_details(self, destination_client_details, mesh_packet, port_num, source_client_details):
self.source_message_type_counter.labels(
source_id=source_client_details.node_id,
portnum=self.get_port_name_from_portnum(port_num)
).inc()
self.destination_message_type_counter.labels(
destination_id=destination_client_details.node_id,
portnum=self.get_port_name_from_portnum(port_num)
).inc()
# Increment the total packets counter
self.total_packets_counter.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).inc()
# Observe the rx_time in the histogram
self.rx_time_histogram.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).observe(mesh_packet.rx_time)
# Set the rx_snr in the gauge
self.rx_snr_gauge.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).set(mesh_packet.rx_snr)
# Increment the hop_limit counter
self.hop_limit_counter.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).inc(mesh_packet.hop_limit)
# Increment the want_ack counter if want_ack is true
if mesh_packet.want_ack:
self.want_ack_counter.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).inc()
# Increment the via_mqtt counter if via_mqtt is true
if mesh_packet.via_mqtt:
self.via_mqtt_counter.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).inc()
# Set the hop_start in the gauge
self.hop_start_gauge.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).set(mesh_packet.hop_start)
# Increment the unique packet ID counter
self.packet_id_counter.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id,
packet_id=mesh_packet.id
).inc()
# Increment the channel counter
self.channel_counter.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id,
channel=mesh_packet.channel
).inc()
# Set the rx_rssi in the gauge
self.rx_rssi_gauge.labels(
source_id=source_client_details.node_id,
destination_id=destination_client_details.node_id
).set(mesh_packet.rx_rssi)
def _get_client_details(self, node_id: str) -> ClientDetails:
user_details_json = self.redis_client.get(f"node:{node_id}")
if user_details_json is not None:
# Decode the JSON string to a Python dictionary
user_details = json.loads(user_details_json)
return ClientDetails(node_id=node_id,
short_name=user_details.get('short_name', 'Unknown'),
long_name=user_details.get('long_name', 'Unknown'),
hardware_model=user_details.get('hardware_model', HardwareModel.UNSET),
role=user_details.get('role', Config.DeviceConfig.Role.ValueType),
)
return ClientDetails(node_id=node_id)