From 760f827d0db692ca1f96e0aae48e5e96040d15ef Mon Sep 17 00:00:00 2001 From: Etienne Dechamps Date: Sun, 25 Feb 2024 13:50:17 +0000 Subject: [PATCH] Add support for Prometheus/OpenMetrics to One V9 This commit adds a new feature to the One V9 (ONE_I-9PSL) firmware: support for exposing metrics to Prometheus (or any other ingestor compatible with the OpenMetrics format). With this change, the AirGradient device will make metrics available on the standard HTTP /metrics endpoint, out-of-the-box, with no need to do anything else. All the user has to do is add their device address as a target to their scrape config on their Prometheus server. For more information on Prometheus and OpenMetrics, see: - https://prometheus.io/docs/instrumenting/exposition_formats/ - https://openmetrics.io/ - https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md This obsoletes projects such as: - https://github.com/geerlingguy/airgradient-prometheus/tree/ebfa8d0ac6d8c47649e4642b27e90b98557fb8f7/AirGradient-DIY - https://forum.airgradient.com/t/prometheus-integration/1504 --- examples/ONE_I-9PSL/ONE_I-9PSL.ino | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/examples/ONE_I-9PSL/ONE_I-9PSL.ino b/examples/ONE_I-9PSL/ONE_I-9PSL.ino index 10b10bd..1e3a609 100644 --- a/examples/ONE_I-9PSL/ONE_I-9PSL.ino +++ b/examples/ONE_I-9PSL/ONE_I-9PSL.ino @@ -892,6 +892,95 @@ void webServerMeasureCurrentGet(void) { webServer.send(200, "application/json", getServerSyncData(true)); } +/** + * Sends metrics in Prometheus/OpenMetrics format to the currently connected + * webServer client. + * + * For background, see: https://prometheus.io/docs/instrumenting/exposition_formats/ + */ +void webServerMetricsGet(void) { + String response; + String current_metric_name; + const auto add_metric = [&](const String &name, const String &help, const String &type, const String &unit = "") + { + current_metric_name = "airgradient_" + name; + if (!unit.isEmpty()) + current_metric_name += "_" + unit; + response += "# HELP " + current_metric_name + " " + help + "\n"; + response += "# TYPE " + current_metric_name + " " + type + "\n"; + if (!unit.isEmpty()) + response += "# UNIT " + current_metric_name + " " + unit + "\n"; + }; + const auto add_metric_point = [&](const String &labels, const String &value) { + response += current_metric_name + "{" + labels + "} " + value + "\n"; + }; + + add_metric("info", "AirGradient device information", "info"); + add_metric_point("airgradient_serial_number=\"" + getDevId() + "\",airgradient_device_type=\"ONE_I-9PSL\",airgradient_library_version=\"" + ag.getVersion() + "\"", "1"); + + add_metric("config_ok", "1 if the AirGradient device was able to successfully fetch its configuration from the server", "gauge"); + add_metric_point("", agServer.isConfigFailed() ? "0" : "1"); + + add_metric("post_ok", "1 if the AirGradient device was able to successfully send to the server", "gauge"); + add_metric_point("", agServer.isServerFailed() ? "0" : "1"); + + add_metric("wifi_rssi", "WiFi signal strength from the AirGradient device perspective, in dBm", "gauge", "dbm"); + add_metric_point("", String(WiFi.RSSI())); + + if (hasSensorS8 && co2Ppm >= 0) { + add_metric("co2", "Carbon dioxide concentration as measured by the AirGradient S8 sensor, in parts per million", "gauge", "ppm"); + add_metric_point("", String(co2Ppm)); + } + + if (hasSensorPMS) { + if (pm01 >= 0) { + add_metric("pm1", "PM1.0 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter", "gauge", "ugm3"); + add_metric_point("", String(pm01)); + } + if (pm25 >= 0) { + add_metric("pm2d5", "PM2.5 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter", "gauge", "ugm3"); + add_metric_point("", String(pm25)); + } + if (pm10 >= 0) { + add_metric("pm10", "PM10 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter", "gauge", "ugm3"); + add_metric_point("", String(pm10)); + } + if (pm03PCount >= 0) { + add_metric("pm0d3", "PM0.3 concentration as measured by the AirGradient PMS sensor, in number of particules per 100 milliliters", "gauge", "p100ml"); + add_metric_point("", String(pm03PCount)); + } + } + + if (hasSensorSGP) { + if (tvocIndex >= 0) { + add_metric("tvoc_index", "The processed Total Volatile Organic Compounds (TVOC) index as measured by the AirGradient SGP sensor", "gauge"); + add_metric_point("", String(tvocIndex)); + } + if (tvocRawIndex >= 0) { + add_metric("tvoc_raw_index", "The raw input value to the Total Volatile Organic Compounds (TVOC) index as measured by the AirGradient SGP sensor", "gauge"); + add_metric_point("", String(tvocRawIndex)); + } + if (noxIndex >= 0) { + add_metric("nox_index", "The processed Nitrous Oxide (NOx) index as measured by the AirGradient SGP sensor", "gauge"); + add_metric_point("", String(noxIndex)); + } + } + + if (hasSensorSHT) { + if (temp > -1001) { + add_metric("temperature", "The ambient temperature as measured by the AirGradient SHT sensor, in degrees Celsius", "gauge", "degc"); + add_metric_point("", String(temp)); + } + if (hum >= 0) { + add_metric("humidity", "The relative humidity as measured by the AirGradient SHT sensor", "gauge", "percent"); + add_metric_point("", String(hum)); + } + } + + response += "# EOF\n"; + webServer.send(200, "application/openmetrics-text; version=1.0.0; charset=utf-8", response); +} + void webServerHandler(void *param) { for (;;) { webServer.handleClient(); @@ -906,6 +995,8 @@ static void webServerInit(void) { } webServer.on("/measures/current", HTTP_GET, webServerMeasureCurrentGet); + // Make it possible to query this device from Prometheus/OpenMetrics. + webServer.on("/metrics", HTTP_GET, webServerMetricsGet); webServer.begin(); MDNS.addService("http", "tcp", 80);