From 0dbbe819c7c160cd9c13fe470df6faa7cab34646 Mon Sep 17 00:00:00 2001
From: "Christian W. Zuckschwerdt" <zany@triq.net>
Date: Sat, 16 Mar 2024 12:14:00 +0100
Subject: [PATCH] Add OpenMetrics/Prometheus API (#2863)

---
 include/rtl_433.h | 19 +++++++++----
 src/http_server.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++
 src/r_api.c       |  5 ++--
 src/rtl_433.c     | 16 ++++++++---
 4 files changed, 101 insertions(+), 11 deletions(-)

diff --git a/include/rtl_433.h b/include/rtl_433.h
index 1d9eb66b..136ceb5b 100644
--- a/include/rtl_433.h
+++ b/include/rtl_433.h
@@ -116,11 +116,20 @@ typedef struct r_cfg {
     char const *sr_filename;
     int sr_execopen;
     int watchdog; ///< SDR acquire stall watchdog
-    /* stats*/
-    time_t frames_since; ///< stats start time
-    unsigned frames_count; ///< stats counter for interval
-    unsigned frames_fsk; ///< stats counter for interval
-    unsigned frames_events; ///< stats counter for interval
+    /* global stats */
+    time_t running_since;           ///< program start time statistic
+    unsigned total_frames_count;    ///< total frames recieved statistic
+    unsigned total_frames_squelch;  ///< total frames with noise only statistic
+    unsigned total_frames_ook;      ///< total frames with ook demod statistic
+    unsigned total_frames_fsk;      ///< total frames with fsk demod statistic
+    unsigned total_frames_events;   ///< total frames with decoder events statistic
+    /* sdr stats */
+    time_t sdr_since; ///< time of last SDR connect statistic
+    /* per report interval stats */
+    time_t frames_since;    ///< time at start of report interval statistic
+    unsigned frames_ook;    ///< counter of ook demods for report interval statistic
+    unsigned frames_fsk;    ///< counter of fsk demods for report interval statistic
+    unsigned frames_events; ///< counter of decoder events for report interval statistic
     struct mg_mgr *mgr;
 } r_cfg_t;
 
diff --git a/src/http_server.c b/src/http_server.c
index 0911551e..2de06b1f 100644
--- a/src/http_server.c
+++ b/src/http_server.c
@@ -733,6 +733,75 @@ static void handle_redirect(struct mg_connection *nc, struct http_message *hm)
             "\r\n\r\n");
 }
 
+static void handle_openmetrics(struct mg_connection *nc, struct http_message *hm)
+{
+    if (mg_vcmp(&hm->method, "GET") != 0) {
+        mg_http_send_error(nc, 405, NULL); // 405 Method Not Allowed
+        return;
+    }
+
+    struct http_server_context *ctx = nc->user_data;
+    r_cfg_t *cfg = ctx->cfg;
+
+    time_t now;
+    time(&now);
+
+    char buf[2000];
+    int len = snprintf(buf, sizeof(buf),
+            "# TYPE uptime_seconds counter\n"
+            "# UNIT uptime_seconds seconds\n"
+            "# HELP uptime_seconds Program uptime.\n"
+            "uptime_seconds_total %.1f\n"
+            "uptime_seconds_created %.1f\n"
+            "# TYPE decoder_enabled gauge\n"
+            "# HELP decoder_enabled Number of enabled decoders.\n"
+            "decoder_enabled %u\n"
+            "# TYPE input_uptime_seconds counter\n"
+            "# UNIT input_uptime_seconds seconds\n"
+            "# HELP input_uptime_seconds SDR Receiver uptime.\n"
+            "input_uptime_seconds_total %.1f\n"
+            "input_uptime_seconds_created %.1f\n"
+            "# TYPE input_count_frames counter\n"
+            "# UNIT input_count_frames frames\n"
+            "# HELP input_count_frames Number of SDR frames received.\n"
+            "input_count_frames_total %u\n"
+            "# TYPE input_squelch_frames counter\n"
+            "# UNIT input_squelch_frames frames\n"
+            "# HELP input_squelch_frames Number of SDR frames skipped by squelch.\n"
+            "input_squelch_frames_total %u\n"
+            "# TYPE input_ook_frames counter\n"
+            "# UNIT input_ook_frames frames\n"
+            "# HELP input_ook_frames Number of SDR frames with OOK demodulation.\n"
+            "input_ook_frames_total %u\n"
+            "# TYPE input_fsk_frames counter\n"
+            "# UNIT input_fsk_frames frames\n"
+            "# HELP input_fsk_frames Number of SDR frames with FSK demodulation.\n"
+            "input_fsk_frames_total %u\n"
+            "# TYPE input_event_frames counter\n"
+            "# UNIT input_event_frames frames\n"
+            "# HELP input_event_frames Number of SDR frames with decode events.\n"
+            "input_event_frames_total %u\n"
+            "# EOF\n",
+            (float)(now - cfg->running_since), // uptime_seconds_total,
+            (float)cfg->running_since,         // uptime_seconds_created,
+            (unsigned)cfg->demod->r_devs.len,  // decoder_enabled,
+            (float)(now - cfg->sdr_since),     // input_uptime_seconds_total,
+            (float)cfg->sdr_since,             // input_uptime_seconds_created,
+            cfg->total_frames_count,           // input_count_frames_total,
+            cfg->total_frames_squelch,         // input_squelch_frames_total,
+            cfg->total_frames_ook,             // input_ook_frames_total,
+            cfg->total_frames_fsk,             // input_fsk_frames_total,
+            cfg->total_frames_events);         // input_event_frames_total,
+
+    mg_printf(nc,
+            "HTTP/1.1 200 OK\r\n"
+            "Content-Length: %u\r\n"
+            "\r\n",
+            len);
+    mg_send(nc, buf, (size_t)len);
+    nc->flags |= MG_F_SEND_AND_CLOSE;
+}
+
 // reply to ws command
 static void rpc_response_ws(rpc_t *rpc, int ret_code, char const *message, int arg)
 {
@@ -1039,6 +1108,9 @@ static void ev_handler(struct mg_connection *nc, int ev, void *ev_data)
         else if (mg_vcmp(&hm->uri, "/stream") == 0) {
             handle_json_stream(nc, hm);
         }
+        else if (mg_vcmp(&hm->uri, "/metrics") == 0) {
+            handle_openmetrics(nc, hm);
+        }
         else if (mg_vcmp(&hm->uri, "/api") == 0) {
             //handle_api_query(nc, hm);
         }
diff --git a/src/r_api.c b/src/r_api.c
index 01a5ac2f..6ad85ece 100644
--- a/src/r_api.c
+++ b/src/r_api.c
@@ -187,6 +187,7 @@ void r_init_cfg(r_cfg_t *cfg)
     // initialize tables
     baseband_init();
 
+    time(&cfg->running_since);
     time(&cfg->frames_since);
     get_time_now(&cfg->demod->now);
 
@@ -938,7 +939,7 @@ data_t *create_report_data(r_cfg_t *cfg, int level)
     }
 
     data = data_make(
-            "count",            "", DATA_INT, cfg->frames_count,
+            "count",            "", DATA_INT, cfg->frames_ook,
             "fsk",              "", DATA_INT, cfg->frames_fsk,
             "events",           "", DATA_INT, cfg->frames_events,
             NULL);
@@ -962,7 +963,7 @@ void flush_report_data(r_cfg_t *cfg)
     list_t *r_devs = &cfg->demod->r_devs;
 
     time(&cfg->frames_since);
-    cfg->frames_count = 0;
+    cfg->frames_ook = 0;
     cfg->frames_fsk = 0;
     cfg->frames_events = 0;
 
diff --git a/src/rtl_433.c b/src/rtl_433.c
index 2bb3f6f4..8f11ca79 100644
--- a/src/rtl_433.c
+++ b/src/rtl_433.c
@@ -460,7 +460,9 @@ static void sdr_callback(unsigned char *iq_buf, uint32_t len, void *ctx)
     int noise_only = avg_db < demod->noise_level + 3.0f; // or demod->min_level_auto?
     // always process frames if loader, dumper, or analyzers are in use, otherwise skip silent frames
     int process_frame = demod->squelch_offset <= 0 || !noise_only || demod->load_info.format || demod->analyze_pulses || demod->dumper.len || demod->samp_grab;
+    cfg->total_frames_count += 1;
     if (noise_only) {
+        cfg->total_frames_squelch += 1;
         demod->noise_level = (demod->noise_level * 7 + avg_db) / 8; // fast fall over 8 frames
         // If auto_level and noise level well below min_level and significant change in noise level
         if (demod->auto_level > 0 && demod->noise_level < demod->min_level - 3.0f
@@ -479,8 +481,9 @@ static void sdr_callback(unsigned char *iq_buf, uint32_t len, void *ctx)
                 noise_only ? "noise" : "signal", avg_db, demod->noise_level);
     }
 
-    if (process_frame)
-    baseband_low_pass_filter(demod->buf.temp, demod->am_buf, n_samples, &demod->lowpass_filter_state);
+    if (process_frame) {
+        baseband_low_pass_filter(demod->buf.temp, demod->am_buf, n_samples, &demod->lowpass_filter_state);
+    }
 
     // FM demodulation
     // Select the correct fsk pulse detector
@@ -539,7 +542,9 @@ static void sdr_callback(unsigned char *iq_buf, uint32_t len, void *ctx)
                 if (demod->analyze_pulses) fprintf(stderr, "Detected OOK package\t%s\n", time_pos_str(cfg, demod->pulse_data.start_ago, time_str));
 
                 p_events += run_ook_demods(&demod->r_devs, &demod->pulse_data);
-                cfg->frames_count++;
+                cfg->total_frames_ook += 1;
+                cfg->total_frames_events += p_events > 0;
+                cfg->frames_ook +=1;
                 cfg->frames_events += p_events > 0;
 
                 for (void **iter = demod->dumper.elems; iter && *iter; ++iter) {
@@ -564,7 +569,9 @@ static void sdr_callback(unsigned char *iq_buf, uint32_t len, void *ctx)
                 if (demod->analyze_pulses) fprintf(stderr, "Detected FSK package\t%s\n", time_pos_str(cfg, demod->fsk_pulse_data.start_ago, time_str));
 
                 p_events += run_fsk_demods(&demod->r_devs, &demod->fsk_pulse_data);
-                cfg->frames_fsk++;
+                cfg->total_frames_fsk +=1;
+                cfg->total_frames_events += p_events > 0;
+                cfg->frames_fsk += 1;
                 cfg->frames_events += p_events > 0;
 
                 for (void **iter = demod->dumper.elems; iter && *iter; ++iter) {
@@ -1517,6 +1524,7 @@ static void timer_handler(struct mg_connection *nc, int ev, void *ev_data)
             if (cfg->dev_state == DEVICE_STATE_STARTING
                     || cfg->dev_state == DEVICE_STATE_GRACE) {
                 cfg->dev_state = DEVICE_STATE_STARTED;
+                time(&cfg->sdr_since);
             }
             cfg->watchdog = 0;
             break;