mirror of https://github.com/arendst/Tasmota.git
prometheus: Use utility functions to format metrics
Format strings for Prometheus metrics were written manually and the
`# TYPE` lines needed to be kept in sync with actual metrics. As
indicated by the previous commit it wasn't always as consistent as
desired. In addition there was a lot of repetition among the strings
which couldn't be reduced at build time.
With this change utility functions are introduced which eliminate the
need for specifying the same metric name more than once. At the same
time the proper escaping for label values, initially added in commit
16b5f2fe9
, is now applied for all labels.
The size of the program shrinks slightly by 212 bytes on ESP8266 in the
"tasmota" configuration with Prometheus enabled and 412 bytes on ESP32
with the "tasmota32" configuration.
Signed-off-by: Michael Hanselmann <public@hansmi.ch>
This commit is contained in:
parent
7d15e15d8a
commit
1b96833d6a
|
@ -74,14 +74,91 @@ String FormatMetricName(const char *metric) {
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels can be any sequence of UTF-8 characters, but backslash, double-quote
|
const uint8_t
|
||||||
// and line feed must be escaped.
|
kPromMetricNoPrefix = _BV(1),
|
||||||
String FormatLabelValue(const char *value) {
|
kPromMetricGauge = _BV(2),
|
||||||
String formatted = value;
|
kPromMetricCounter = _BV(3),
|
||||||
formatted.replace("\\", "\\\\");
|
kPromMetricTypeMask = kPromMetricGauge | kPromMetricCounter;
|
||||||
formatted.replace("\"", "\\\"");
|
|
||||||
formatted.replace("\n", "\\n");
|
// Format and send a Prometheus metric to the client. Use flags to configure
|
||||||
return formatted;
|
// the type. Labels must be supplied in tuples of two character array pointers
|
||||||
|
// and terminated by nullptr.
|
||||||
|
void WritePromMetric(const char *name, uint8_t flags, const char *value, va_list labels) {
|
||||||
|
PGM_P const prefix = (flags & kPromMetricNoPrefix) ? PSTR("") : PSTR("tasmota_");
|
||||||
|
PGM_P tmp;
|
||||||
|
String lval;
|
||||||
|
|
||||||
|
switch (flags & kPromMetricTypeMask) {
|
||||||
|
case kPromMetricGauge:
|
||||||
|
tmp = PSTR("gauge");
|
||||||
|
break;
|
||||||
|
case kPromMetricCounter:
|
||||||
|
tmp = PSTR("counter");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
tmp = nullptr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tmp != nullptr) {
|
||||||
|
WSContentSend_P(PSTR("# TYPE %s%s %s\n"), prefix, name, tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
WSContentSend_P(PSTR("%s%s{"), prefix, name);
|
||||||
|
|
||||||
|
for (const char *sep = PSTR(""); ; sep = PSTR(",")) {
|
||||||
|
if ((tmp = va_arg(labels, PGM_P)) == nullptr) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A few label values are stored in PROGMEM. The _P functions, e.g.
|
||||||
|
// snprintf_P, support both program and heap/stack memory with the "%s"
|
||||||
|
// format on ESP8266/ESP32. Casting the pointer to __FlashStringHelper has
|
||||||
|
// the same effect with String::operator=.
|
||||||
|
if (!(lval = va_arg(labels, const __FlashStringHelper *))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels can be any sequence of UTF-8 characters, but backslash,
|
||||||
|
// double-quote and line feed must be escaped.
|
||||||
|
lval.replace("\\", "\\\\");
|
||||||
|
lval.replace("\"", "\\\"");
|
||||||
|
lval.replace("\n", "\\n");
|
||||||
|
|
||||||
|
WSContentSend_P(PSTR("%s%s=\"%s\""), sep, tmp, lval.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
WSContentSend_P(PSTR("} %s\n"), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WritePromMetricInt32(const char *name, uint8_t flags, const int32_t value, ...) {
|
||||||
|
char str[16];
|
||||||
|
|
||||||
|
snprintf_P(str, sizeof(str), PSTR("%d"), value);
|
||||||
|
|
||||||
|
va_list labels;
|
||||||
|
va_start(labels, value);
|
||||||
|
WritePromMetric(name, flags, str, labels);
|
||||||
|
va_end(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WritePromMetricDec(const char *name, uint8_t flags, double number, unsigned char prec, ...) {
|
||||||
|
char value[FLOATSZ];
|
||||||
|
|
||||||
|
// Prometheus always requires "." as the decimal separator.
|
||||||
|
dtostrfd(number, prec, value);
|
||||||
|
|
||||||
|
va_list labels;
|
||||||
|
va_start(labels, prec);
|
||||||
|
WritePromMetric(name, flags, value, labels);
|
||||||
|
va_end(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WritePromMetricStr(const char *name, uint8_t flags, const char *value, ...) {
|
||||||
|
va_list labels;
|
||||||
|
va_start(labels, value);
|
||||||
|
WritePromMetric(name, flags, value, labels);
|
||||||
|
va_end(labels);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleMetrics(void) {
|
void HandleMetrics(void) {
|
||||||
|
@ -91,63 +168,94 @@ void HandleMetrics(void) {
|
||||||
|
|
||||||
WSContentBegin(200, CT_PLAIN);
|
WSContentBegin(200, CT_PLAIN);
|
||||||
|
|
||||||
char parameter[FLOATSZ];
|
char namebuf[64];
|
||||||
|
|
||||||
// Pseudo-metric providing metadata about the running firmware version.
|
// Pseudo-metric providing metadata about the running firmware version.
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_info gauge\ntasmota_info{version=\"%s\",image=\"%s\",build_timestamp=\"%s\",devicename=\"%s\"} 1\n"),
|
WritePromMetricInt32(PSTR("info"), kPromMetricGauge, 1,
|
||||||
TasmotaGlobal.version, TasmotaGlobal.image_name, GetBuildDateAndTime().c_str(), FormatLabelValue(SettingsText(SET_DEVICENAME)).c_str());
|
PSTR("version"), TasmotaGlobal.version,
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_uptime_seconds gauge\ntasmota_uptime_seconds %d\n"), TasmotaGlobal.uptime);
|
PSTR("image"), TasmotaGlobal.image_name,
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_boot_count counter\ntasmota_boot_count %d\n"), Settings->bootcount);
|
PSTR("build_timestamp"), GetBuildDateAndTime().c_str(),
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_flash_writes_total counter\ntasmota_flash_writes_total %d\n"), Settings->save_flag);
|
PSTR("devicename"), SettingsText(SET_DEVICENAME),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
WritePromMetricInt32(PSTR("uptime_seconds"), kPromMetricGauge, TasmotaGlobal.uptime, nullptr);
|
||||||
|
WritePromMetricInt32(PSTR("boot_count"), kPromMetricCounter, Settings->bootcount, nullptr);
|
||||||
|
WritePromMetricInt32(PSTR("flash_writes_total"), kPromMetricCounter, Settings->save_flag, nullptr);
|
||||||
|
|
||||||
// Pseudo-metric providing metadata about the WiFi station.
|
// Pseudo-metric providing metadata about the WiFi station.
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_wifi_station_info gauge\ntasmota_wifi_station_info{bssid=\"%s\",ssid=\"%s\"} 1\n"), WiFi.BSSIDstr().c_str(), WiFi.SSID().c_str());
|
WritePromMetricInt32(PSTR("wifi_station_info"), kPromMetricGauge, 1,
|
||||||
|
PSTR("bssid"), WiFi.BSSIDstr().c_str(),
|
||||||
|
PSTR("ssid"), WiFi.SSID().c_str(),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
// Wi-Fi Signal strength
|
// Wi-Fi Signal strength
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_wifi_station_signal_dbm gauge\ntasmota_wifi_station_signal_dbm{mac_address=\"%s\"} %d\n"), WiFi.BSSIDstr().c_str(), WiFi.RSSI());
|
WritePromMetricInt32(PSTR("wifi_station_signal_dbm"), kPromMetricGauge, WiFi.RSSI(),
|
||||||
|
PSTR("mac_address"), WiFi.BSSIDstr().c_str(),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
if (!isnan(TasmotaGlobal.temperature_celsius)) {
|
if (!isnan(TasmotaGlobal.temperature_celsius)) {
|
||||||
dtostrfd(TasmotaGlobal.temperature_celsius, Settings->flag2.temperature_resolution, parameter);
|
WritePromMetricDec(PSTR("global_temperature_celsius"), kPromMetricGauge,
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_global_temperature_celsius gauge\ntasmota_global_temperature_celsius %s\n"), parameter);
|
TasmotaGlobal.temperature_celsius, Settings->flag2.temperature_resolution,
|
||||||
|
nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TasmotaGlobal.humidity != 0) {
|
if (TasmotaGlobal.humidity != 0) {
|
||||||
dtostrfd(TasmotaGlobal.humidity, Settings->flag2.humidity_resolution, parameter);
|
WritePromMetricDec(PSTR("global_humidity_percentage"), kPromMetricGauge,
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_global_humidity_percentage gauge\ntasmota_global_humidity_percentage %s\n"), parameter);
|
TasmotaGlobal.humidity, Settings->flag2.humidity_resolution,
|
||||||
|
nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TasmotaGlobal.pressure_hpa != 0) {
|
if (TasmotaGlobal.pressure_hpa != 0) {
|
||||||
dtostrfd(TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution, parameter);
|
WritePromMetricDec(PSTR("global_pressure_hpa"), kPromMetricGauge,
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_global_pressure_hpa gauge\ntasmota_global_pressure_hpa %s\n"), parameter);
|
TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution,
|
||||||
|
nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pseudo-metric providing metadata about the free memory.
|
// Pseudo-metric providing metadata about the free memory.
|
||||||
#ifdef ESP32
|
#ifdef ESP32
|
||||||
int32_t freeMaxMem = 100 - (int32_t)(ESP_getMaxAllocHeap() * 100 / ESP_getFreeHeap());
|
int32_t freeMaxMem = 100 - (int32_t)(ESP_getMaxAllocHeap() * 100 / ESP_getFreeHeap());
|
||||||
WSContentSend_PD(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"Ram\"} %d\n"), ESP_getFreeHeap());
|
|
||||||
WSContentSend_PD(PSTR("# TYPE tasmota_memory_ratio gauge\ntasmota_memory_ratio{memory=\"Fragmentation\"} %d\n"), freeMaxMem / 100);
|
WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge,
|
||||||
if (UsePSRAM()) {
|
ESP_getFreeHeap(), PSTR("memory"), PSTR("Ram"), nullptr);
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"Psram\"} %d\n"), ESP.getFreePsram() );
|
|
||||||
}
|
// FIXME: Always truncated to integer
|
||||||
#else // ESP32
|
WritePromMetricInt32(PSTR("memory_ratio"), kPromMetricGauge,
|
||||||
WSContentSend_PD(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"ram\"} %d\n"), ESP_getFreeHeap());
|
freeMaxMem / 100, PSTR("memory"), PSTR("Fragmentation"), nullptr);
|
||||||
#endif // ESP32
|
|
||||||
|
if (UsePSRAM()) {
|
||||||
|
WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge,
|
||||||
|
ESP.getFreePsram(), PSTR("memory"), PSTR("Psram"), nullptr);
|
||||||
|
}
|
||||||
|
#else // ESP32
|
||||||
|
WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge,
|
||||||
|
ESP_getFreeHeap(), PSTR("memory"), PSTR("ram"), nullptr);
|
||||||
|
#endif // ESP32
|
||||||
|
|
||||||
#ifdef USE_ENERGY_SENSOR
|
#ifdef USE_ENERGY_SENSOR
|
||||||
dtostrfd(Energy.voltage[0], Settings->flag2.voltage_resolution, parameter);
|
// TODO: Don't disable prefix on energy metrics
|
||||||
WSContentSend_P(PSTR("# TYPE energy_voltage_volts gauge\nenergy_voltage_volts %s\n"), parameter);
|
WritePromMetricDec(PSTR("energy_voltage_volts"),
|
||||||
dtostrfd(Energy.current[0], Settings->flag2.current_resolution, parameter);
|
kPromMetricGauge | kPromMetricNoPrefix,
|
||||||
WSContentSend_P(PSTR("# TYPE energy_current_amperes gauge\nenergy_current_amperes %s\n"), parameter);
|
Energy.voltage[0], Settings->flag2.voltage_resolution, nullptr);
|
||||||
dtostrfd(Energy.active_power[0], Settings->flag2.wattage_resolution, parameter);
|
WritePromMetricDec(PSTR("energy_current_amperes"),
|
||||||
WSContentSend_P(PSTR("# TYPE energy_power_active_watts gauge\nenergy_power_active_watts %s\n"), parameter);
|
kPromMetricGauge | kPromMetricNoPrefix,
|
||||||
dtostrfd(Energy.daily, Settings->flag2.energy_resolution, parameter);
|
Energy.current[0], Settings->flag2.current_resolution, nullptr);
|
||||||
WSContentSend_P(PSTR("# TYPE energy_power_kilowatts_daily counter\nenergy_power_kilowatts_daily %s\n"), parameter);
|
WritePromMetricDec(PSTR("energy_power_active_watts"),
|
||||||
dtostrfd(Energy.total, Settings->flag2.energy_resolution, parameter);
|
kPromMetricGauge | kPromMetricNoPrefix,
|
||||||
WSContentSend_P(PSTR("# TYPE energy_power_kilowatts_total counter\nenergy_power_kilowatts_total %s\n"), parameter);
|
Energy.active_power[0], Settings->flag2.wattage_resolution, nullptr);
|
||||||
|
WritePromMetricDec(PSTR("energy_power_kilowatts_daily"),
|
||||||
|
kPromMetricCounter | kPromMetricNoPrefix,
|
||||||
|
Energy.daily, Settings->flag2.energy_resolution, nullptr);
|
||||||
|
WritePromMetricDec(PSTR("energy_power_kilowatts_total"),
|
||||||
|
kPromMetricCounter | kPromMetricNoPrefix,
|
||||||
|
Energy.total, Settings->flag2.energy_resolution, nullptr);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
for (uint32_t device = 0; device < TasmotaGlobal.devices_present; device++) {
|
for (uint32_t device = 0; device < TasmotaGlobal.devices_present; device++) {
|
||||||
power_t mask = 1 << device;
|
power_t mask = 1 << device;
|
||||||
WSContentSend_P(PSTR("# TYPE relay%d_state gauge\nrelay%d_state %d\n"), device+1, device+1, (TasmotaGlobal.power & mask));
|
// TODO: Don't disable prefix
|
||||||
|
snprintf_P(namebuf, sizeof(namebuf), PSTR("relay%d_state"), device + 1);
|
||||||
|
WritePromMetricInt32(namebuf, kPromMetricGauge | kPromMetricNoPrefix,
|
||||||
|
(TasmotaGlobal.power & mask), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseClear();
|
ResponseClear();
|
||||||
|
@ -169,9 +277,12 @@ void HandleMetrics(void) {
|
||||||
if (value != nullptr && isdigit(value[0])) {
|
if (value != nullptr && isdigit(value[0])) {
|
||||||
String sensor = FormatMetricName(key2.getStr());
|
String sensor = FormatMetricName(key2.getStr());
|
||||||
String type = FormatMetricName(key3.getStr());
|
String type = FormatMetricName(key3.getStr());
|
||||||
const char *unit = UnitfromType(type.c_str()); //grab base unit corresponding to type
|
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\"} %s\n"),
|
snprintf_P(namebuf, sizeof(namebuf), PSTR("sensors_%s_%s"),
|
||||||
type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value); //build metric as "# TYPE tasmota_sensors_%type%_%unit% gauge\ntasmotasensors_%type%_%unit%{sensor=%sensor%"} %value%""
|
type.c_str(), UnitfromType(type.c_str()));
|
||||||
|
WritePromMetricStr(namebuf, kPromMetricGauge, value,
|
||||||
|
PSTR("sensor"), sensor.c_str(),
|
||||||
|
nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -179,14 +290,19 @@ void HandleMetrics(void) {
|
||||||
if (value != nullptr && isdigit(value[0])) {
|
if (value != nullptr && isdigit(value[0])) {
|
||||||
String sensor = FormatMetricName(key1.getStr());
|
String sensor = FormatMetricName(key1.getStr());
|
||||||
String type = FormatMetricName(key2.getStr());
|
String type = FormatMetricName(key2.getStr());
|
||||||
const char *unit = UnitfromType(type.c_str());
|
|
||||||
if (strcmp(type.c_str(), "totalstarttime") != 0) { // this metric causes Prometheus of fail
|
if (strcmp(type.c_str(), "totalstarttime") != 0) { // this metric causes Prometheus of fail
|
||||||
|
snprintf_P(namebuf, sizeof(namebuf), PSTR("sensors_%s_%s"),
|
||||||
|
type.c_str(), UnitfromType(type.c_str()));
|
||||||
|
|
||||||
if (strcmp(type.c_str(), "id") == 0) { // this metric is NaN, so convert it to a label, see Wi-Fi metrics above
|
if (strcmp(type.c_str(), "id") == 0) { // this metric is NaN, so convert it to a label, see Wi-Fi metrics above
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\",id=\"%s\"} 1\n"),
|
WritePromMetricInt32(namebuf, kPromMetricGauge, 1,
|
||||||
type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value);
|
PSTR("sensor"), sensor.c_str(),
|
||||||
|
PSTR("id"), value,
|
||||||
|
nullptr);
|
||||||
} else {
|
} else {
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\"} %s\n"),
|
WritePromMetricStr(namebuf, kPromMetricGauge, value,
|
||||||
type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value);
|
PSTR("sensor"), sensor.c_str(),
|
||||||
|
nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,8 +311,11 @@ void HandleMetrics(void) {
|
||||||
} else {
|
} else {
|
||||||
const char *value = value1.getStr(nullptr);
|
const char *value = value1.getStr(nullptr);
|
||||||
String sensor = FormatMetricName(key1.getStr());
|
String sensor = FormatMetricName(key1.getStr());
|
||||||
|
|
||||||
if (value != nullptr && isdigit(value[0] && strcmp(sensor.c_str(), "time") != 0)) { //remove false 'time' metric
|
if (value != nullptr && isdigit(value[0] && strcmp(sensor.c_str(), "time") != 0)) { //remove false 'time' metric
|
||||||
WSContentSend_P(PSTR("# TYPE tasmota_sensors gauge\ntasmota_sensors{sensor=\"%s\"} %s\n"), sensor.c_str(), value);
|
WritePromMetricStr(PSTR("sensors"), kPromMetricGauge, value,
|
||||||
|
PSTR("sensor"), sensor.c_str(),
|
||||||
|
nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue