mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Unexport unavailable metrics in Prometheus (#125492)
This commit is contained in:
parent
45fb21e32d
commit
f12ba5f7a9
@ -76,6 +76,8 @@ from homeassistant.util.unit_conversion import TemperatureConverter
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
API_ENDPOINT = "/api/prometheus"
|
API_ENDPOINT = "/api/prometheus"
|
||||||
|
IGNORED_STATES = frozenset({STATE_UNAVAILABLE, STATE_UNKNOWN})
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = "prometheus"
|
DOMAIN = "prometheus"
|
||||||
CONF_FILTER = "filter"
|
CONF_FILTER = "filter"
|
||||||
@ -211,14 +213,6 @@ class PrometheusMetrics:
|
|||||||
"""Add/update a state in Prometheus."""
|
"""Add/update a state in Prometheus."""
|
||||||
entity_id = state.entity_id
|
entity_id = state.entity_id
|
||||||
_LOGGER.debug("Handling state update for %s", entity_id)
|
_LOGGER.debug("Handling state update for %s", entity_id)
|
||||||
domain, _ = hacore.split_entity_id(entity_id)
|
|
||||||
|
|
||||||
ignored_states = (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
|
||||||
|
|
||||||
handler = f"_handle_{domain}"
|
|
||||||
|
|
||||||
if hasattr(self, handler) and state.state not in ignored_states:
|
|
||||||
getattr(self, handler)(state)
|
|
||||||
|
|
||||||
labels = self._labels(state)
|
labels = self._labels(state)
|
||||||
state_change = self._metric(
|
state_change = self._metric(
|
||||||
@ -231,7 +225,7 @@ class PrometheusMetrics:
|
|||||||
prometheus_client.Gauge,
|
prometheus_client.Gauge,
|
||||||
"Entity is available (not in the unavailable or unknown state)",
|
"Entity is available (not in the unavailable or unknown state)",
|
||||||
)
|
)
|
||||||
entity_available.labels(**labels).set(float(state.state not in ignored_states))
|
entity_available.labels(**labels).set(float(state.state not in IGNORED_STATES))
|
||||||
|
|
||||||
last_updated_time_seconds = self._metric(
|
last_updated_time_seconds = self._metric(
|
||||||
"last_updated_time_seconds",
|
"last_updated_time_seconds",
|
||||||
@ -240,6 +234,18 @@ class PrometheusMetrics:
|
|||||||
)
|
)
|
||||||
last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp())
|
last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp())
|
||||||
|
|
||||||
|
if state.state in IGNORED_STATES:
|
||||||
|
self._remove_labelsets(
|
||||||
|
entity_id,
|
||||||
|
None,
|
||||||
|
{state_change, entity_available, last_updated_time_seconds},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
domain, _ = hacore.split_entity_id(entity_id)
|
||||||
|
handler = f"_handle_{domain}"
|
||||||
|
if hasattr(self, handler) and state.state:
|
||||||
|
getattr(self, handler)(state)
|
||||||
|
|
||||||
def handle_entity_registry_updated(
|
def handle_entity_registry_updated(
|
||||||
self, event: Event[EventEntityRegistryUpdatedData]
|
self, event: Event[EventEntityRegistryUpdatedData]
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -266,10 +272,17 @@ class PrometheusMetrics:
|
|||||||
self._remove_labelsets(metrics_entity_id)
|
self._remove_labelsets(metrics_entity_id)
|
||||||
|
|
||||||
def _remove_labelsets(
|
def _remove_labelsets(
|
||||||
self, entity_id: str, friendly_name: str | None = None
|
self,
|
||||||
|
entity_id: str,
|
||||||
|
friendly_name: str | None = None,
|
||||||
|
ignored_metrics: set[MetricWrapperBase] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Remove labelsets matching the given entity id from all metrics."""
|
"""Remove labelsets matching the given entity id from all non-ignored metrics."""
|
||||||
|
if ignored_metrics is None:
|
||||||
|
ignored_metrics = set()
|
||||||
for metric in list(self._metrics.values()):
|
for metric in list(self._metrics.values()):
|
||||||
|
if metric in ignored_metrics:
|
||||||
|
continue
|
||||||
for sample in cast(list[prometheus_client.Metric], metric.collect())[
|
for sample in cast(list[prometheus_client.Metric], metric.collect())[
|
||||||
0
|
0
|
||||||
].samples:
|
].samples:
|
||||||
@ -663,7 +676,7 @@ class PrometheusMetrics:
|
|||||||
def _sensor_override_component_metric(
|
def _sensor_override_component_metric(
|
||||||
self, state: State, unit: str | None
|
self, state: State, unit: str | None
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Get metric from override in component confioguration."""
|
"""Get metric from override in component configuration."""
|
||||||
return self._component_config.get(state.entity_id).get(CONF_OVERRIDE_METRIC)
|
return self._component_config.get(state.entity_id).get(CONF_OVERRIDE_METRIC)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -74,6 +74,7 @@ from homeassistant.const import (
|
|||||||
STATE_OPEN,
|
STATE_OPEN,
|
||||||
STATE_OPENING,
|
STATE_OPENING,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
UnitOfEnergy,
|
UnitOfEnergy,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
@ -1666,13 +1667,15 @@ async def test_disabling_entity(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("namespace", [""])
|
@pytest.mark.parametrize("namespace", [""])
|
||||||
async def test_entity_becomes_unavailable_with_export(
|
@pytest.mark.parametrize("unavailable_state", [STATE_UNAVAILABLE, STATE_UNKNOWN])
|
||||||
|
async def test_entity_becomes_unavailable(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
client: ClientSessionGenerator,
|
client: ClientSessionGenerator,
|
||||||
sensor_entities: dict[str, er.RegistryEntry],
|
sensor_entities: dict[str, er.RegistryEntry],
|
||||||
|
unavailable_state: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test an entity that becomes unavailable is still exported."""
|
"""Test an entity that becomes unavailable/unknown is no longer exported."""
|
||||||
data = {**sensor_entities}
|
data = {**sensor_entities}
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -1699,6 +1702,20 @@ async def test_entity_becomes_unavailable_with_export(
|
|||||||
entity="sensor.outside_temperature",
|
entity="sensor.outside_temperature",
|
||||||
).withValue(1).assert_in_metrics(body)
|
).withValue(1).assert_in_metrics(body)
|
||||||
|
|
||||||
|
EntityMetric(
|
||||||
|
metric_name="last_updated_time_seconds",
|
||||||
|
domain="sensor",
|
||||||
|
friendly_name="Outside Temperature",
|
||||||
|
entity="sensor.outside_temperature",
|
||||||
|
).assert_in_metrics(body)
|
||||||
|
|
||||||
|
EntityMetric(
|
||||||
|
metric_name="battery_level_percent",
|
||||||
|
domain="sensor",
|
||||||
|
friendly_name="Outside Temperature",
|
||||||
|
entity="sensor.outside_temperature",
|
||||||
|
).withValue(12.0).assert_in_metrics(body)
|
||||||
|
|
||||||
EntityMetric(
|
EntityMetric(
|
||||||
metric_name="sensor_humidity_percent",
|
metric_name="sensor_humidity_percent",
|
||||||
domain="sensor",
|
domain="sensor",
|
||||||
@ -1720,21 +1737,28 @@ async def test_entity_becomes_unavailable_with_export(
|
|||||||
entity="sensor.outside_humidity",
|
entity="sensor.outside_humidity",
|
||||||
).withValue(1).assert_in_metrics(body)
|
).withValue(1).assert_in_metrics(body)
|
||||||
|
|
||||||
# Make sensor_1 unavailable.
|
# Make sensor_1 unavailable/unknown.
|
||||||
set_state_with_entry(
|
set_state_with_entry(
|
||||||
hass, data["sensor_1"], STATE_UNAVAILABLE, data["sensor_1_attributes"]
|
hass, data["sensor_1"], unavailable_state, data["sensor_1_attributes"]
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
body = await generate_latest_metrics(client)
|
body = await generate_latest_metrics(client)
|
||||||
|
|
||||||
# Check that only the availability changed on sensor_1.
|
# Check that the availability changed on sensor_1 and the metric with the value is gone.
|
||||||
EntityMetric(
|
EntityMetric(
|
||||||
metric_name="sensor_temperature_celsius",
|
metric_name="sensor_temperature_celsius",
|
||||||
domain="sensor",
|
domain="sensor",
|
||||||
friendly_name="Outside Temperature",
|
friendly_name="Outside Temperature",
|
||||||
entity="sensor.outside_temperature",
|
entity="sensor.outside_temperature",
|
||||||
).withValue(15.6).assert_in_metrics(body)
|
).assert_not_in_metrics(body)
|
||||||
|
|
||||||
|
EntityMetric(
|
||||||
|
metric_name="battery_level_percent",
|
||||||
|
domain="sensor",
|
||||||
|
friendly_name="Outside Temperature",
|
||||||
|
entity="sensor.outside_temperature",
|
||||||
|
).assert_not_in_metrics(body)
|
||||||
|
|
||||||
EntityMetric(
|
EntityMetric(
|
||||||
metric_name="state_change_total",
|
metric_name="state_change_total",
|
||||||
@ -1750,6 +1774,13 @@ async def test_entity_becomes_unavailable_with_export(
|
|||||||
entity="sensor.outside_temperature",
|
entity="sensor.outside_temperature",
|
||||||
).withValue(0.0).assert_in_metrics(body)
|
).withValue(0.0).assert_in_metrics(body)
|
||||||
|
|
||||||
|
EntityMetric(
|
||||||
|
metric_name="last_updated_time_seconds",
|
||||||
|
domain="sensor",
|
||||||
|
friendly_name="Outside Temperature",
|
||||||
|
entity="sensor.outside_temperature",
|
||||||
|
).assert_in_metrics(body)
|
||||||
|
|
||||||
# The other sensor should be unchanged.
|
# The other sensor should be unchanged.
|
||||||
EntityMetric(
|
EntityMetric(
|
||||||
metric_name="sensor_humidity_percent",
|
metric_name="sensor_humidity_percent",
|
||||||
@ -1772,8 +1803,8 @@ async def test_entity_becomes_unavailable_with_export(
|
|||||||
entity="sensor.outside_humidity",
|
entity="sensor.outside_humidity",
|
||||||
).withValue(1).assert_in_metrics(body)
|
).withValue(1).assert_in_metrics(body)
|
||||||
|
|
||||||
# Bring sensor_1 back and check that it is correct.
|
# Bring sensor_1 back and check that it returned.
|
||||||
set_state_with_entry(hass, data["sensor_1"], 200.0, data["sensor_1_attributes"])
|
set_state_with_entry(hass, data["sensor_1"], 201.0, data["sensor_1_attributes"])
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
body = await generate_latest_metrics(client)
|
body = await generate_latest_metrics(client)
|
||||||
@ -1783,7 +1814,14 @@ async def test_entity_becomes_unavailable_with_export(
|
|||||||
domain="sensor",
|
domain="sensor",
|
||||||
friendly_name="Outside Temperature",
|
friendly_name="Outside Temperature",
|
||||||
entity="sensor.outside_temperature",
|
entity="sensor.outside_temperature",
|
||||||
).withValue(200.0).assert_in_metrics(body)
|
).withValue(201.0).assert_in_metrics(body)
|
||||||
|
|
||||||
|
EntityMetric(
|
||||||
|
metric_name="battery_level_percent",
|
||||||
|
domain="sensor",
|
||||||
|
friendly_name="Outside Temperature",
|
||||||
|
entity="sensor.outside_temperature",
|
||||||
|
).withValue(12.0).assert_in_metrics(body)
|
||||||
|
|
||||||
EntityMetric(
|
EntityMetric(
|
||||||
metric_name="state_change_total",
|
metric_name="state_change_total",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user