Encode prometheus metric names per the prom spec (#26639)

Referencing issue #26418.

Prometheus metric names can only contain chars a-zA-Z0-9, : and _
(https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).

HA currently generates invalid prometheus names, e.g. if the unit for a
sensor is a non-ASCII character containing  ° or μ. To resolve, we need
to sanitize the name before creating, replacing non-valid characters
with a valid representation. In this case, I've used
"u{unicode-hex-code}".

Also updated the test case to make sure that the ° case is handled.
This commit is contained in:
Andrew Rowson 2019-09-19 11:51:49 +01:00 committed by Martin Hjelmare
parent 5e15675593
commit 770eeaf82f
2 changed files with 26 additions and 1 deletions

View File

@ -1,5 +1,6 @@
"""Support for Prometheus metrics export.""" """Support for Prometheus metrics export."""
import logging import logging
import string
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
@ -159,10 +160,23 @@ class PrometheusMetrics:
try: try:
return self._metrics[metric] return self._metrics[metric]
except KeyError: except KeyError:
full_metric_name = f"{self.metrics_prefix}{metric}" full_metric_name = self._sanitize_metric_name(
f"{self.metrics_prefix}{metric}"
)
self._metrics[metric] = factory(full_metric_name, documentation, labels) self._metrics[metric] = factory(full_metric_name, documentation, labels)
return self._metrics[metric] return self._metrics[metric]
@staticmethod
def _sanitize_metric_name(metric: str) -> str:
return "".join(
[
c
if c in string.ascii_letters or c.isdigit() or c == "_" or c == ":"
else f"u{hex(ord(c))}"
for c in metric
]
)
@staticmethod @staticmethod
def state_as_number(state): def state_as_number(state):
"""Return a state casted to a float.""" """Return a state casted to a float."""

View File

@ -41,6 +41,11 @@ async def prometheus_client(loop, hass, hass_client):
sensor3.entity_id = "sensor.electricity_price" sensor3.entity_id = "sensor.electricity_price"
await sensor3.async_update_ha_state() await sensor3.async_update_ha_state()
sensor4 = DemoSensor("Wind Direction", 25, None, "°", None)
sensor4.hass = hass
sensor4.entity_id = "sensor.wind_direction"
await sensor4.async_update_ha_state()
return await hass_client() return await hass_client()
@ -103,3 +108,9 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name
'entity="sensor.electricity_price",' 'entity="sensor.electricity_price",'
'friendly_name="Electricity price"} 0.123' in body 'friendly_name="Electricity price"} 0.123' in body
) )
assert (
'sensor_unit_u0xb0{domain="sensor",'
'entity="sensor.wind_direction",'
'friendly_name="Wind Direction"} 25.0' in body
)