mirror of
https://github.com/home-assistant/core
synced 2024-08-02 23:40:32 +02:00
Fix duplicate metrics in prometheus (#61355)
* Fix duplicate metrics in prometheus * Fix duplicate prometheus metrics for entities with multiple labelsets - Move friendly_name detection to state_changed event - Add additional test case * Add review suggestions for prometheus friendly name update * Remove commented out code in prometheus * Update prometheus tests for deleted metrics * Add review suggestions for prometheus - Remove unnecessary firendly_name check in handle_entity_registry_updated - Add assert in test
This commit is contained in:
parent
51a04585e7
commit
3e0e9e54bf
@ -40,6 +40,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entityfilter, state as state_helper
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.entity_values import EntityValues
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.temperature import fahrenheit_to_celsius
|
||||
@ -112,7 +113,10 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
default_metric,
|
||||
)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event)
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed)
|
||||
hass.bus.listen(
|
||||
EVENT_ENTITY_REGISTRY_UPDATED, metrics.handle_entity_registry_updated
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@ -150,7 +154,7 @@ class PrometheusMetrics:
|
||||
self._metrics = {}
|
||||
self._climate_units = climate_units
|
||||
|
||||
def handle_event(self, event):
|
||||
def handle_state_changed(self, event):
|
||||
"""Listen for new messages on the bus, and add them to Prometheus."""
|
||||
if (state := event.data.get("new_state")) is None:
|
||||
return
|
||||
@ -162,6 +166,11 @@ class PrometheusMetrics:
|
||||
if not self._filter(state.entity_id):
|
||||
return
|
||||
|
||||
if (old_state := event.data.get("old_state")) is not None and (
|
||||
old_friendly_name := old_state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
) != state.attributes.get(ATTR_FRIENDLY_NAME):
|
||||
self._remove_labelsets(old_state.entity_id, old_friendly_name)
|
||||
|
||||
ignored_states = (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
|
||||
handler = f"_handle_{domain}"
|
||||
@ -189,6 +198,46 @@ class PrometheusMetrics:
|
||||
)
|
||||
last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp())
|
||||
|
||||
def handle_entity_registry_updated(self, event):
|
||||
"""Listen for deleted, disabled or renamed entities and remove them from the Prometheus Registry."""
|
||||
if (action := event.data.get("action")) in (None, "create"):
|
||||
return
|
||||
|
||||
entity_id = event.data.get("entity_id")
|
||||
_LOGGER.debug("Handling entity update for %s", entity_id)
|
||||
|
||||
metrics_entity_id = None
|
||||
|
||||
if action == "remove":
|
||||
metrics_entity_id = entity_id
|
||||
elif action == "update":
|
||||
changes = event.data.get("changes")
|
||||
|
||||
if "entity_id" in changes:
|
||||
metrics_entity_id = changes["entity_id"]
|
||||
elif "disabled_by" in changes:
|
||||
metrics_entity_id = entity_id
|
||||
|
||||
if metrics_entity_id:
|
||||
self._remove_labelsets(metrics_entity_id)
|
||||
|
||||
def _remove_labelsets(self, entity_id, friendly_name=None):
|
||||
"""Remove labelsets matching the given entity id from all metrics."""
|
||||
for _, metric in self._metrics.items():
|
||||
for sample in metric.collect()[0].samples:
|
||||
if sample.labels["entity"] == entity_id and (
|
||||
not friendly_name or sample.labels["friendly_name"] == friendly_name
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Removing labelset from %s for entity_id: %s",
|
||||
sample.name,
|
||||
entity_id,
|
||||
)
|
||||
try:
|
||||
metric.remove(*sample.labels.values())
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _handle_attributes(self, state):
|
||||
for key, value in state.attributes.items():
|
||||
metric = self._metric(
|
||||
|
@ -24,6 +24,7 @@ from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@ -726,6 +727,392 @@ async def test_counter(hass, hass_client):
|
||||
)
|
||||
|
||||
|
||||
async def test_renaming_entity_name(hass, hass_client):
|
||||
"""Test renaming entity name."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"conversation",
|
||||
{},
|
||||
)
|
||||
client = await setup_prometheus_client(hass, hass_client, "")
|
||||
|
||||
assert await async_setup_component(
|
||||
hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]}
|
||||
)
|
||||
|
||||
assert await async_setup_component(
|
||||
hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
assert (
|
||||
'sensor_temperature_celsius{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 15.6' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="heating",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="cooling",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump"} 0.0' in body
|
||||
)
|
||||
|
||||
registry = entity_registry.async_get(hass)
|
||||
assert "sensor.outside_temperature" in registry.entities
|
||||
assert "climate.heatpump" in registry.entities
|
||||
registry.async_update_entity(
|
||||
entity_id="sensor.outside_temperature",
|
||||
name="Outside Temperature Renamed",
|
||||
)
|
||||
registry.async_update_entity(
|
||||
entity_id="climate.heatpump",
|
||||
name="HeatPump Renamed",
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
# Check if old metrics deleted
|
||||
body_line = "\n".join(body)
|
||||
assert 'friendly_name="Outside Temperature"' not in body_line
|
||||
assert 'friendly_name="HeatPump"' not in body_line
|
||||
|
||||
# Check if new metrics created
|
||||
assert (
|
||||
'sensor_temperature_celsius{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature Renamed"} 15.6' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature Renamed"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="heating",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump Renamed"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="cooling",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump Renamed"} 0.0' in body
|
||||
)
|
||||
|
||||
# Keep other sensors
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
|
||||
async def test_renaming_entity_id(hass, hass_client):
|
||||
"""Test renaming entity id."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"conversation",
|
||||
{},
|
||||
)
|
||||
client = await setup_prometheus_client(hass, hass_client, "")
|
||||
|
||||
assert await async_setup_component(
|
||||
hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
assert (
|
||||
'sensor_temperature_celsius{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 15.6' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
registry = entity_registry.async_get(hass)
|
||||
assert "sensor.outside_temperature" in registry.entities
|
||||
registry.async_update_entity(
|
||||
entity_id="sensor.outside_temperature",
|
||||
new_entity_id="sensor.outside_temperature_renamed",
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
# Check if old metrics deleted
|
||||
body_line = "\n".join(body)
|
||||
assert 'entity="sensor.outside_temperature"' not in body_line
|
||||
|
||||
# Check if new metrics created
|
||||
assert (
|
||||
'sensor_temperature_celsius{domain="sensor",'
|
||||
'entity="sensor.outside_temperature_renamed",'
|
||||
'friendly_name="Outside Temperature"} 15.6' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_temperature_renamed",'
|
||||
'friendly_name="Outside Temperature"} 1.0' in body
|
||||
)
|
||||
|
||||
# Keep other sensors
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
|
||||
async def test_deleting_entity(hass, hass_client):
|
||||
"""Test deleting a entity."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"conversation",
|
||||
{},
|
||||
)
|
||||
client = await setup_prometheus_client(hass, hass_client, "")
|
||||
|
||||
await async_setup_component(
|
||||
hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]}
|
||||
)
|
||||
|
||||
assert await async_setup_component(
|
||||
hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
assert (
|
||||
'sensor_temperature_celsius{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 15.6' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="heating",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="cooling",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump"} 0.0' in body
|
||||
)
|
||||
|
||||
registry = entity_registry.async_get(hass)
|
||||
assert "sensor.outside_temperature" in registry.entities
|
||||
assert "climate.heatpump" in registry.entities
|
||||
registry.async_remove("sensor.outside_temperature")
|
||||
registry.async_remove("climate.heatpump")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
# Check if old metrics deleted
|
||||
body_line = "\n".join(body)
|
||||
assert 'entity="sensor.outside_temperature"' not in body_line
|
||||
assert 'friendly_name="Outside Temperature"' not in body_line
|
||||
assert 'entity="climate.heatpump"' not in body_line
|
||||
assert 'friendly_name="HeatPump"' not in body_line
|
||||
|
||||
# Keep other sensors
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
|
||||
async def test_disabling_entity(hass, hass_client):
|
||||
"""Test disabling a entity."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"conversation",
|
||||
{},
|
||||
)
|
||||
client = await setup_prometheus_client(hass, hass_client, "")
|
||||
|
||||
await async_setup_component(
|
||||
hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]}
|
||||
)
|
||||
|
||||
assert await async_setup_component(
|
||||
hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
assert (
|
||||
'sensor_temperature_celsius{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 15.6' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'state_change_total{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"} 1.0' in body
|
||||
)
|
||||
|
||||
assert any(
|
||||
'state_change_created{domain="sensor",'
|
||||
'entity="sensor.outside_temperature",'
|
||||
'friendly_name="Outside Temperature"}' in metric
|
||||
for metric in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="heating",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump"} 1.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'climate_action{action="cooling",'
|
||||
'domain="climate",'
|
||||
'entity="climate.heatpump",'
|
||||
'friendly_name="HeatPump"} 0.0' in body
|
||||
)
|
||||
|
||||
registry = entity_registry.async_get(hass)
|
||||
assert "sensor.outside_temperature" in registry.entities
|
||||
assert "climate.heatpump" in registry.entities
|
||||
registry.async_update_entity(
|
||||
entity_id="sensor.outside_temperature",
|
||||
disabled_by="user",
|
||||
)
|
||||
registry.async_update_entity(entity_id="climate.heatpump", disabled_by="user")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
body = await generate_latest_metrics(client)
|
||||
|
||||
# Check if old metrics deleted
|
||||
body_line = "\n".join(body)
|
||||
assert 'entity="sensor.outside_temperature"' not in body_line
|
||||
assert 'friendly_name="Outside Temperature"' not in body_line
|
||||
assert 'entity="climate.heatpump"' not in body_line
|
||||
assert 'friendly_name="HeatPump"' not in body_line
|
||||
|
||||
# Keep other sensors
|
||||
assert (
|
||||
'sensor_humidity_percent{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 54.0' in body
|
||||
)
|
||||
|
||||
assert (
|
||||
'entity_available{domain="sensor",'
|
||||
'entity="sensor.outside_humidity",'
|
||||
'friendly_name="Outside Humidity"} 1.0' in body
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_client")
|
||||
def mock_client_fixture():
|
||||
"""Mock the prometheus client."""
|
||||
|
Loading…
Reference in New Issue
Block a user