]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
stats: stats-service - Add support for OpenMetrics/Prometheus.
authorStephan Bosch <stephan.bosch@open-xchange.com>
Sun, 17 Nov 2019 09:55:49 +0000 (10:55 +0100)
committermartti.rannanjarvi <martti.rannanjarvi@open-xchange.com>
Sat, 18 Apr 2020 14:55:11 +0000 (14:55 +0000)
src/stats/Makefile.am
src/stats/stats-service-openmetrics.c [new file with mode: 0644]
src/stats/stats-service-private.h
src/stats/stats-service.c

index d1d4ad6a26f4399cb83eca3d1594f503304d01c0..0d6f5982864a609650429c050c605ee1efadbf65 100644 (file)
@@ -25,7 +25,8 @@ stats_DEPENDENCIES = \
        $(DOVECOT_SSL_LIBS) \
        $(LIBDOVECOT_DEPS)
 
-stats_services =
+stats_services = \
+       stats-service-openmetrics.c
 
 stats_SOURCES = \
        main.c
diff --git a/src/stats/stats-service-openmetrics.c b/src/stats/stats-service-openmetrics.c
new file mode 100644 (file)
index 0000000..5cbac30
--- /dev/null
@@ -0,0 +1,303 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "dovecot-version.h"
+#include "str.h"
+#include "array.h"
+#include "ioloop.h"
+#include "stats-dist.h"
+#include "http-server.h"
+#include "client-http.h"
+#include "stats-settings.h"
+#include "stats-metrics.h"
+#include "stats-service-private.h"
+
+#define OPENMETRICS_CONTENT_VERSION "0.0.4"
+
+#ifdef DOVECOT_REVISION
+#define OPENMETRICS_BUILD_INFO \
+       "version=\""DOVECOT_VERSION"\"," \
+       "revision=\""DOVECOT_REVISION"\""
+#else
+#define OPENMETRICS_BUILD_INFO \
+       "version=\""DOVECOT_VERSION"\""
+#endif
+
+/* https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels:
+
+   Every time series is uniquely identified by its metric name and optional
+   key-value pairs called labels.
+
+   The metric name specifies the general feature of a system that is measured
+   (e.g. http_requests_total - the total number of HTTP requests received). It
+   may contain ASCII letters and digits, as well as underscores and colons. It
+   must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*.
+ */
+
+static bool openmetrics_check_name(const char *name)
+{
+       const unsigned char *p, *pend;
+
+       p = (const unsigned char *)name;
+       pend = p + strlen(name);
+
+       if (p == pend)
+               return FALSE;
+
+       if (!(*p >= 'a' && *p <= 'z') && !(*p >= 'A' && *p <= 'Z') &&
+           *p != '_' && *p != ':')
+               return FALSE;
+       p++;
+       while (p < pend) {
+               if (!(*p >= 'a' && *p <= 'z') && !(*p >= 'A' && *p <= 'Z') &&
+                   !(*p >= '0' && *p <= '9') && *p != '_' && *p != ':')
+                       return FALSE;
+               p++;
+       }
+       return TRUE;
+}
+
+static bool openmetrics_check_metric(const struct metric *metric)
+{
+       const char *const *filters;
+       unsigned int i, count;
+
+       if (!openmetrics_check_name(metric->name))
+               return FALSE;
+
+       if (!array_is_created(&metric->set->filter))
+               return TRUE;
+
+       filters = array_get(&metric->set->filter, &count);
+       if (count == 0)
+               return TRUE;
+       i_assert(count % 2 == 0);
+
+       count /= 2;
+       for (i = 1; i < count; i++) {
+               if (!openmetrics_check_name(filters[i * 2]))
+                       return FALSE;
+       }
+       return TRUE;
+}
+
+/* https://prometheus.io/docs/prometheus/latest/querying/basics/#literals:
+
+   PromQL follows the same escaping rules as Go. In single or double quotes a
+   backslash begins an escape sequence, which may be followed by a, b, f, n, r,
+   t, v or \. Specific characters can be provided using octal (\nnn) or
+   hexadecimal (\xnn, \unnnn and \Unnnnnnnn).
+
+   https://golang.org/ref/spec#String_literals:
+
+   Interpreted string literals are character sequences between double quotes, as
+   in "bar". Within the quotes, any character may appear except newline and
+   unescaped double quote. The text between the quotes forms the value of the
+   literal, with backslash escapes interpreted as they are in rune literals
+   (except that \' is illegal and \" is legal), with the same restrictions.
+
+   -> Cannot use strescape.h, since \' is illegal.
+ */
+static void openmetrics_escape_string(string_t *dest, const char *value)
+{
+       const unsigned char *pstart, *p, *pend;
+
+       pstart = p = (const unsigned char *)value;
+       pend = pstart + strlen(value);
+
+       /* See if we need to quote it */
+       for (; p < pend; p++) {
+               if (*p == '\n' || *p == '"')
+                       break;
+       }
+
+       /* Quote */
+       str_append_data(dest, pstart, (size_t)(p - pstart));
+
+       for (; p < pend; p++) {
+               if (*p == '\n' || *p == '"')
+                       str_append_c(dest, '\\');
+               str_append_data(dest, p, 1);
+       }
+}
+
+static void
+openmetrics_export_metric_labels(string_t *out, const struct metric *metric)
+{
+       const char *const *filters;
+       unsigned int i, count;
+
+       if (!array_is_created(&metric->set->filter))
+               return;
+
+       filters = array_get(&metric->set->filter, &count);
+       if (count == 0)
+               return;
+       i_assert(count % 2 == 0);
+
+       str_append_c(out, '{');
+
+       str_append(out, filters[0]);
+       str_append(out, "=\"");
+       openmetrics_escape_string(out, filters[1]);
+       str_append_c(out, '"');
+
+       count /= 2;
+       for (i = 1; i < count; i++) {
+               str_append_c(out, ',');
+               str_append(out, filters[i * 2]);
+               str_append(out, "=\"");
+               openmetrics_escape_string(out, filters[i * 2 + 1]);
+               str_append_c(out, '"');
+       }
+
+       str_append_c(out, '}');
+}
+
+static void
+openmetrics_export_metric(string_t *out, const struct metric *metric,
+                        int64_t timestamp)
+{
+       if (!openmetrics_check_metric(metric))
+               return;
+       
+       /* Description */
+       str_append(out, "# HELP dovecot_");
+       str_append(out, metric->name);
+       str_append(out, "_count Total number");
+       if (*metric->set->description != '\0') {
+               str_append(out, " of ");
+               str_append(out, metric->set->description);
+       }
+       str_append_c(out, '\n');
+       /* Type */
+       str_append(out, "# TYPE dovecot_");
+       str_append(out, metric->name);
+       str_append(out, "_count counter\n");
+       /* Metric name */
+       str_append(out, "dovecot_");
+       str_append(out, metric->name);
+       str_append(out, "_count");
+       /* Labels */
+       openmetrics_export_metric_labels(out, metric);
+       /* Value */
+       str_printfa(out, " %u %"PRId64"\n\n",
+                   stats_dist_get_count(metric->duration_stats), timestamp);
+
+       /* Description */
+       str_append(out, "# HELP dovecot_");
+       str_append(out, metric->name);
+       str_append(out, "_duration_usecs_sum Duration");
+       if (*metric->set->description != '\0') {
+               str_append(out, " of ");
+               str_append(out, metric->set->description);
+       }
+       str_append_c(out, '\n');
+       /* Type */
+       str_append(out, "# TYPE dovecot_");
+       str_append(out, metric->name);
+       str_append(out, "_duration_usecs_sum counter\n");
+       /* Metric name*/
+       str_append(out, "dovecot_");
+       str_append(out, metric->name);
+       str_append(out, "_duration_usecs_sum");
+       /* Labels */
+       openmetrics_export_metric_labels(out, metric);
+       /* Value */
+       str_printfa(out, " %"PRIu64" %"PRId64"\n",
+                   stats_dist_get_sum(metric->duration_stats),
+                   timestamp);
+}
+
+static void
+openmetrics_export(struct http_server_response *resp)
+{
+       struct stats_metrics_iter *iter;
+       const struct metric *metric;
+       string_t *out = t_str_new(2048);
+       int64_t timestamp;
+
+       i_assert(ioloop_timeval.tv_usec < 1000000);
+       timestamp = ((int64_t)ioloop_timeval.tv_sec * 1000 +
+                    (int64_t)ioloop_timeval.tv_usec / 1000);
+
+       i_assert(stats_startup_time <= ioloop_time);
+       str_append(out, "# HELP dovecot_stats_uptime_seconds "
+                       "Dovecot stats service uptime\n");
+       str_append(out, "# TYPE dovecot_stats_uptime_seconds counter\n");
+       str_printfa(out, "dovecot_stats_uptime_seconds %"PRId64" %"PRId64"\n\n",
+                   (int64_t)(ioloop_time - stats_startup_time), timestamp);
+
+       str_append(out, "# HELP dovecot_build_info "
+                       "Dovecot build information\n");
+       str_append(out, "# TYPE dovecot_build_info untyped\n");
+       str_printfa(out, "dovecot_build_info{"OPENMETRICS_BUILD_INFO"} "
+                        "1 %"PRId64"\n", timestamp);
+
+       iter = stats_metrics_iterate_init(stats_metrics);
+       while ((metric = stats_metrics_iterate(iter)) != NULL) {
+               /* Empty line */
+               str_append_c(out, '\n');
+
+               openmetrics_export_metric(out, metric, timestamp);
+       }
+       stats_metrics_iterate_deinit(&iter);
+
+       http_server_response_set_payload_data(
+               resp, str_data(out), str_len(out));
+}
+
+static void
+stats_service_openmetrics_request(void *context ATTR_UNUSED,
+                                struct http_server_request *req,
+                                const char *sub_path)
+{
+       const struct http_request *hreq = http_server_request_get(req);
+       struct http_server_response *resp;
+
+       if (strcmp(hreq->method, "OPTIONS") == 0) {
+               resp = http_server_response_create(req, 200, "OK");
+               http_server_response_add_header(resp, "Allow", "GET");
+               http_server_response_submit(resp);
+               return;
+       }
+       if (strcmp(hreq->method, "GET") != 0) {
+               http_server_request_fail_bad_method(req, "GET");
+               return;
+       }
+       if (*sub_path != '\0') {
+               http_server_request_fail(req, 404, "Not Found");
+               return;
+       }
+
+       resp = http_server_response_create(req, 200, "OK");
+       http_server_response_add_header(
+               resp, "Content-Type", "text/plain; "
+                                     "version="OPENMETRICS_CONTENT_VERSION"; "
+                                     "charset=utf-8");
+
+       openmetrics_export(resp);
+
+       http_server_response_submit(resp);
+}
+
+void stats_service_openmetrics_init(void)
+{
+       struct stats_metrics_iter *iter;
+       const struct metric *metric;
+
+       iter = stats_metrics_iterate_init(stats_metrics);
+       while ((metric = stats_metrics_iterate(iter)) != NULL) {
+               if (!openmetrics_check_metric(metric)) {
+                       i_warning(
+                               "stats: openmetrics: "
+                               "Metric `%s' is not valid for OpenMetrics"
+                               "(invalid metric or label name; skipped)",
+                               metric->name);
+               }
+       }
+       stats_metrics_iterate_deinit(&iter);
+
+       stats_http_resource_add("/metrics", "OpenMetrics",
+                               stats_service_openmetrics_request, NULL);
+}
index f6842106c870d53ceda7af574cb1a51837e07b73..5f5ab204a674db8b79fd31e62f4b79786b515a87 100644 (file)
@@ -3,4 +3,6 @@
 
 #include "stats-service.h"
 
+void stats_service_openmetrics_init(void);
+
 #endif
index a26d697e9d9f42122c6a2c7ba24236d19cfb210f..fa678647ee639e8c2b2046b86b8d0e1b0c5db61a 100644 (file)
@@ -6,7 +6,7 @@
 
 void stats_services_init(void)
 {
-       /* Nothing yet */
+        stats_service_openmetrics_init();
 }
 
 void stats_services_deinit(void)