From 16a947aa5fc38ddd8760f72f7f4e02736f7430f7 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 12 Jul 2020 12:27:33 -0700 Subject: [PATCH] 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 --- .../components/prometheus/__init__.py | 22 +++++++++-- tests/components/prometheus/test_init.py | 37 +++++++++++++------ 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index cf689f408d9..654e6245a57 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -25,6 +25,7 @@ from homeassistant.const import ( CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, STATE_ON, + STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE, @@ -153,13 +154,28 @@ class PrometheusMetrics: handler = f"_handle_{domain}" - if hasattr(self, handler): + if hasattr(self, handler) and state.state != STATE_UNAVAILABLE: 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" ) - 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): for key, value in state.attributes.items(): diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 42cb286268d..e04cbf9e632 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,9 +1,9 @@ """The tests for the Prometheus exporter.""" from dataclasses import dataclass +import datetime import pytest -from homeassistant import setup from homeassistant.components import climate, humidifier, sensor from homeassistant.components.demo.sensor import DemoSensor import homeassistant.components.prometheus as prometheus @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util import tests.async_mock as mock @@ -30,21 +31,18 @@ class FilterTest: should_pass: bool -@pytest.fixture -async def prometheus_client(loop, hass, hass_client): +async def prometheus_client(hass, hass_client): """Initialize an hass_client with Prometheus component.""" await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) - await setup.async_setup_component( - hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} - ) + await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) - await setup.async_setup_component( + await async_setup_component( hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} ) await hass.async_block_till_done() - await setup.async_setup_component( + await async_setup_component( hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} ) @@ -60,7 +58,11 @@ async def prometheus_client(loop, hass, hass_client): ) sensor2.hass = hass 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( 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() -async def test_view(prometheus_client): # pylint: disable=redefined-outer-name +async def test_view(hass, hass_client): """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.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 ) + 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 ( 'sensor_unit_sek_per_kwh{domain="sensor",' 'entity="sensor.electricity_price",'