return status;
}
-static int format_metric(strbuf_t *buf, metric_t const *m) {
+static int format_metric(strbuf_t *buf, metric_t const *m,
+ char const *metric_family_name) {
if ((buf == NULL) || (m == NULL) || (m->family == NULL)) {
return EINVAL;
}
label_set_t resource = m->family->resource;
+ /* metric_family_name is already escaped, so strbuf_print_restricted should
+ * not replace any characters. */
int status =
- strbuf_print_restricted(buf, m->family->name, VALID_NAME_CHARS, '_');
+ strbuf_print_restricted(buf, metric_family_name, VALID_NAME_CHARS, '_');
if (resource.num == 0 && m->label.num == 0) {
return status;
}
return status || strbuf_print(buf, "}");
}
+/* format_metric_family_name creates a Prometheus compatible metric name by
+ * replacing all characters that are invalid in Prometheus with underscores,
+ * drop any leading and trailing underscores, and collapses a sequence of
+ * multiple underscores into one underscore.
+ *
+ * Visible for testing */
+void format_metric_family_name(strbuf_t *buf, metric_family_t const *fam) {
+ size_t name_len = strlen(fam->name);
+ char name[name_len + 1];
+ memset(name, 0, sizeof(name));
+
+ strbuf_t namebuf = STRBUF_CREATE_FIXED(name, sizeof(name));
+ strbuf_print_restricted(&namebuf, fam->name, VALID_NAME_CHARS, '_');
+ STRBUF_DESTROY(namebuf);
+
+ bool skip_underscore = true;
+ size_t out = 0;
+ for (size_t in = 0; in < name_len; in++) {
+ if (skip_underscore && name[in] == '_') {
+ continue;
+ }
+ skip_underscore = (name[in] == '_');
+ name[out] = name[in];
+ out++;
+ }
+ name_len = out;
+ name[name_len] = 0;
+
+ while (name_len > 0 && name[name_len - 1] == '_') {
+ name_len--;
+ name[name_len] = 0;
+ }
+
+ strbuf_print(buf, name);
+
+ if (fam->type == METRIC_TYPE_COUNTER) {
+ strbuf_print(buf, "_total");
+ }
+}
+
/* visible for testing */
void format_metric_family(strbuf_t *buf, metric_family_t const *prom_fam) {
if (prom_fam->metric.num == 0)
}
strbuf_t family_name = STRBUF_CREATE;
- strbuf_print_restricted(&family_name, prom_fam->name, VALID_NAME_CHARS, '_');
+ format_metric_family_name(&family_name, prom_fam);
if (prom_fam->help == NULL)
strbuf_printf(buf, "# HELP %s\n", family_name.ptr);
strbuf_printf(buf, "# HELP %s %s\n", family_name.ptr, prom_fam->help);
strbuf_printf(buf, "# TYPE %s %s\n", family_name.ptr, type);
- STRBUF_DESTROY(family_name);
-
for (size_t i = 0; i < prom_fam->metric.num; i++) {
metric_t *m = &prom_fam->metric.ptr[i];
- format_metric(buf, m);
+ format_metric(buf, m, family_name.ptr);
if (prom_fam->type == METRIC_TYPE_COUNTER)
strbuf_printf(buf, " %" PRIu64, m->value.counter);
strbuf_printf(buf, "\n");
}
}
+
+ STRBUF_DESTROY(family_name);
}
/* target_info prints a special "info" metric that contains all the "target
#include "testing.h"
#include "utils/common/common.h"
+void format_metric_family_name(strbuf_t *buf, metric_family_t const *fam);
+
+DEF_TEST(format_metric_family_name) {
+ // Test cases are based on:
+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/translator/prometheus/README.md
+ struct {
+ char *name;
+ metric_type_t type;
+ char *want;
+ } cases[] = {
+ {"(lambda).function.executions(#)", METRIC_TYPE_UNTYPED,
+ "lambda_function_executions"},
+ {"system.processes.created", METRIC_TYPE_COUNTER,
+ "system_processes_created_total"},
+ };
+
+ 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;
+
+ metric_family_t fam = {
+ .name = cases[i].name,
+ .type = cases[i].type,
+ };
+
+ format_metric_family_name(&got, &fam);
+ EXPECT_EQ_STR(cases[i].want, got.ptr);
+
+ STRBUF_DESTROY(got);
+ }
+
+ return 0;
+}
+
void format_metric_family(strbuf_t *buf, metric_family_t const *prom_fam);
DEF_TEST(format_metric_family) {
.num = 1,
},
},
- .want = "# HELP unit_test\n"
- "# TYPE unit_test counter\n"
- "unit_test 42\n",
+ .want = "# HELP unit_test_total\n"
+ "# TYPE unit_test_total counter\n"
+ "unit_test_total 42\n",
},
{
.name = "metric with one label",
.fam =
{
.name = "unittest",
- .type = METRIC_TYPE_COUNTER,
+ .type = METRIC_TYPE_GAUGE,
.metric =
{
.ptr =
},
.value =
(value_t){
- .counter = 42,
+ .gauge = 42,
},
},
.num = 1,
},
},
.want = "# HELP unittest\n"
- "# TYPE unittest counter\n"
+ "# TYPE unittest gauge\n"
"unittest{foo=\"bar\"} 42\n",
},
{
.fam =
{
.name = "unit.test",
- .type = METRIC_TYPE_COUNTER,
+ .type = METRIC_TYPE_UNTYPED,
.metric =
{
.ptr =
},
.value =
(value_t){
- .counter = 42,
+ .gauge = 42,
},
},
.num = 1,
},
},
.want = "# HELP unit_test\n"
- "# TYPE unit_test counter\n"
+ "# TYPE unit_test untyped\n"
"unit_test{metric_name=\"unit.test\"} 42\n",
},
};
}
int main(void) {
+ RUN_TEST(format_metric_family_name);
RUN_TEST(format_metric_family);
RUN_TEST(target_info);