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:
Michael Hanselmann 2021-07-19 09:58:50 +02:00
parent 7d15e15d8a
commit 1b96833d6a
1 changed files with 171 additions and 52 deletions

View File

@ -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);
}
}
}