Metric collection

Exporting home sensor metrics to Prometheus

· Prometheus · Python

I wanted the sensor collector to remain boring. It already received readings from several ESP32 nodes and stored the latest value for each device. Adding Prometheus did not need a second database or a large library; it only needed one HTTP endpoint that renders the current state in the text exposition format.

The collector keeps an in-memory record with a sensor identifier, location, temperature, humidity, and timestamp. The endpoint exposes gauges plus a simple age metric. Age turned out to be more useful than a binary online flag because an alert can decide whether 90 seconds or 15 minutes is acceptable for a particular sensor.

from time import time
from flask import Response

def metric_line(name, labels, value):
    label_text = ",".join(
        f'{key}="{str(val).replace(chr(34), chr(39))}"'
        for key, val in labels.items()
    )
    return f"{name}{{{label_text}}} {value}\\n"

@app.get("/metrics")
def metrics():
    now = time()
    body = []

    for sensor in sensors.values():
        labels = {"sensor": sensor.id, "location": sensor.location}
        body.append(metric_line("metrion_temperature_celsius", labels, sensor.temp_c))
        body.append(metric_line("metrion_humidity_percent", labels, sensor.rh))
        body.append(metric_line("metrion_sample_age_seconds", labels, now - sensor.timestamp))

    return Response("".join(body), mimetype="text/plain")

I avoided putting firmware version, network address, or free-form descriptions into labels. Labels create time-series combinations, so they should be stable and limited. For this small setup, sensor and location are enough to distinguish devices without turning every metadata change into a new series.

Scrape settings

The collector runs on the same local machine as Prometheus. A 30-second scrape interval is faster than most sensor update periods, but that is intentional: Prometheus sees the current value repeatedly while metrion_sample_age_seconds continues to grow if the device stops reporting.

scrape_configs:
  - job_name: metrion_sensors
    scrape_interval: 30s
    static_configs:
      - targets: ["127.0.0.1:9108"]

An alert is then easy to read: metrion_sample_age_seconds > 300 means a sensor has not produced a fresh value for five minutes. I also keep a separate rule for room humidity above 70% for twenty minutes, because a brief spike after opening a window is not interesting.

The exporter currently exposes fewer than fifty series. That is small enough that clarity matters more than clever optimization. The main rule is that a metric name should include the unit, labels should stay stable, and stale data should be visible as age rather than silently disappearing.

Practical result: a tiny read-only metrics endpoint was enough. The useful addition was sample age, not extra device metadata.