From 84bac8356a76ee1710629f7c0dca81f201cf2617 Mon Sep 17 00:00:00 2001 From: Jan Grewe Date: Tue, 30 Aug 2022 00:55:55 +0200 Subject: [PATCH] Add Prometheus metrics relabeling (#3734) Co-authored-by: Guillermo Ruffino --- esphome/components/prometheus/__init__.py | 23 ++++ .../prometheus/prometheus_handler.cpp | 114 ++++++++++-------- .../prometheus/prometheus_handler.h | 22 ++++ esphome/config.py | 2 + esphome/const.py | 1 + tests/test4.yaml | 7 ++ 6 files changed, 117 insertions(+), 52 deletions(-) diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index e7c0459251..5b63710c6a 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -2,16 +2,27 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( CONF_ID, + CONF_NAME, CONF_INCLUDE_INTERNAL, + CONF_RELABEL, ) from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.components import web_server_base +from esphome.cpp_types import EntityBase AUTO_LOAD = ["web_server_base"] prometheus_ns = cg.esphome_ns.namespace("prometheus") PrometheusHandler = prometheus_ns.class_("PrometheusHandler", cg.Component) +CUSTOMIZED_ENTITY = cv.Schema( + { + cv.Optional(CONF_ID): cv.string_strict, + cv.Optional(CONF_NAME): cv.string_strict, + }, + cv.has_at_least_one_key, +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(PrometheusHandler), @@ -19,6 +30,11 @@ CONFIG_SCHEMA = cv.Schema( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, + cv.Optional(CONF_RELABEL, default={}): cv.Schema( + { + cv.use_id(EntityBase): CUSTOMIZED_ENTITY, + } + ), }, cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) @@ -33,3 +49,10 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) + + for key, value in config[CONF_RELABEL].items(): + entity = await cg.get_variable(key) + if CONF_ID in value: + cg.add(var.add_label_id(entity, value[CONF_ID])) + if CONF_NAME in value: + cg.add(var.add_label_name(entity, value[CONF_NAME])) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index a52347ba57..abb5111aaf 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -54,6 +54,16 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { req->send(stream); } +std::string PrometheusHandler::relabel_id_(EntityBase *obj) { + auto item = relabel_map_id_.find(obj); + return item == relabel_map_id_.end() ? obj->get_object_id() : item->second; +} + +std::string PrometheusHandler::relabel_name_(EntityBase *obj) { + auto item = relabel_map_name_.find(obj); + return item == relabel_map_name_.end() ? obj->get_name() : item->second; +} + // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { @@ -66,15 +76,15 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor if (!std::isnan(obj->state)) { // We have a valid value, output this value stream->print(F("esphome_sensor_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_sensor_value{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",unit=\"")); stream->print(obj->get_unit_of_measurement().c_str()); stream->print(F("\"} ")); @@ -83,9 +93,9 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor } else { // Invalid state stream->print(F("esphome_sensor_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); } } @@ -103,24 +113,24 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s if (obj->has_state()) { // We have a valid value, output this value stream->print(F("esphome_binary_sensor_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_binary_sensor_value{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); stream->print('\n'); } else { // Invalid state stream->print(F("esphome_binary_sensor_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); } } @@ -137,24 +147,24 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_fan_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_fan_value{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); stream->print('\n'); // Speed if available if (obj->get_traits().supports_speed()) { stream->print(F("esphome_fan_speed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->speed); stream->print('\n'); @@ -162,9 +172,9 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { // Oscillation if available if (obj->get_traits().supports_oscillation()) { stream->print(F("esphome_fan_oscillation{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->oscillating); stream->print('\n'); @@ -183,9 +193,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat return; // State stream->print(F("esphome_light_state{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->remote_values.is_on()); stream->print(F("\n")); @@ -195,37 +205,37 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat color.as_brightness(&brightness); color.as_rgbw(&r, &g, &b, &w); stream->print(F("esphome_light_color{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"brightness\"} ")); stream->print(brightness); stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"r\"} ")); stream->print(r); stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"g\"} ")); stream->print(g); stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"b\"} ")); stream->print(b); stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"w\"} ")); stream->print(w); stream->print(F("\n")); @@ -233,15 +243,15 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat std::string effect = obj->get_effect_name(); if (effect == "None") { stream->print(F("esphome_light_effect_active{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",effect=\"None\"} 0\n")); } else { stream->print(F("esphome_light_effect_active{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\",effect=\"")); stream->print(effect.c_str()); stream->print(F("\"} 1\n")); @@ -260,23 +270,23 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob if (!std::isnan(obj->position)) { // We have a valid value, output this value stream->print(F("esphome_cover_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_cover_value{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->position); stream->print('\n'); if (obj->get_traits().get_supports_tilt()) { stream->print(F("esphome_cover_tilt{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->tilt); stream->print('\n'); @@ -284,9 +294,9 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob } else { // Invalid state stream->print(F("esphome_cover_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); } } @@ -301,15 +311,15 @@ void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_switch_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_switch_value{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); stream->print('\n'); @@ -325,15 +335,15 @@ void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_lock_failed{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_lock_value{id=\"")); - stream->print(obj->get_object_id().c_str()); + stream->print(relabel_id_(obj).c_str()); stream->print(F("\",name=\"")); - stream->print(obj->get_name().c_str()); + stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); stream->print('\n'); diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index b378e46ea3..f416ecf246 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -2,6 +2,9 @@ #ifdef USE_ARDUINO +#include +#include + #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/controller.h" #include "esphome/core/component.h" @@ -20,6 +23,20 @@ class PrometheusHandler : public AsyncWebHandler, public Component { */ void set_include_internal(bool include_internal) { include_internal_ = include_internal; } + /** Add the value for an entity's "id" label. + * + * @param obj The entity for which to set the "id" label + * @param value The value for the "id" label + */ + void add_label_id(EntityBase *obj, const std::string &value) { relabel_map_id_.insert({obj, value}); } + + /** Add the value for an entity's "name" label. + * + * @param obj The entity for which to set the "name" label + * @param value The value for the "name" label + */ + void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } + bool canHandle(AsyncWebServerRequest *request) override { if (request->method() == HTTP_GET) { if (request->url() == "/metrics") @@ -41,6 +58,9 @@ class PrometheusHandler : public AsyncWebHandler, public Component { } protected: + std::string relabel_id_(EntityBase *obj); + std::string relabel_name_(EntityBase *obj); + #ifdef USE_SENSOR /// Return the type for prometheus void sensor_type_(AsyncResponseStream *stream); @@ -92,6 +112,8 @@ class PrometheusHandler : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool include_internal_{false}; + std::map relabel_map_id_; + std::map relabel_map_name_; }; } // namespace prometheus diff --git a/esphome/config.py b/esphome/config.py index 545b805367..56fe75a4c7 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -244,6 +244,8 @@ def iter_ids(config, path=None): yield from iter_ids(item, path + [i]) elif isinstance(config, dict): for key, value in config.items(): + if isinstance(key, core.ID): + yield key, path yield from iter_ids(value, path + [key]) diff --git a/esphome/const.py b/esphome/const.py index 7735e96661..5d3ddcf518 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -565,6 +565,7 @@ CONF_REF = "ref" CONF_REFERENCE_RESISTANCE = "reference_resistance" CONF_REFERENCE_TEMPERATURE = "reference_temperature" CONF_REFRESH = "refresh" +CONF_RELABEL = "relabel" CONF_REPEAT = "repeat" CONF_REPOSITORY = "repository" CONF_RESET_DURATION = "reset_duration" diff --git a/tests/test4.yaml b/tests/test4.yaml index 847639289e..c3079037c1 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -667,3 +667,10 @@ media_player: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% + +prometheus: + include_internal: true + relabel: + ha_hello_world: + id: hellow_world + name: "Hello World"