From: Stephan Bosch Date: Sun, 17 Nov 2019 09:55:49 +0000 (+0100) Subject: stats: stats-service - Add support for OpenMetrics/Prometheus. X-Git-Tag: 2.3.11.2~312 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d22a32424b7646e8a7a42987f8e58958323031ee;p=thirdparty%2Fdovecot%2Fcore.git stats: stats-service - Add support for OpenMetrics/Prometheus. --- diff --git a/src/stats/Makefile.am b/src/stats/Makefile.am index d1d4ad6a26..0d6f598286 100644 --- a/src/stats/Makefile.am +++ b/src/stats/Makefile.am @@ -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 index 0000000000..5cbac30413 --- /dev/null +++ b/src/stats/stats-service-openmetrics.c @@ -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); +} diff --git a/src/stats/stats-service-private.h b/src/stats/stats-service-private.h index f6842106c8..5f5ab204a6 100644 --- a/src/stats/stats-service-private.h +++ b/src/stats/stats-service-private.h @@ -3,4 +3,6 @@ #include "stats-service.h" +void stats_service_openmetrics_init(void); + #endif diff --git a/src/stats/stats-service.c b/src/stats/stats-service.c index a26d697e9d..fa678647ee 100644 --- a/src/stats/stats-service.c +++ b/src/stats/stats-service.c @@ -6,7 +6,7 @@ void stats_services_init(void) { - /* Nothing yet */ + stats_service_openmetrics_init(); } void stats_services_deinit(void)