diff --git a/mqtt_exporter/main.py b/mqtt_exporter/main.py index 51c321f..324f8ec 100644 --- a/mqtt_exporter/main.py +++ b/mqtt_exporter/main.py @@ -9,7 +9,7 @@ import sys import paho.mqtt.client as mqtt -from prometheus_client import Counter, Gauge, start_http_server +from prometheus_client import Counter, Gauge, metrics, start_http_server from mqtt_exporter import settings @@ -61,6 +61,24 @@ def subscribe(client, _, __, result_code, *args): LOG.error("MQTT %s", mqtt.connack_string(result_code)) +def _normalize_prometheus_metric_name(prom_metric_name): + """Transform an invalid prometheus metric to a valid one. + + https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + """ + if metrics.METRIC_NAME_RE.match(prom_metric_name): + return prom_metric_name + + # clean invalid characted + prom_metric_name = re.sub(r"[^a-zA-Z0-9_:]", "", prom_metric_name) + + # ensure to start with valid character + if not re.match(r"^[a-zA-Z_:]", prom_metric_name): + prom_metric_name = ":" + prom_metric_name + + return prom_metric_name + + def _create_prometheus_metric(prom_metric_name): """Create Prometheus metric if does not exist.""" if not prom_metrics.get(prom_metric_name): @@ -68,6 +86,8 @@ def _create_prometheus_metric(prom_metric_name): if settings.MQTT_EXPOSE_CLIENT_ID: labels.append("client_id") + prom_metric_name = _normalize_prometheus_metric_name(prom_metric_name) + try: prom_metrics[prom_metric_name] = Gauge( prom_metric_name, "metric generated from MQTT message.", labels diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e0310a0 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/tests/unit/test_normalize_prometheus_name.py b/tests/unit/test_normalize_prometheus_name.py new file mode 100644 index 0000000..5c875ee --- /dev/null +++ b/tests/unit/test_normalize_prometheus_name.py @@ -0,0 +1,15 @@ +"""Tests of Prometheus normalization metrics.""" +from mqtt_exporter.main import _normalize_prometheus_metric_name + + +def test_normalize_prometheus_metric_name(): + """Test _normalize_prometheus_metric_name.""" + tests = { + "1234invalid": ":1234invalid", + "valid1234": "valid1234", + "_this_is_valid": "_this_is_valid", + "not_so_valid%_name": "not_so_valid_name", + } + + for candidate, wanted in tests.items(): + assert _normalize_prometheus_metric_name(candidate) == wanted