]> git.ipfire.org Git - thirdparty/collectd.git/commitdiff
write_prometheus plugin: Replace invalid characters in names.
authorFlorian Forster <octo@collectd.org>
Thu, 21 Dec 2023 10:52:25 +0000 (11:52 +0100)
committerFlorian Forster <octo@collectd.org>
Thu, 21 Dec 2023 11:59:59 +0000 (12:59 +0100)
src/write_prometheus.c
src/write_prometheus_test.c

index 22446f4d69f0ee726de36f9c0a268fe7d14a4fef..c6c11f71d7ab08be07574f183ef5d1715f4103c7 100644 (file)
 #define MHD_RESULT int
 #endif
 
+/* Label names must match the regex `[a-zA-Z_][a-zA-Z0-9_]*`. Label names
+ * beginning with __ are reserved for internal use.
+ *
+ * Source:
+ * https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels */
+#define VALID_LABEL_CHARS                                                      \
+  "abcdefghijklmnopqrstuvwxyz"                                                 \
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"                                                 \
+  "0123456789_"
+
+/* Metric names must match the regex `[a-zA-Z_:][a-zA-Z0-9_:]*` */
+// instrument-name = ALPHA 0*254 ("_" / "." / "-" / "/" / ALPHA / DIGIT)
+#define VALID_NAME_CHARS VALID_LABEL_CHARS ":"
+
+#define RESOURCE_LABEL_PREFIX "resource_"
+
 static c_avl_tree_t *prom_metrics;
 static pthread_mutex_t prom_metrics_lock = PTHREAD_MUTEX_INITIALIZER;
 
@@ -59,49 +75,100 @@ static struct MHD_Daemon *httpd;
 
 static cdtime_t staleness_delta = PROMETHEUS_DEFAULT_STALENESS_DELTA;
 
+static int format_label_set(strbuf_t *buf, label_set_t const *labels,
+                            char const *prefix, bool first_label) {
+  int status = 0;
+  for (size_t i = 0; i < labels->num; i++) {
+    if (!first_label) {
+      status = status || strbuf_print(buf, ",");
+    }
+
+    status =
+        status || strbuf_print_restricted(buf, prefix, VALID_LABEL_CHARS, '_');
+    status = status || strbuf_print_restricted(buf, labels->ptr[i].name,
+                                               VALID_LABEL_CHARS, '_');
+    status = status || strbuf_print(buf, "=\"");
+    status = status || strbuf_print_escaped(buf, labels->ptr[i].value,
+                                            "\\\"\n\r\t", '\\');
+    status = status || strbuf_print(buf, "\"");
+    first_label = false;
+  }
+  return status;
+}
+
+static int format_metric(strbuf_t *buf, metric_t const *m) {
+  if ((buf == NULL) || (m == NULL) || (m->family == NULL)) {
+    return EINVAL;
+  }
+  label_set_t const *resource = &m->family->resource;
+
+  int status =
+      strbuf_print_restricted(buf, m->family->name, VALID_NAME_CHARS, '_');
+  if (resource->num == 0 && m->label.num == 0) {
+    return status;
+  }
+
+  status = status || strbuf_print(buf, "{");
+
+  bool first_label = true;
+  if (resource->num != 0) {
+    status = status || format_label_set(buf, resource, RESOURCE_LABEL_PREFIX,
+                                        first_label);
+    first_label = false;
+  }
+  status = status || format_label_set(buf, &m->label, "", first_label);
+
+  return status || strbuf_print(buf, "}");
+}
+
 /* visible for testing */
 void format_metric_family(strbuf_t *buf, metric_family_t const *prom_fam) {
-    if (prom_fam->metric.num == 0)
-      return;
+  if (prom_fam->metric.num == 0)
+    return;
 
-    char *type = NULL;
-    switch (prom_fam->type) {
-    case METRIC_TYPE_GAUGE:
-      type = "gauge";
-      break;
-    case METRIC_TYPE_COUNTER:
-      type = "counter";
-      break;
-    case METRIC_TYPE_UNTYPED:
-      type = "untyped";
-      break;
-    }
-    if (type == NULL) {
-      return;
-    }
+  char *type = NULL;
+  switch (prom_fam->type) {
+  case METRIC_TYPE_GAUGE:
+    type = "gauge";
+    break;
+  case METRIC_TYPE_COUNTER:
+    type = "counter";
+    break;
+  case METRIC_TYPE_UNTYPED:
+    type = "untyped";
+    break;
+  }
+  if (type == NULL) {
+    return;
+  }
 
-    if (prom_fam->help == NULL)
-      strbuf_printf(buf, "# HELP %s\n", prom_fam->name);
-    else
-      strbuf_printf(buf, "# HELP %s %s\n", prom_fam->name, prom_fam->help);
-    strbuf_printf(buf, "# TYPE %s %s\n", prom_fam->name, type);
+  strbuf_t family_name = STRBUF_CREATE;
+  strbuf_print_restricted(&family_name, prom_fam->name, VALID_NAME_CHARS, '_');
 
-    for (size_t i = 0; i < prom_fam->metric.num; i++) {
-      metric_t *m = &prom_fam->metric.ptr[i];
+  if (prom_fam->help == NULL)
+    strbuf_printf(buf, "# HELP %s\n", family_name.ptr);
+  else
+    strbuf_printf(buf, "# HELP %s %s\n", family_name.ptr, prom_fam->help);
+  strbuf_printf(buf, "# TYPE %s %s\n", family_name.ptr, type);
 
-      metric_identity(buf, m);
+  STRBUF_DESTROY(family_name);
 
-      if (prom_fam->type == METRIC_TYPE_COUNTER)
-        strbuf_printf(buf, " %" PRIu64, m->value.counter);
-      else
-        strbuf_printf(buf, " " GAUGE_FORMAT, m->value.gauge);
+  for (size_t i = 0; i < prom_fam->metric.num; i++) {
+    metric_t *m = &prom_fam->metric.ptr[i];
 
-      if (m->time > 0) {
-        strbuf_printf(buf, " %" PRIi64 "\n", CDTIME_T_TO_MS(m->time));
-      } else {
-        strbuf_printf(buf, "\n");
-      }
+    format_metric(buf, m);
+
+    if (prom_fam->type == METRIC_TYPE_COUNTER)
+      strbuf_printf(buf, " %" PRIu64, m->value.counter);
+    else
+      strbuf_printf(buf, " " GAUGE_FORMAT, m->value.gauge);
+
+    if (m->time > 0) {
+      strbuf_printf(buf, " %" PRIi64 "\n", CDTIME_T_TO_MS(m->time));
+    } else {
+      strbuf_printf(buf, "\n");
     }
+  }
 }
 
 static void format_text(strbuf_t *buf) {
index 2f8d77617e0dfa5eaf64674cf26e7c7cd2d1fab4..957c1b0d6b73090b234146b8363d1dbb9ac2ad2c 100644 (file)
@@ -50,22 +50,23 @@ DEF_TEST(format_metric_family) {
           .name = "metric without labels",
           .fam =
               {
-                  .name = "unittest",
+                  .name = "unit.test",
                   .type = METRIC_TYPE_COUNTER,
                   .metric =
                       {
                           .ptr =
                               &(metric_t){
-                                  .value = (value_t){
-                                    .counter = 42,
-                                  },
+                                  .value =
+                                      (value_t){
+                                          .counter = 42,
+                                      },
                               },
                           .num = 1,
                       },
               },
-          .want = "# HELP unittest\n"
-              "# TYPE unittest counter\n"
-              "unittest 42\n",
+          .want = "# HELP unit_test\n"
+                  "# TYPE unit_test counter\n"
+                  "unit_test 42\n",
       },
       {
           .name = "metric with one label",
@@ -86,16 +87,48 @@ DEF_TEST(format_metric_family) {
                                               },
                                           .num = 1,
                                       },
-                                  .value = (value_t){
-                                    .counter = 42,
-                                  },
+                                  .value =
+                                      (value_t){
+                                          .counter = 42,
+                                      },
                               },
                           .num = 1,
                       },
               },
           .want = "# HELP unittest\n"
-              "# TYPE unittest counter\n"
-              "unittest{foo=\"bar\"} 42\n",
+                  "# TYPE unittest counter\n"
+                  "unittest{foo=\"bar\"} 42\n",
+      },
+      {
+          .name = "invalid characters are replaced",
+          .fam =
+              {
+                  .name = "unit.test",
+                  .type = METRIC_TYPE_COUNTER,
+                  .metric =
+                      {
+                          .ptr =
+                              &(metric_t){
+                                  .label =
+                                      {
+                                          .ptr =
+                                              &(label_pair_t){
+                                                  .name = "metric.name",
+                                                  .value = "unit.test",
+                                              },
+                                          .num = 1,
+                                      },
+                                  .value =
+                                      (value_t){
+                                          .counter = 42,
+                                      },
+                              },
+                          .num = 1,
+                      },
+              },
+          .want = "# HELP unit_test\n"
+                  "# TYPE unit_test counter\n"
+                  "unit_test{metric_name=\"unit.test\"} 42\n",
       },
   };