From 1b96833d6afc386c7b870e1ddb840ef43a93ed26 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Mon, 19 Jul 2021 09:58:50 +0200 Subject: [PATCH] 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 --- tasmota/xsns_75_prometheus.ino | 223 +++++++++++++++++++++++++-------- 1 file changed, 171 insertions(+), 52 deletions(-) diff --git a/tasmota/xsns_75_prometheus.ino b/tasmota/xsns_75_prometheus.ino index 2a560c743..8640a8d02 100644 --- a/tasmota/xsns_75_prometheus.ino +++ b/tasmota/xsns_75_prometheus.ino @@ -74,14 +74,91 @@ String FormatMetricName(const char *metric) { return formatted; } -// Labels can be any sequence of UTF-8 characters, but backslash, double-quote -// and line feed must be escaped. -String FormatLabelValue(const char *value) { - String formatted = value; - formatted.replace("\\", "\\\\"); - formatted.replace("\"", "\\\""); - formatted.replace("\n", "\\n"); - return formatted; +const uint8_t + kPromMetricNoPrefix = _BV(1), + kPromMetricGauge = _BV(2), + kPromMetricCounter = _BV(3), + kPromMetricTypeMask = kPromMetricGauge | kPromMetricCounter; + +// Format and send a Prometheus metric to the client. Use flags to configure +// 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) { @@ -91,63 +168,94 @@ void HandleMetrics(void) { WSContentBegin(200, CT_PLAIN); - char parameter[FLOATSZ]; + char namebuf[64]; // 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"), - TasmotaGlobal.version, TasmotaGlobal.image_name, GetBuildDateAndTime().c_str(), FormatLabelValue(SettingsText(SET_DEVICENAME)).c_str()); - WSContentSend_P(PSTR("# TYPE tasmota_uptime_seconds gauge\ntasmota_uptime_seconds %d\n"), TasmotaGlobal.uptime); - WSContentSend_P(PSTR("# TYPE tasmota_boot_count counter\ntasmota_boot_count %d\n"), Settings->bootcount); - WSContentSend_P(PSTR("# TYPE tasmota_flash_writes_total counter\ntasmota_flash_writes_total %d\n"), Settings->save_flag); + WritePromMetricInt32(PSTR("info"), kPromMetricGauge, 1, + PSTR("version"), TasmotaGlobal.version, + PSTR("image"), TasmotaGlobal.image_name, + PSTR("build_timestamp"), GetBuildDateAndTime().c_str(), + 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. - 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 - 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)) { - dtostrfd(TasmotaGlobal.temperature_celsius, Settings->flag2.temperature_resolution, parameter); - WSContentSend_P(PSTR("# TYPE tasmota_global_temperature_celsius gauge\ntasmota_global_temperature_celsius %s\n"), parameter); + WritePromMetricDec(PSTR("global_temperature_celsius"), kPromMetricGauge, + TasmotaGlobal.temperature_celsius, Settings->flag2.temperature_resolution, + nullptr); } + if (TasmotaGlobal.humidity != 0) { - dtostrfd(TasmotaGlobal.humidity, Settings->flag2.humidity_resolution, parameter); - WSContentSend_P(PSTR("# TYPE tasmota_global_humidity_percentage gauge\ntasmota_global_humidity_percentage %s\n"), parameter); + WritePromMetricDec(PSTR("global_humidity_percentage"), kPromMetricGauge, + TasmotaGlobal.humidity, Settings->flag2.humidity_resolution, + nullptr); } + if (TasmotaGlobal.pressure_hpa != 0) { - dtostrfd(TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution, parameter); - WSContentSend_P(PSTR("# TYPE tasmota_global_pressure_hpa gauge\ntasmota_global_pressure_hpa %s\n"), parameter); + WritePromMetricDec(PSTR("global_pressure_hpa"), kPromMetricGauge, + TasmotaGlobal.pressure_hpa, Settings->flag2.pressure_resolution, + nullptr); } // Pseudo-metric providing metadata about the free memory. - #ifdef ESP32 - 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); - if (UsePSRAM()) { - WSContentSend_P(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"Psram\"} %d\n"), ESP.getFreePsram() ); - } - #else // ESP32 - WSContentSend_PD(PSTR("# TYPE tasmota_memory_bytes gauge\ntasmota_memory_bytes{memory=\"ram\"} %d\n"), ESP_getFreeHeap()); - #endif // ESP32 +#ifdef ESP32 + int32_t freeMaxMem = 100 - (int32_t)(ESP_getMaxAllocHeap() * 100 / ESP_getFreeHeap()); + + WritePromMetricInt32(PSTR("memory_bytes"), kPromMetricGauge, + ESP_getFreeHeap(), PSTR("memory"), PSTR("Ram"), nullptr); + + // FIXME: Always truncated to integer + WritePromMetricInt32(PSTR("memory_ratio"), kPromMetricGauge, + freeMaxMem / 100, PSTR("memory"), PSTR("Fragmentation"), nullptr); + + 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 - dtostrfd(Energy.voltage[0], Settings->flag2.voltage_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_voltage_volts gauge\nenergy_voltage_volts %s\n"), parameter); - dtostrfd(Energy.current[0], Settings->flag2.current_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_current_amperes gauge\nenergy_current_amperes %s\n"), parameter); - dtostrfd(Energy.active_power[0], Settings->flag2.wattage_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_power_active_watts gauge\nenergy_power_active_watts %s\n"), parameter); - dtostrfd(Energy.daily, Settings->flag2.energy_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_power_kilowatts_daily counter\nenergy_power_kilowatts_daily %s\n"), parameter); - dtostrfd(Energy.total, Settings->flag2.energy_resolution, parameter); - WSContentSend_P(PSTR("# TYPE energy_power_kilowatts_total counter\nenergy_power_kilowatts_total %s\n"), parameter); + // TODO: Don't disable prefix on energy metrics + WritePromMetricDec(PSTR("energy_voltage_volts"), + kPromMetricGauge | kPromMetricNoPrefix, + Energy.voltage[0], Settings->flag2.voltage_resolution, nullptr); + WritePromMetricDec(PSTR("energy_current_amperes"), + kPromMetricGauge | kPromMetricNoPrefix, + Energy.current[0], Settings->flag2.current_resolution, nullptr); + WritePromMetricDec(PSTR("energy_power_active_watts"), + kPromMetricGauge | kPromMetricNoPrefix, + 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 for (uint32_t device = 0; device < TasmotaGlobal.devices_present; 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(); @@ -169,9 +277,12 @@ void HandleMetrics(void) { if (value != nullptr && isdigit(value[0])) { String sensor = FormatMetricName(key2.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"), - 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%"" + + snprintf_P(namebuf, sizeof(namebuf), PSTR("sensors_%s_%s"), + type.c_str(), UnitfromType(type.c_str())); + WritePromMetricStr(namebuf, kPromMetricGauge, value, + PSTR("sensor"), sensor.c_str(), + nullptr); } } } else { @@ -179,14 +290,19 @@ void HandleMetrics(void) { if (value != nullptr && isdigit(value[0])) { String sensor = FormatMetricName(key1.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 + 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 - WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\",id=\"%s\"} 1\n"), - type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value); + WritePromMetricInt32(namebuf, kPromMetricGauge, 1, + PSTR("sensor"), sensor.c_str(), + PSTR("id"), value, + nullptr); } else { - WSContentSend_P(PSTR("# TYPE tasmota_sensors_%s_%s gauge\ntasmota_sensors_%s_%s{sensor=\"%s\"} %s\n"), - type.c_str(), unit, type.c_str(), unit, sensor.c_str(), value); + WritePromMetricStr(namebuf, kPromMetricGauge, value, + PSTR("sensor"), sensor.c_str(), + nullptr); } } } @@ -195,8 +311,11 @@ void HandleMetrics(void) { } else { const char *value = value1.getStr(nullptr); String sensor = FormatMetricName(key1.getStr()); + 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); } } }