Add generic unavailable and last_updated metrics for prometheus (#37456)

* Add generic unavailable and last_updated metrics for prometheus

* Updated with feedback from the code review
This commit is contained in:
Eric Severance 2020-07-12 12:27:33 -07:00 committed by GitHub
parent c6ab2c5d0a
commit 16a947aa5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 45 additions and 14 deletions

View File

@ -25,6 +25,7 @@ from homeassistant.const import (
CONTENT_TYPE_TEXT_PLAIN, CONTENT_TYPE_TEXT_PLAIN,
EVENT_STATE_CHANGED, EVENT_STATE_CHANGED,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE,
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
@ -153,13 +154,28 @@ class PrometheusMetrics:
handler = f"_handle_{domain}" handler = f"_handle_{domain}"
if hasattr(self, handler): if hasattr(self, handler) and state.state != STATE_UNAVAILABLE:
getattr(self, handler)(state) getattr(self, handler)(state)
metric = self._metric( labels = self._labels(state)
state_change = self._metric(
"state_change", self.prometheus_cli.Counter, "The number of state changes" "state_change", self.prometheus_cli.Counter, "The number of state changes"
) )
metric.labels(**self._labels(state)).inc() state_change.labels(**labels).inc()
entity_available = self._metric(
"entity_available",
self.prometheus_cli.Gauge,
"Entity is available (not in the unavailable state)",
)
entity_available.labels(**labels).set(float(state.state != STATE_UNAVAILABLE))
last_updated_time_seconds = self._metric(
"last_updated_time_seconds",
self.prometheus_cli.Gauge,
"The last_updated timestamp",
)
last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp())
def _handle_attributes(self, state): def _handle_attributes(self, state):
for key, value in state.attributes.items(): for key, value in state.attributes.items():

View File

@ -1,9 +1,9 @@
"""The tests for the Prometheus exporter.""" """The tests for the Prometheus exporter."""
from dataclasses import dataclass from dataclasses import dataclass
import datetime
import pytest import pytest
from homeassistant import setup
from homeassistant.components import climate, humidifier, sensor from homeassistant.components import climate, humidifier, sensor
from homeassistant.components.demo.sensor import DemoSensor from homeassistant.components.demo.sensor import DemoSensor
import homeassistant.components.prometheus as prometheus import homeassistant.components.prometheus as prometheus
@ -16,6 +16,7 @@ from homeassistant.const import (
) )
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
import tests.async_mock as mock import tests.async_mock as mock
@ -30,21 +31,18 @@ class FilterTest:
should_pass: bool should_pass: bool
@pytest.fixture async def prometheus_client(hass, hass_client):
async def prometheus_client(loop, hass, hass_client):
"""Initialize an hass_client with Prometheus component.""" """Initialize an hass_client with Prometheus component."""
await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}})
await setup.async_setup_component( await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]})
hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}
)
await setup.async_setup_component( await async_setup_component(
hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
await setup.async_setup_component( await async_setup_component(
hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]}
) )
@ -60,7 +58,11 @@ async def prometheus_client(loop, hass, hass_client):
) )
sensor2.hass = hass sensor2.hass = hass
sensor2.entity_id = "sensor.radio_energy" sensor2.entity_id = "sensor.radio_energy"
await sensor2.async_update_ha_state() with mock.patch(
"homeassistant.util.dt.utcnow",
return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC),
):
await sensor2.async_update_ha_state()
sensor3 = DemoSensor( sensor3 = DemoSensor(
None, "Electricity price", 0.123, None, f"SEK/{ENERGY_KILO_WATT_HOUR}", None None, "Electricity price", 0.123, None, f"SEK/{ENERGY_KILO_WATT_HOUR}", None
@ -89,9 +91,10 @@ async def prometheus_client(loop, hass, hass_client):
return await hass_client() return await hass_client()
async def test_view(prometheus_client): # pylint: disable=redefined-outer-name async def test_view(hass, hass_client):
"""Test prometheus metrics view.""" """Test prometheus metrics view."""
resp = await prometheus_client.get(prometheus.API_ENDPOINT) client = await prometheus_client(hass, hass_client)
resp = await client.get(prometheus.API_ENDPOINT)
assert resp.status == 200 assert resp.status == 200
assert resp.headers["content-type"] == "text/plain" assert resp.headers["content-type"] == "text/plain"
@ -167,6 +170,18 @@ async def test_view(prometheus_client): # pylint: disable=redefined-outer-name
'friendly_name="Radio Energy"} 14.0' in body 'friendly_name="Radio Energy"} 14.0' in body
) )
assert (
'entity_available{domain="sensor",'
'entity="sensor.radio_energy",'
'friendly_name="Radio Energy"} 1.0' in body
)
assert (
'last_updated_time_seconds{domain="sensor",'
'entity="sensor.radio_energy",'
'friendly_name="Radio Energy"} 86400.0' in body
)
assert ( assert (
'sensor_unit_sek_per_kwh{domain="sensor",' 'sensor_unit_sek_per_kwh{domain="sensor",'
'entity="sensor.electricity_price",' 'entity="sensor.electricity_price",'