]> git.ipfire.org Git - thirdparty/collectd.git/commitdiff
write_prometheus plugin: Emit a single `target_info` metric family. 4213/head
authorFlorian Forster <octo@collectd.org>
Tue, 26 Dec 2023 07:27:43 +0000 (08:27 +0100)
committerFlorian Forster <octo@collectd.org>
Thu, 28 Dec 2023 19:13:31 +0000 (20:13 +0100)
With this change, the *write_promtheus plugin* will emit a single
`target_info` metric family with one metric per resource.

src/write_prometheus.c
src/write_prometheus_test.c

index 1054697b48c43b980eed062f06ebd57ed464ab74..d4c4f812e516102926fa516db3d0765a120cdba7 100644 (file)
@@ -253,24 +253,75 @@ void format_metric_family(strbuf_t *buf, metric_family_t const *prom_fam) {
   strbuf_printf(buf, "\n");
 }
 
+typedef struct {
+  label_set_t *resources;
+  size_t resources_num;
+} target_info_t;
+
+static int target_info_compare(void const *a, void const *b) {
+  label_set_t const *lsa = a;
+  label_set_t const *lsb = b;
+
+  return label_set_compare(*lsa, *lsb);
+}
+
+static int target_info_add(target_info_t *ti, label_set_t resource) {
+  label_set_t *found = bsearch(&resource, ti->resources, ti->resources_num,
+                               sizeof(*ti->resources), target_info_compare);
+  if (found != NULL) {
+    return 0;
+  }
+
+  label_set_t *ls =
+      realloc(ti->resources, sizeof(*ti->resources) * (ti->resources_num + 1));
+  if (ls == NULL) {
+    ERROR("write_prometheus plugin: realloc failed.");
+    return ENOMEM;
+  }
+  ti->resources = ls;
+
+  ls = ti->resources + ti->resources_num;
+  memset(ls, 0, sizeof(*ls));
+  int status = label_set_clone(ls, resource);
+  if (status != 0) {
+    ERROR("write_prometheus plugin: label_set_clone failed.");
+    return status;
+  }
+
+  ti->resources_num++;
+  qsort(ti->resources, ti->resources_num, sizeof(*ti->resources),
+        target_info_compare);
+
+  return 0;
+}
+
+static void target_info_reset(target_info_t *ti) {
+  for (size_t i = 0; i < ti->resources_num; i++) {
+    label_set_reset(ti->resources + i);
+  }
+  free(ti->resources);
+  ti->resources = NULL;
+  ti->resources_num = 0;
+}
+
 /* target_info prints a special "info" metric that contains all the "target
  * labels" aka. resource attributes.
  * See
  * https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#supporting-target-metadata-in-both-push-based-and-pull-based-systems
  * for more details. */
 /* visible for testing */
-void target_info(strbuf_t *buf, label_set_t resource) {
-  if (resource.num == 0) {
-    return;
-  }
+void target_info(strbuf_t *buf, metric_family_t const **families,
+                 size_t families_num) {
+  target_info_t ti = {0};
 
-  char const *job = label_set_get(resource, "service.name");
-  char const *instance = label_set_get(resource, "service.instance.id");
+  for (size_t i = 0; i < families_num; i++) {
+    metric_family_t const *fam = families[i];
+    target_info_add(&ti, fam->resource);
+  }
 
-  label_set_t rattr = {0};
-  label_set_clone(&rattr, resource);
-  label_set_update(&rattr, "service.name", NULL);
-  label_set_update(&rattr, "service.instance.id", NULL);
+  if (ti.resources_num == 0) {
+    return;
+  }
 
 #ifdef EXPOSE_OPEN_METRICS
   strbuf_print(buf, "# TYPE target info\n");
@@ -280,28 +331,66 @@ void target_info(strbuf_t *buf, label_set_t resource) {
   strbuf_print(buf, "# TYPE target_info gauge\n");
 #endif
 
-  strbuf_print(buf, "target_info{");
-  format_label_set(buf, rattr, job, instance);
-  strbuf_print(buf, "} 1\n");
+  for (size_t i = 0; i < ti.resources_num; i++) {
+    label_set_t *resource = ti.resources + i;
+
+    char *job = NULL;
+    char *instance = NULL;
+
+    char const *v;
+    if ((v = label_set_get(*resource, "service.name")) != NULL) {
+      job = strdup(v);
+      label_set_update(resource, "service.name", NULL);
+    }
+
+    if ((v = label_set_get(*resource, "service.instance.id")) != NULL) {
+      instance = strdup(v);
+      label_set_update(resource, "service.instance.id", NULL);
+    }
+
+    strbuf_print(buf, "target_info{");
+    format_label_set(buf, *resource, job, instance);
+    strbuf_print(buf, "} 1\n");
+
+    free(job);
+    free(instance);
+  }
 
-  label_set_reset(&rattr);
+  strbuf_print(buf, "\n");
+  target_info_reset(&ti);
 }
 
-static void format_text(strbuf_t *buf) {
-  label_set_t resource = default_resource_attributes();
-  target_info(buf, resource);
+static void format_metric_families(strbuf_t *buf,
+                                   metric_family_t const **families,
+                                   size_t families_num) {
+  target_info(buf, families, families_num);
+
+  for (size_t i = 0; i < families_num; i++) {
+    metric_family_t const *fam = families[i];
+    format_metric_family(buf, fam);
+  }
+}
 
+static void format_text(strbuf_t *buf) {
   pthread_mutex_lock(&prom_metrics_lock);
 
-  char *unused;
-  metric_family_t *prom_fam;
+  size_t families_num = (size_t)c_avl_size(prom_metrics);
+  metric_family_t const *families[families_num];
+  memset(families, 0, sizeof(families));
 
+  char *unused = NULL;
+  metric_family_t *prom_fam = NULL;
   c_avl_iterator_t *iter = c_avl_get_iterator(prom_metrics);
-  while (c_avl_iterator_next(iter, (void *)&unused, (void *)&prom_fam) == 0) {
-    format_metric_family(buf, prom_fam);
+  for (size_t i = 0;
+       c_avl_iterator_next(iter, (void *)&unused, (void *)&prom_fam) == 0;
+       i++) {
+    assert(i < families_num);
+    families[i] = prom_fam;
   }
   c_avl_iterator_destroy(iter);
 
+  format_metric_families(buf, families, families_num);
+
   strbuf_printf(buf, "# collectd/write_prometheus %s at %s\n", PACKAGE_VERSION,
                 hostname_g);
 
index 4391e9122a0d1dbf4265afe797c9dea67296088d..ecbaa9a075beeec22510e983a1fd9dab4f955105 100644 (file)
@@ -257,54 +257,133 @@ DEF_TEST(format_metric_family) {
   return 0;
 }
 
-void target_info(strbuf_t *buf, label_set_t resource);
+void target_info(strbuf_t *buf, metric_family_t const **families,
+                 size_t families_num);
 
 DEF_TEST(target_info) {
   struct {
     char const *name;
-    label_set_t resource;
+    label_set_t *resources;
+    size_t resources_num;
     char const *want;
   } cases[] = {
       {
           .name = "single resource attribute",
-          .resource =
-              {
-                  .ptr = &(label_pair_t){"foo", "bar"},
-                  .num = 1,
+          .resources =
+              (label_set_t[]){
+                  {
+                      .ptr = &(label_pair_t){"foo", "bar"},
+                      .num = 1,
+                  },
+              },
+          .resources_num = 1,
+          .want = "# HELP target_info Target metadata\n"
+                  "# TYPE target_info gauge\n"
+                  "target_info{foo=\"bar\"} 1\n\n",
+      },
+      {
+          .name = "identical resources get deduplicated",
+          .resources =
+              (label_set_t[]){
+                  {
+                      .ptr = &(label_pair_t){"foo", "bar"},
+                      .num = 1,
+                  },
+                  {
+                      .ptr = &(label_pair_t){"foo", "bar"},
+                      .num = 1,
+                  },
               },
+          .resources_num = 2,
           .want = "# HELP target_info Target metadata\n"
                   "# TYPE target_info gauge\n"
-                  "target_info{foo=\"bar\"} 1\n",
+                  "target_info{foo=\"bar\"} 1\n\n",
       },
       {
           .name = "service.name gets translated to job",
-          .resource =
-              {
-                  .ptr = &(label_pair_t){"service.name", "unittest"},
-                  .num = 1,
+          .resources =
+              (label_set_t[]){
+                  {
+                      .ptr = &(label_pair_t){"service.name", "unittest"},
+                      .num = 1,
+                  },
               },
+          .resources_num = 1,
           .want = "# HELP target_info Target metadata\n"
                   "# TYPE target_info gauge\n"
-                  "target_info{job=\"unittest\"} 1\n",
+                  "target_info{job=\"unittest\"} 1\n\n",
       },
       {
           .name = "service.instance.id gets translated to instance",
-          .resource =
-              {
-                  .ptr = &(label_pair_t){"service.instance.id", "42"},
-                  .num = 1,
+          .resources =
+              (label_set_t[]){
+                  {
+                      .ptr = &(label_pair_t){"service.instance.id", "42"},
+                      .num = 1,
+                  },
               },
+          .resources_num = 1,
           .want = "# HELP target_info Target metadata\n"
                   "# TYPE target_info gauge\n"
-                  "target_info{instance=\"42\"} 1\n",
+                  "target_info{instance=\"42\"} 1\n\n",
+      },
+      {
+          .name = "multiple resources",
+          .resources =
+              (label_set_t[]){
+                  {
+                      .ptr =
+                          (label_pair_t[]){
+                              {"additional", "label"},
+                              {"service.instance.id", "id:0"},
+                              {"service.name", "unit.test"},
+                          },
+                      .num = 3,
+                  },
+                  {
+                      .ptr =
+                          (label_pair_t[]){
+                              {"(additional)", "\"label\""},
+                              {"service.instance.id", "id:1"},
+                              {"service.name", "unit.test"},
+                          },
+                      .num = 3,
+                  },
+                  {
+                      .ptr =
+                          (label_pair_t[]){
+                              {"42 additional", "label\n"},
+                              {"service.instance.id", "id:2"},
+                              {"service.name", "unit.test"},
+                          },
+                      .num = 3,
+                  },
+              },
+          .resources_num = 3,
+          // clang-format off
+          .want =
+"# HELP target_info Target metadata\n"
+"# TYPE target_info gauge\n"
+"target_info{job=\"unit.test\",instance=\"id:1\",key_additional_=\"\\\"label\\\"\"} 1\n"
+"target_info{job=\"unit.test\",instance=\"id:2\",key_42_additional=\"label\\n\"} 1\n"
+"target_info{job=\"unit.test\",instance=\"id:0\",additional=\"label\"} 1\n"
+"\n",
+          // clang-format on
       },
   };
 
   for (size_t i = 0; i < STATIC_ARRAY_SIZE(cases); i++) {
     printf("# Case %zu: %s\n", i, cases[i].name);
-    strbuf_t got = STRBUF_CREATE;
 
-    target_info(&got, cases[i].resource);
+    metric_family_t families[cases[i].resources_num];
+    metric_family_t const *family_ptrs[cases[i].resources_num];
+    for (size_t j = 0; j < cases[i].resources_num; j++) {
+      families[j].resource = cases[i].resources[j];
+      family_ptrs[j] = &families[j];
+    }
+
+    strbuf_t got = STRBUF_CREATE;
+    target_info(&got, family_ptrs, cases[i].resources_num);
     EXPECT_EQ_STR(cases[i].want, got.ptr);
 
     STRBUF_DESTROY(got);