MINOR: contrib/prometheus-exporter: filter exported metrics by scope

Now, the prometheus exporter parses the HTTP query-string to filter or to adapt
the exported metrics. In this first version, it is only possible select the
scopes of metrics to export. To do so, one or more parameters with "scope" as
name must be passed in the query-string, with one of those values: global,
frontend, backend, server or '*' (means all). A scope parameter with no value
means to filter out all scopes (nothing is returned). The scope parameters are
parsed in their appearance order in the query-string. So an empty scope will
reset all scopes already parsed. But it can be overridden by following scope
parameters in the query-string. By default everything is exported.

The filtering can also be done on prometheus scraping configuration, but general
aim is to optimise the source of data to improve load and scraping time. This is
particularly true for huge configuration with thousands of backends and servers.
Also note that this configuration was possible on the previous official haproxy
exporter but with even more parameters to select the needed metrics. Here we
thought it was sufficient to simply avoid a given type of metric. However, more
filters are still possible.

Thanks to William Dauchy. This patch is based on his work.

(cherry picked from commit 78407ce156b9dd317ae9f4becccd4afa3d5f8c56)
Signed-off-by: Christopher Faulet <cfaulet@haproxy.com>
diff --git a/contrib/prometheus-exporter/service-prometheus.c b/contrib/prometheus-exporter/service-prometheus.c
index 67e71ec..ef25434 100644
--- a/contrib/prometheus-exporter/service-prometheus.c
+++ b/contrib/prometheus-exporter/service-prometheus.c
@@ -64,6 +64,12 @@
 #define PROMEX_FL_METRIC_HDR    0x00000001
 #define PROMEX_FL_INFO_METRIC   0x00000002
 #define PROMEX_FL_STATS_METRIC  0x00000004
+#define PROMEX_FL_SCOPE_GLOBAL  0x00000008
+#define PROMEX_FL_SCOPE_FRONT   0x00000010
+#define PROMEX_FL_SCOPE_BACK    0x00000020
+#define PROMEX_FL_SCOPE_SERVER  0x00000040
+
+#define PROMEX_FL_SCOPE_ALL (PROMEX_FL_SCOPE_GLOBAL|PROMEX_FL_SCOPE_FRONT|PROMEX_FL_SCOPE_BACK|PROMEX_FL_SCOPE_SERVER)
 
 /* The max length for metrics name. It is a hard limit but it should be
  * enougth.
@@ -2102,72 +2108,81 @@
 static int promex_dump_metrics(struct appctx *appctx, struct stream_interface *si, struct htx *htx)
 {
 	int ret;
+	int flags = appctx->ctx.stats.flags;
 
 	switch (appctx->st1) {
 		case PROMEX_DUMPER_INIT:
 			appctx->ctx.stats.px = NULL;
 			appctx->ctx.stats.sv = NULL;
-			appctx->ctx.stats.flags = (PROMEX_FL_METRIC_HDR|PROMEX_FL_INFO_METRIC);
+			appctx->ctx.stats.flags = (flags|PROMEX_FL_METRIC_HDR|PROMEX_FL_INFO_METRIC);
 			appctx->st2 = promex_global_metrics[INF_NAME];
 			appctx->st1 = PROMEX_DUMPER_GLOBAL;
 			/* fall through */
 
 		case PROMEX_DUMPER_GLOBAL:
-			ret = promex_dump_global_metrics(appctx, htx);
-			if (ret <= 0) {
-				if (ret == -1)
-					goto error;
-				goto full;
+			if (appctx->ctx.stats.flags & PROMEX_FL_SCOPE_GLOBAL) {
+				ret = promex_dump_global_metrics(appctx, htx);
+				if (ret <= 0) {
+					if (ret == -1)
+						goto error;
+					goto full;
+				}
 			}
 
 			appctx->ctx.stats.px = proxies_list;
 			appctx->ctx.stats.sv = NULL;
-			appctx->ctx.stats.flags = (PROMEX_FL_METRIC_HDR|PROMEX_FL_STATS_METRIC);
+			appctx->ctx.stats.flags = (flags|PROMEX_FL_METRIC_HDR|PROMEX_FL_STATS_METRIC);
 			appctx->st2 = promex_front_metrics[ST_F_PXNAME];
 			appctx->st1 = PROMEX_DUMPER_FRONT;
 			/* fall through */
 
 		case PROMEX_DUMPER_FRONT:
-			ret = promex_dump_front_metrics(appctx, htx);
-			if (ret <= 0) {
-				if (ret == -1)
-					goto error;
-				goto full;
+			if (appctx->ctx.stats.flags & PROMEX_FL_SCOPE_FRONT) {
+				ret = promex_dump_front_metrics(appctx, htx);
+				if (ret <= 0) {
+					if (ret == -1)
+						goto error;
+					goto full;
+				}
 			}
 
 			appctx->ctx.stats.px = proxies_list;
 			appctx->ctx.stats.sv = NULL;
-			appctx->ctx.stats.flags = (PROMEX_FL_METRIC_HDR|PROMEX_FL_STATS_METRIC);
+			appctx->ctx.stats.flags = (flags|PROMEX_FL_METRIC_HDR|PROMEX_FL_STATS_METRIC);
 			appctx->st2 = promex_back_metrics[ST_F_PXNAME];
 			appctx->st1 = PROMEX_DUMPER_BACK;
 			/* fall through */
 
 		case PROMEX_DUMPER_BACK:
-			ret = promex_dump_back_metrics(appctx, htx);
-			if (ret <= 0) {
-				if (ret == -1)
-					goto error;
-				goto full;
+			if (appctx->ctx.stats.flags & PROMEX_FL_SCOPE_BACK) {
+				ret = promex_dump_back_metrics(appctx, htx);
+				if (ret <= 0) {
+					if (ret == -1)
+						goto error;
+					goto full;
+				}
 			}
 
 			appctx->ctx.stats.px = proxies_list;
 			appctx->ctx.stats.sv = (appctx->ctx.stats.px ? appctx->ctx.stats.px->srv : NULL);
-			appctx->ctx.stats.flags = (PROMEX_FL_METRIC_HDR|PROMEX_FL_STATS_METRIC);
+			appctx->ctx.stats.flags = (flags|PROMEX_FL_METRIC_HDR|PROMEX_FL_STATS_METRIC);
 			appctx->st2 = promex_srv_metrics[ST_F_PXNAME];
 			appctx->st1 = PROMEX_DUMPER_SRV;
 			/* fall through */
 
 		case PROMEX_DUMPER_SRV:
-			ret = promex_dump_srv_metrics(appctx, htx);
-			if (ret <= 0) {
-				if (ret == -1)
-					goto error;
-				goto full;
+			if (appctx->ctx.stats.flags & PROMEX_FL_SCOPE_SERVER) {
+				ret = promex_dump_srv_metrics(appctx, htx);
+				if (ret <= 0) {
+					if (ret == -1)
+						goto error;
+					goto full;
+				}
 			}
 
 			appctx->ctx.stats.px = NULL;
 			appctx->ctx.stats.sv = NULL;
-			appctx->ctx.stats.flags = 0;
+			appctx->ctx.stats.flags = flags;
 			appctx->st2 = 0;
 			appctx->st1 = PROMEX_DUMPER_DONE;
 			/* fall through */
@@ -2192,6 +2207,92 @@
 	return -1;
 }
 
+/* Parse the query stirng of request URI to filter the metrics. It returns 1 on
+ * success and -1 on error. */
+static int promex_parse_uri(struct appctx *appctx, struct stream_interface *si)
+{
+	struct channel *req = si_oc(si);
+	struct channel *res = si_ic(si);
+	struct htx *req_htx, *res_htx;
+	struct htx_sl *sl;
+	const char *p, *end;
+	struct buffer *err;
+	int default_scopes = PROMEX_FL_SCOPE_ALL;
+	int len;
+
+	/* Get the query-string */
+	req_htx = htxbuf(&req->buf);
+	sl = http_get_stline(req_htx);
+	if (!sl)
+		goto error;
+	p = http_find_param_list(HTX_SL_REQ_UPTR(sl), HTX_SL_REQ_ULEN(sl), '?');
+	if (!p)
+		goto end;
+	end = HTX_SL_REQ_UPTR(sl) + HTX_SL_REQ_ULEN(sl);
+	len = end-p;
+
+	/* Decode the query-string */
+	chunk_reset(&trash);
+	memcpy(trash.area, p, len);
+	trash.area[len] = 0;
+	len = url_decode(trash.area);
+	if (len == -1)
+		goto error;
+	p = trash.area;
+	end = p + len;
+
+	/* Parse the query-string */
+	while (p < end) {
+		if (*p == '&')
+			++p;
+		else if (*p == 's' && (end-p) >= 6 && !memcmp(p, "scope=", 6)) {
+			default_scopes = 0; /* at least a scope defined, unset default scopes */
+			p += 6;             /* now p point on the parameter value */
+			len = 0;            /* len is the value length */
+			while ((p+len) < end && *(p+len) != '&')
+				++len;
+
+			if (len == 0)
+				appctx->ctx.stats.flags &= ~PROMEX_FL_SCOPE_ALL;
+			else if (len == 1 && *p == '*')
+				appctx->ctx.stats.flags |= PROMEX_FL_SCOPE_ALL;
+			else if (len == 6) {
+				if (!memcmp(p, "global", len))
+					appctx->ctx.stats.flags |= PROMEX_FL_SCOPE_GLOBAL;
+				else if (!memcmp(p, "server", len))
+					appctx->ctx.stats.flags |= PROMEX_FL_SCOPE_SERVER;
+			}
+			else if (len == 7 && !memcmp(p, "backend", len))
+				appctx->ctx.stats.flags |= PROMEX_FL_SCOPE_BACK;
+			else if (len == 8 && !memcmp(p, "frontend", len))
+				appctx->ctx.stats.flags |= PROMEX_FL_SCOPE_FRONT;
+			else
+				goto error;
+
+			p += len;
+		}
+		else {
+			/* ignore all other params for now */
+			while (p < end && *p != '&')
+				p++;
+		}
+	}
+
+  end:
+	appctx->ctx.stats.flags |= default_scopes;
+	return 1;
+
+  error:
+	err = &http_err_chunks[HTTP_ERR_400];
+	channel_erase(res);
+	res->buf.data = b_data(err);
+	memcpy(res->buf.area, b_head(err), b_data(err));
+	res_htx = htx_from_buf(&res->buf);
+	channel_add_input(res, res_htx->data);
+	appctx->st0 = PROMEX_ST_END;
+	return -1;
+}
+
 /* Send HTTP headers of the response. It returns 1 on success and 0 if <htx> is
  * full. */
 static int promex_send_headers(struct appctx *appctx, struct stream_interface *si, struct htx *htx)
@@ -2252,6 +2353,12 @@
 
 	switch (appctx->st0) {
 		case PROMEX_ST_INIT:
+			ret = promex_parse_uri(appctx, si);
+			if (ret <= 0) {
+				if (ret == -1)
+					goto error;
+				goto out;
+			}
 			appctx->st0 = PROMEX_ST_HEAD;
 			appctx->st1 = PROMEX_DUMPER_INIT;
 			/* fall through */