From 03985a5d2dc83e5457ac1341e872ef790785f25f Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 26 Dec 2023 08:27:43 +0100 Subject: [PATCH] write_prometheus plugin: Emit a single `target_info` metric family. With this change, the *write_promtheus plugin* will emit a single `target_info` metric family with one metric per resource. --- src/write_prometheus.c | 131 ++++++++++++++++++++++++++++++------ src/write_prometheus_test.c | 117 ++++++++++++++++++++++++++------ 2 files changed, 208 insertions(+), 40 deletions(-) diff --git a/src/write_prometheus.c b/src/write_prometheus.c index 1054697b4..d4c4f812e 100644 --- a/src/write_prometheus.c +++ b/src/write_prometheus.c @@ -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); diff --git a/src/write_prometheus_test.c b/src/write_prometheus_test.c index 4391e9122..ecbaa9a07 100644 --- a/src/write_prometheus_test.c +++ b/src/write_prometheus_test.c @@ -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); -- 2.47.2