Skip to content

Commit

Permalink
Add Prometheus metrics relabeling (esphome#3734)
Browse files Browse the repository at this point in the history
Co-authored-by: Guillermo Ruffino <[email protected]>
  • Loading branch information
jangrewe and glmnet authored Aug 29, 2022
1 parent 2819166 commit 84bac83
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 52 deletions.
23 changes: 23 additions & 0 deletions esphome/components/prometheus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,39 @@
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),
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
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)
Expand All @@ -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]))
114 changes: 62 additions & 52 deletions esphome/components/prometheus/prometheus_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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("\"} "));
Expand All @@ -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"));
}
}
Expand All @@ -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"));
}
}
Expand All @@ -137,34 +147,34 @@ 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');
}
// 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');
Expand All @@ -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"));
Expand All @@ -195,53 +205,53 @@ 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"));
// Effect
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"));
Expand All @@ -260,33 +270,33 @@ 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');
}
} 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"));
}
}
Expand All @@ -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');
Expand All @@ -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');
Expand Down
22 changes: 22 additions & 0 deletions esphome/components/prometheus/prometheus_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#ifdef USE_ARDUINO

#include <map>
#include <utility>

#include "esphome/components/web_server_base/web_server_base.h"
#include "esphome/core/controller.h"
#include "esphome/core/component.h"
Expand All @@ -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")
Expand All @@ -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);
Expand Down Expand Up @@ -92,6 +112,8 @@ class PrometheusHandler : public AsyncWebHandler, public Component {

web_server_base::WebServerBase *base_;
bool include_internal_{false};
std::map<EntityBase *, std::string> relabel_map_id_;
std::map<EntityBase *, std::string> relabel_map_name_;
};

} // namespace prometheus
Expand Down
2 changes: 2 additions & 0 deletions esphome/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])


Expand Down
Loading

0 comments on commit 84bac83

Please sign in to comment.