From a98fc72e2970a36ec1f0d401b03f65fd8ef86398 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 13:45:22 +0100 Subject: [PATCH 01/19] json: add json_variant_compare() helper for comparint two json variants by order --- src/libsystemd/sd-json/json-util.c | 102 +++++++++++++++++++++++++++++ src/libsystemd/sd-json/json-util.h | 2 + src/test/test-json.c | 65 ++++++++++++++++++ 3 files changed, 169 insertions(+) diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c index 41ac8c4097..7f90b7fc79 100644 --- a/src/libsystemd/sd-json/json-util.c +++ b/src/libsystemd/sd-json/json-util.c @@ -743,3 +743,105 @@ int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_jso return 0; } + +int json_variant_compare(sd_json_variant *a, sd_json_variant *b) { + int r; + + r = CMP(!!a, !!b); + if (r != 0) + return r; + + if (sd_json_variant_equal(a, b)) + return 0; + + if (sd_json_variant_is_null(a)) + return -1; + if (sd_json_variant_is_null(b)) + return 1; + + if (sd_json_variant_is_string(a) && + sd_json_variant_is_string(b)) + return strcmp(sd_json_variant_string(a), sd_json_variant_string(b)); + + if (sd_json_variant_is_integer(a) && + sd_json_variant_is_integer(b)) + return CMP(sd_json_variant_integer(a), sd_json_variant_integer(b)); + + if (sd_json_variant_is_unsigned(a) && + sd_json_variant_is_unsigned(b)) + return CMP(sd_json_variant_unsigned(a), sd_json_variant_unsigned(b)); + + /* We cannot necessarily compare 64bit signed with unsigned, hence we go via sign checking instead */ + if (sd_json_variant_is_number(a) && sd_json_variant_is_number(b)) { + if (sd_json_variant_is_negative(a) && + !sd_json_variant_is_negative(b)) + return -1; + + if (!sd_json_variant_is_negative(a) && + sd_json_variant_is_negative(b)) + return 1; + } + + if (sd_json_variant_is_real(a) && + sd_json_variant_is_real(b)) + return CMP(sd_json_variant_real(a), sd_json_variant_real(b)); + + if (sd_json_variant_is_boolean(a) && + sd_json_variant_is_boolean(b)) + return CMP(sd_json_variant_boolean(a), sd_json_variant_boolean(b)); + + if (sd_json_variant_is_array(a) && + sd_json_variant_is_array(b)) { + + size_t n = sd_json_variant_elements(a), + m = sd_json_variant_elements(b); + for (size_t i = 0; i < n || i < m; i++) { + + if (i >= n) + return -1; + if (i >= m) + return 1; + + r = json_variant_compare( + sd_json_variant_by_index(a, i), + sd_json_variant_by_index(b, i)); + if (r != 0) + return r; + } + + return 0; + } + + if (sd_json_variant_is_object(a) && + sd_json_variant_is_object(b)) { + const char *k, *lowest = NULL; + sd_json_variant *v; + int result = 0; + + JSON_VARIANT_OBJECT_FOREACH(k, v, a) { + if (lowest && strcmp(k, lowest) >= 0) + continue; + + r = json_variant_compare(v, sd_json_variant_by_key(b, k)); + if (r != 0) { + lowest = k; + result = r; + } + } + + JSON_VARIANT_OBJECT_FOREACH(k, v, b) { + if (lowest && strcmp(k, lowest) >= 0) + continue; + + r = json_variant_compare(v, sd_json_variant_by_key(a, k)); + if (r != 0) { + lowest = k; + result = -r; + } + } + + return result; + } + + return CMP(sd_json_variant_type(a), sd_json_variant_type(b)); +} diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h index d06a72aef9..847725a41e 100644 --- a/src/libsystemd/sd-json/json-util.h +++ b/src/libsystemd/sd-json/json-util.h @@ -273,3 +273,5 @@ int json_variant_new_fd_info(sd_json_variant **ret, int fd); char *json_underscorify(char *p); char *json_dashify(char *p); + +int json_variant_compare(sd_json_variant *a, sd_json_variant *b); diff --git a/src/test/test-json.c b/src/test/test-json.c index 1f0e07d7d4..679152fd95 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -1523,4 +1523,69 @@ TEST(access_mode) { &mm), ERANGE); } +static void test_json_variant_compare_one(const char *a, const char *b, int expected) { + int r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *aa = NULL; + if (!isempty(a)) + ASSERT_OK(sd_json_parse(a, /* flags= */ 0, &aa, /* reterr_line= */ NULL, /* reterr_column= */ NULL)); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *bb = NULL; + if (!isempty(b)) + ASSERT_OK(sd_json_parse(b, /* flags= */ 0, &bb, /* reterr_line= */ NULL, /* reterr_column= */ NULL)); + + r = json_variant_compare(aa, bb); + + log_debug("%s vs %s → %i (expected %i)", a, b, r, expected); + + if (expected < 0) + ASSERT_LT(r, 0); + else if (expected > 0) + ASSERT_GT(r, 0); + else + ASSERT_EQ(r, 0); + + r = json_variant_compare(bb, aa); + + if (expected < 0) + ASSERT_GT(r, 0); + else if (expected > 0) + ASSERT_LT(r, 0); + else + ASSERT_EQ(r, 0); +} + +TEST(json_variant_compare) { + test_json_variant_compare_one("null", "\"a\"", -1); + test_json_variant_compare_one(NULL, "\"a\"", -1); + test_json_variant_compare_one("0", "1", -1); + test_json_variant_compare_one("1", "0", 1); + test_json_variant_compare_one("0", "0", 0); + test_json_variant_compare_one("1", "1", 0); + test_json_variant_compare_one("1", "null", 1); + test_json_variant_compare_one("null", "1", -1); + test_json_variant_compare_one("null", "null", 0); + test_json_variant_compare_one("false", "true", -1); + test_json_variant_compare_one("true", "false", 1); + test_json_variant_compare_one("true", "true", 0); + test_json_variant_compare_one("false", "false", 0); + test_json_variant_compare_one("\"a\"", "\"b\"", -1); + test_json_variant_compare_one("\"b\"", "\"a\"", 1); + test_json_variant_compare_one("18446744073709551615", "0", 1); + test_json_variant_compare_one("0", "18446744073709551615", -1); + test_json_variant_compare_one("18446744073709551615", "18446744073709551615", 0); + test_json_variant_compare_one("-9223372036854775808", "18446744073709551615", -1); + test_json_variant_compare_one("18446744073709551615", "-9223372036854775808", 1); + test_json_variant_compare_one("1.1", "3.4", -1); + test_json_variant_compare_one("1", "3.4", -1); + test_json_variant_compare_one("[1,2]", "[1,2]", 0); + test_json_variant_compare_one("[1,2]", "[2,1]", -1); + test_json_variant_compare_one("[1,2]", "[1,2,3]", -1); + test_json_variant_compare_one("{}", "{\"a\":\"b\"}", -1); + test_json_variant_compare_one("{\"a\":\"b\"}", "{\"a\":\"b\"}", 0); + test_json_variant_compare_one("{\"a\":\"b\"}", "{\"b\":\"c\"}", 1); + test_json_variant_compare_one("{\"a\":\"b\",\"b\":\"c\"}", "{\"b\":\"c\",\"a\":\"b\"}", 0); + test_json_variant_compare_one("{\"a\":\"b\",\"b\":\"c\"}", "{\"a\":\"b\"}", 1); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From 22cc1625e7760d2ec7902421f02a715c2890cb73 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 13:45:52 +0100 Subject: [PATCH 02/19] format-table: add a new JSON cell type This formats the specified json variant as a string, and displays it in a cell. --- src/shared/format-table.c | 53 +++++++++++++++++++++++++++++++++++++-- src/shared/format-table.h | 1 + 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 9f6c46afb7..8b5b88370f 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -14,6 +14,7 @@ #include "glyph-util.h" #include "gunicode.h" #include "in-addr-util.h" +#include "json-util.h" #include "memory-util.h" #include "memstream-util.h" #include "pager.h" @@ -110,6 +111,7 @@ typedef struct TableData { pid_t pid; mode_t mode; dev_t devnum; + sd_json_variant *json; /* … add more here as we start supporting more cell data types … */ }; } TableData; @@ -249,6 +251,9 @@ static TableData *table_data_free(TableData *d) { if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED)) strv_free(d->strv); + if (d->type == TABLE_JSON) + sd_json_variant_unref(d->json); + return mfree(d); } @@ -363,6 +368,9 @@ static size_t table_data_size(TableDataType type, const void *data) { case TABLE_DEVNUM: return sizeof(dev_t); + case TABLE_JSON: + return sizeof(sd_json_variant*); + default: assert_not_reached(); } @@ -445,12 +453,22 @@ static TableData *table_data_new( d->ellipsize_percent = ellipsize_percent; d->uppercase = uppercase; - if (IN_SET(type, TABLE_STRV, TABLE_STRV_WRAPPED)) { + switch (type) { + + case TABLE_STRV: + case TABLE_STRV_WRAPPED: d->strv = strv_copy(data); if (!d->strv) return NULL; - } else + break; + + case TABLE_JSON: + d->json = sd_json_variant_ref((sd_json_variant*) data); + break; + + default: memcpy_safe(d->data, data, data_size); + } return TAKE_PTR(d); } @@ -1096,6 +1114,10 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) { data = &buffer.devnum; break; + case TABLE_JSON: + data = va_arg(ap, sd_json_variant*); + break; + case TABLE_SET_MINIMUM_WIDTH: { size_t w = va_arg(ap, size_t); @@ -1506,6 +1528,9 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t return CMP(minor(a->devnum), minor(b->devnum)); + case TABLE_JSON: + return json_variant_compare(a->json, b->json); + default: ; } @@ -2071,6 +2096,18 @@ static const char *table_data_format( break; + case TABLE_JSON: { + if (!d->json) + return table_ersatz_string(t); + + char *p; + if (sd_json_variant_format(d->json, /* flags= */ 0, &p) < 0) + return NULL; + + d->formatted = p; + break; + } + default: assert_not_reached(); } @@ -2284,6 +2321,9 @@ static bool table_data_isempty(const TableData *d) { if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED)) return strv_isempty(d->strv); + if (d->type == TABLE_JSON) + return sd_json_variant_is_null(d->json); + /* Note that an empty string we do not consider empty here! */ return false; } @@ -2991,6 +3031,15 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) { SD_JSON_BUILD_UNSIGNED(major(d->devnum)), SD_JSON_BUILD_UNSIGNED(minor(d->devnum)))); + case TABLE_JSON: + if (!d->json) + return sd_json_variant_new_null(ret); + + if (ret) + *ret = sd_json_variant_ref(d->json); + + return 0; + case TABLE_STRING_WITH_ANSI: { _cleanup_free_ char *s = strdup(d->string); if (!s) diff --git a/src/shared/format-table.h b/src/shared/format-table.h index a6a06a009a..ec9989c54c 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -58,6 +58,7 @@ typedef enum TableDataType { TABLE_MODE, /* as in UNIX file mode (mode_t), in typical octal output */ TABLE_MODE_INODE_TYPE, /* also mode_t, but displays only the inode type as string */ TABLE_DEVNUM, /* a dev_t, displayed in the usual major:minor way */ + TABLE_JSON, /* an sd_json_variant, for output formatted into text */ _TABLE_DATA_TYPE_MAX, /* The following are not really data types, but commands for table_add_cell_many() to make changes to From 98417865814baa0a6560fee507f7c0cc4653f0f1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 11:38:40 +0100 Subject: [PATCH 03/19] metrics: show metrics 'keys' before 'values' In a way, metrics are a key-value concept, where the key is a triplet of metrics family name, object name, and "fields". Let's put them together in the varlink call, and put the value last, separately from that. Also, update docs a bit, i.e be explicit about the metrics *family* name everyhwere. --- src/shared/varlink-io.systemd.Metrics.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/varlink-io.systemd.Metrics.c b/src/shared/varlink-io.systemd.Metrics.c index e9d463e9fd..f4623f1f15 100644 --- a/src/shared/varlink-io.systemd.Metrics.c +++ b/src/shared/varlink-io.systemd.Metrics.c @@ -18,15 +18,15 @@ static SD_VARLINK_DEFINE_ERROR(NoSuchMetric); static SD_VARLINK_DEFINE_METHOD_FULL( List, SD_VARLINK_REQUIRES_MORE, - SD_VARLINK_FIELD_COMMENT("Metric name, e.g. io.systemd.Manager.unitsByTypeTotal or io.systemd.Manager.unitActiveState"), + SD_VARLINK_FIELD_COMMENT("Metric family name, e.g. io.systemd.Manager.unitsByTypeTotal or io.systemd.Manager.unitActiveState"), SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0), /* metric value has various types depending on MetricFamilyType and actual data double/int/uint */ - SD_VARLINK_FIELD_COMMENT("Metric value"), - SD_VARLINK_DEFINE_OUTPUT(value, SD_VARLINK_ANY, 0), SD_VARLINK_FIELD_COMMENT("Metric object name can be unit name, process name, etc, e.g. dev-hvc0.device"), SD_VARLINK_DEFINE_OUTPUT(object, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Metric fields are values to differentiate between different metrics in the same metric family"), - SD_VARLINK_DEFINE_OUTPUT(fields, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE)); + SD_VARLINK_DEFINE_OUTPUT(fields, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Metric value"), + SD_VARLINK_DEFINE_OUTPUT(value, SD_VARLINK_ANY, 0)); static SD_VARLINK_DEFINE_METHOD_FULL( Describe, From b01dfa2c83158c2d4b2fcdb1f1c91ebf827f8233 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 19 Feb 2026 08:47:51 +0100 Subject: [PATCH 04/19] report: add comment explaining that metric_startswith_prefix() does a true prefix match --- src/report/report.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/report/report.c b/src/report/report.c index e038d4fa5b..ab13ae905b 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -103,6 +103,9 @@ static inline bool metric_startswith_prefix(const char *metric_name, const char if (isempty(metric_name) || isempty(prefix)) return false; + /* NB: this checks for a *true* prefix, i.e. insists on the dot separator after the prefix. Or in + * other words, "foo" is not going to be considered a prefix of "foo", but of "foo.bar" it will. */ + const char *m = startswith(metric_name, prefix); return !isempty(m) && m[0] == '.'; } From 5344bede1e912b769eaf814b9c997cd4c29b7a7c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 11:38:11 +0100 Subject: [PATCH 05/19] report: adjust indentation to our usual style --- src/report/report.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/report/report.c b/src/report/report.c index ab13ae905b..fe4b96c5f0 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -61,12 +61,12 @@ static void context_done(Context *context) { DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_free); DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( - link_info_hash_ops, - void, - trivial_hash_func, - trivial_compare_func, - LinkInfo, - link_info_free); + link_info_hash_ops, + void, + trivial_hash_func, + trivial_compare_func, + LinkInfo, + link_info_free); static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) { const char *name_a, *name_b, *object_a, *object_b; From 5046a12fbcd24d208bbca02b229a8ddd0465d28d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 11:19:35 +0100 Subject: [PATCH 06/19] report: switch to "verbs" command line interface, and add 'describe-metrics' Let's prepare for a future where the "systemd-report" tool can do more than enumerate metrics: let's introduce our usual "verbs" style interface. Let's also add a second command right-away: "describe-metrics" shows the description of the metrics. --- man/systemd-report.xml | 27 ++++++++ src/report/report.c | 77 ++++++++++++++++------ test/units/TEST-74-AUX-UTILS.varlinkctl.sh | 2 +- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/man/systemd-report.xml b/man/systemd-report.xml index f786f0eb72..13c4958b03 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -37,6 +37,33 @@ standard output. + + Commands + + The following commands are understood: + + + + metrics + + Acquire a list of metrics values from all local services providing them, and write them to standard output. + + + + + + describe-metrics + + Acquire a list of metric families from all local services providing them, and write + them to standard output. This returns information about metrics, their data types and human readable + description without values. + + + + + + + Options diff --git a/src/report/report.c b/src/report/report.c index fe4b96c5f0..d8e262049a 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -19,7 +19,9 @@ #include "set.h" #include "sort-util.h" #include "string-util.h" +#include "strv.h" #include "time-util.h" +#include "verbs.h" #define METRICS_MAX 1024U #define METRICS_LINKS_MAX 128U @@ -28,7 +30,15 @@ static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; +typedef enum Action { + ACTION_LIST, + ACTION_DESCRIBE, + _ACTION_MAX, + _ACTION_INVALID = -EINVAL, +} Action; + typedef struct Context { + Action action; sd_event *event; Set *link_infos; sd_json_variant **metrics; /* Collected metrics for sorting */ @@ -204,9 +214,12 @@ static int metrics_call(Context *context, const char *path) { if (r < 0) return log_error_errno(r, "Failed to bind reply callback: %m"); - r = sd_varlink_observe(vl, "io.systemd.Metrics.List", /* parameters= */ NULL); + const char *method = context->action == ACTION_LIST ? "io.systemd.Metrics.List" : "io.systemd.Metrics.Describe"; + r = sd_varlink_observe(vl, + method, + /* parameters= */ NULL); if (r < 0) - return log_error_errno(r, "Failed to issue io.systemd.Metrics.List call: %m"); + return log_error_errno(r, "Failed to issue %s() call: %m", method); _cleanup_(link_info_freep) LinkInfo *li = new(LinkInfo, 1); if (!li) @@ -252,17 +265,30 @@ static int metrics_output_sorted(Context *context) { return 0; } -static int metrics_query(void) { +static int verb_metrics(int argc, char *argv[], void *userdata) { + Action action; int r; + assert(argc == 1); + assert(argv); + + if (streq_ptr(argv[0], "metrics")) + action = ACTION_LIST; + else { + assert(streq_ptr(argv[0], "describe-metrics")); + action = ACTION_DESCRIBE; + } + _cleanup_free_ char *metrics_path = NULL; r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &metrics_path); if (r < 0) return log_error_errno(r, "Failed to determine metrics directory path: %m"); - log_debug("Looking for reports in %s/", metrics_path); + log_debug("Looking for metrics in %s/", metrics_path); - _cleanup_(context_done) Context context = {}; + _cleanup_(context_done) Context context = { + .action = action, + }; r = sd_event_default(&context.event); if (r < 0) @@ -323,7 +349,7 @@ static int metrics_query(void) { return 0; } -static int help(void) { +static int verb_help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -331,19 +357,25 @@ static int help(void) { if (r < 0) return log_oom(); - printf("%s [OPTIONS...] \n\n" - "%sPrint metrics for all system components.%s\n\n" + printf("%1$s [OPTIONS...] COMMAND ...\n" + "\n%5$sAcquire metrics from local sources.%6$s\n" + "\n%3$sCommands:%4$s\n" + " metrics Acquire list of metrics and their values\n" + " describe-metrics Describe available metrics\n" + "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Show package version\n" " --user Connect to user service manager\n" " --system Connect to system service manager (default)\n" " --json=pretty|short\n" " Configure JSON output\n" - "\nSee the %s for details.\n", + "\nSee the %2$s for details.\n", program_invocation_short_name, - ansi_highlight(), + link, + ansi_underline(), ansi_normal(), - link); + ansi_highlight(), + ansi_normal()); return 0; } @@ -373,7 +405,7 @@ static int parse_argv(int argc, char *argv[]) { while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) switch (c) { case 'h': - return help(); + return verb_help(/* argc= */ 0, /* argv= */ NULL, /* userdata= */ NULL); case ARG_VERSION: return version(); @@ -400,15 +432,22 @@ static int parse_argv(int argc, char *argv[]) { assert_not_reached(); } - if (optind < argc) - return log_error_errno( - SYNTHETIC_ERRNO(EINVAL), - "%s takes no arguments.", - program_invocation_short_name); - return 1; } + +static int report_main(int argc, char *argv[]) { + + static const Verb verbs[] = { + { "help", VERB_ANY, 1, 0, verb_help }, + { "metrics", VERB_ANY, 1, 0, verb_metrics }, + { "describe-metrics", VERB_ANY, 1, 0, verb_metrics }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + static int run(int argc, char *argv[]) { int r; @@ -418,7 +457,7 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - return metrics_query(); + return report_main(argc, argv); } DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh index 377c4734e0..d72acc5949 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh @@ -244,7 +244,7 @@ systemd-run --wait --pipe --user --machine testuser@ \ varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}' # test report -systemd-report +systemd-report metrics # test io.systemd.Network Metrics varlinkctl info /run/systemd/report/io.systemd.Network From 0e05a035779ca752590783cc2cc48c111775483e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 11:47:31 +0100 Subject: [PATCH 07/19] report: split out service enumeration logic We want to reuse it later to list all services, hence make it generic. (Also, allow symlinked services too) --- src/report/report.c | 105 ++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/src/report/report.c b/src/report/report.c index d8e262049a..aea6d97a13 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -9,12 +9,12 @@ #include "ansi-color.h" #include "build.h" #include "dirent-util.h" -#include "fd-util.h" #include "log.h" #include "main-func.h" #include "parse-argument.h" #include "path-lookup.h" #include "pretty-print.h" +#include "recurse-dir.h" #include "runtime-scope.h" #include "set.h" #include "sort-util.h" @@ -191,7 +191,7 @@ finish: return 0; } -static int metrics_call(Context *context, const char *path) { +static int metrics_call(Context *context, const char *name, const char *path) { _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; int r; @@ -228,16 +228,17 @@ static int metrics_call(Context *context, const char *path) { *li = (LinkInfo) { .context = context, .link = sd_varlink_ref(vl), + .metric_prefix = strdup(name), }; - r = path_extract_filename(path, &li->metric_prefix); - if (r < 0) - return log_error_errno(r, "Failed to extract metric name from path %s: %m", path); + if (!li->metric_prefix) + return log_oom(); - (void) sd_varlink_set_userdata(vl, li); if (set_ensure_put(&context->link_infos, &link_info_hash_ops, li) < 0) return log_oom(); + (void) sd_varlink_set_userdata(vl, li); + TAKE_PTR(li); return 0; } @@ -265,6 +266,50 @@ static int metrics_output_sorted(Context *context) { return 0; } +static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { + int r; + + assert(ret_directory); + assert(ret); + + _cleanup_free_ char *sources_path = NULL; + r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &sources_path); + if (r < 0) + return log_error_errno(r, "Failed to determine sources directory path: %m"); + + log_debug("Looking for metrics in '%s'.", sources_path); + + size_t m = 0; + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all_at( + AT_FDCWD, + sources_path, + RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, + &de); + if (r == -ENOENT) + *ret = NULL; + else if (r < 0) + return log_error_errno(r, "Failed to enumerate '%s': %m", sources_path); + else { + /* Filter out non-sockets/non-symlinks entries */ + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *d = *i; + + if (!IN_SET(d->d_type, DT_SOCK, DT_LNK)) + continue; + + de->entries[m++] = *i; + } + + de->n_entries = m; + *ret = TAKE_PTR(de); + } + + *ret_directory = TAKE_PTR(sources_path); + return m > 0; +} + static int verb_metrics(int argc, char *argv[], void *userdata) { Action action; int r; @@ -279,52 +324,46 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { action = ACTION_DESCRIBE; } - _cleanup_free_ char *metrics_path = NULL; - r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &metrics_path); - if (r < 0) - return log_error_errno(r, "Failed to determine metrics directory path: %m"); - - log_debug("Looking for metrics in %s/", metrics_path); - _cleanup_(context_done) Context context = { .action = action, }; - - r = sd_event_default(&context.event); - if (r < 0) - return log_error_errno(r, "Failed to get event loop: %m"); - - r = sd_event_set_signal_exit(context.event, true); - if (r < 0) - return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m"); - size_t n_skipped_sources = 0; - _cleanup_closedir_ DIR *d = opendir(metrics_path); - if (!d) { - if (errno != ENOENT) - return log_error_errno(errno, "Failed to open metrics directory %s: %m", metrics_path); - } else - FOREACH_DIRENT(de, d, - return log_warning_errno(errno, "Failed to read %s: %m", metrics_path)) { - if (!IN_SET(de->d_type, DT_SOCK, DT_UNKNOWN)) - continue; + _cleanup_free_ DirectoryEntries *de = NULL; + _cleanup_free_ char *sources_path = NULL; + r = readdir_sources(&sources_path, &de); + if (r < 0) + return r; + if (r > 0) { + r = sd_event_default(&context.event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); + + r = sd_event_set_signal_exit(context.event, true); + if (r < 0) + return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m"); + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *d = *i; if (set_size(context.link_infos) >= METRICS_LINKS_MAX) { n_skipped_sources++; break; } - _cleanup_free_ char *p = path_join(metrics_path, de->d_name); + _cleanup_free_ char *p = path_join(sources_path, d->d_name); if (!p) return log_oom(); - (void) metrics_call(&context, p); + (void) metrics_call(&context, d->d_name, p); } + } if (set_isempty(context.link_infos)) log_info("No metrics sources found."); else { + assert(context.event); + r = sd_event_loop(context.event); if (r < 0) return log_error_errno(r, "Failed to run event loop: %m"); From e1f7f33713b1c1f42021251a89d3b75331725bcc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 11:37:58 +0100 Subject: [PATCH 08/19] report: add 'list-sources' verb for enumerating metrics sources --- man/systemd-report.xml | 8 ++++ src/report/report.c | 88 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 13c4958b03..530dac1e14 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -61,6 +61,13 @@ + + list-sources + + Show list of known metrics sources. + + + @@ -81,6 +88,7 @@ + diff --git a/src/report/report.c b/src/report/report.c index aea6d97a13..809346b5da 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -8,7 +8,9 @@ #include "alloc-util.h" #include "ansi-color.h" #include "build.h" +#include "chase.h" #include "dirent-util.h" +#include "format-table.h" #include "log.h" #include "main-func.h" #include "parse-argument.h" @@ -27,8 +29,9 @@ #define METRICS_LINKS_MAX 128U #define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */ +static PagerFlags arg_pager_flags = 0; static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; -static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; +static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; typedef enum Action { ACTION_LIST, @@ -317,6 +320,8 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { assert(argc == 1); assert(argv); + arg_json_format_flags &= ~SD_JSON_FORMAT_OFF; + if (streq_ptr(argv[0], "metrics")) action = ACTION_LIST; else { @@ -388,6 +393,61 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { return 0; } +static int verb_list_sources(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(table_unrefp) Table *table = table_new("source", "address"); + if (!table) + return log_oom(); + + _cleanup_free_ char *sources_path = NULL; + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_sources(&sources_path, &de); + if (r < 0) + return r; + if (r > 0) + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *d = *i; + + _cleanup_free_ char *k = path_join(sources_path, d->d_name); + if (!k) + return log_oom(); + + _cleanup_free_ char *resolved = NULL; + r = chase(k, /* root= */ NULL, CHASE_MUST_BE_SOCKET, &resolved, /* ret_fd= */ NULL); + if (r < 0) { + log_warning_errno(r, "Failed to resolve '%s', skipping: %m", k); + continue; + } + + _cleanup_free_ char *j = strjoin("unix:", resolved); + if (!j) + return log_oom(); + + r = table_add_many( + table, + TABLE_STRING, d->d_name, + TABLE_STRING, j); + if (r < 0) + return table_log_add_error(r); + } + + if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + if (r < 0) + return r; + } + + if (!sd_json_format_enabled(arg_json_format_flags)) { + if (table_isempty(table)) + printf("No sources available.\n"); + else + printf("\n%zu sources listed.\n", table_get_rows(table) - 1); + } + + return 0; +} + static int verb_help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -401,9 +461,11 @@ static int verb_help(int argc, char *argv[], void *userdata) { "\n%3$sCommands:%4$s\n" " metrics Acquire list of metrics and their values\n" " describe-metrics Describe available metrics\n" + " list-sources Show list of known metrics sources\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" " --user Connect to user service manager\n" " --system Connect to system service manager (default)\n" " --json=pretty|short\n" @@ -422,17 +484,19 @@ static int verb_help(int argc, char *argv[], void *userdata) { static int parse_argv(int argc, char *argv[]) { enum { ARG_VERSION = 0x100, + ARG_NO_PAGER, ARG_USER, ARG_SYSTEM, ARG_JSON, }; static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "user", no_argument, NULL, ARG_USER }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "json", required_argument, NULL, ARG_JSON }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "user", no_argument, NULL, ARG_USER }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "json", required_argument, NULL, ARG_JSON }, {} }; @@ -449,6 +513,10 @@ static int parse_argv(int argc, char *argv[]) { case ARG_VERSION: return version(); + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + case ARG_USER: arg_runtime_scope = RUNTIME_SCOPE_USER; break; @@ -474,13 +542,13 @@ static int parse_argv(int argc, char *argv[]) { return 1; } - static int report_main(int argc, char *argv[]) { static const Verb verbs[] = { - { "help", VERB_ANY, 1, 0, verb_help }, - { "metrics", VERB_ANY, 1, 0, verb_metrics }, - { "describe-metrics", VERB_ANY, 1, 0, verb_metrics }, + { "help", VERB_ANY, 1, 0, verb_help }, + { "metrics", VERB_ANY, 1, 0, verb_metrics }, + { "describe-metrics", VERB_ANY, 1, 0, verb_metrics }, + { "list-sources", VERB_ANY, 1, 0, verb_list_sources }, {} }; From 39225c99047050eda9901edd011a5c05567378f0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 14:08:42 +0100 Subject: [PATCH 09/19] report: also dump metrics in tabular output JSON output is great, but let's show the metrics by default in a more human readable fashion. --- src/report/report.c | 165 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 151 insertions(+), 14 deletions(-) diff --git a/src/report/report.c b/src/report/report.c index 809346b5da..d6c52b6ff9 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -246,25 +246,164 @@ static int metrics_call(Context *context, const char *name, const char *path) { return 0; } -static int metrics_output_sorted(Context *context) { +static int metrics_output_list(Context *context, Table **ret) { + int r; + + assert(context); + + _cleanup_(table_unrefp) Table *table = table_new("family", "object", "fields", "value"); + if (!table) + return log_oom(); + + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + table_set_sort(table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3); + + FOREACH_ARRAY(m, context->metrics, context->n_metrics) { + struct { + const char *name; + const char *object; + sd_json_variant *fields, *value; + } d = {}; + + static const sd_json_dispatch_field dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, name), SD_JSON_MANDATORY }, + { "object", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, object), 0 }, + { "fields", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_variant_noref, voffsetof(d, fields), 0 }, + { "value", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_variant_noref, voffsetof(d, value), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(*m, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &d); + if (r < 0) { + _cleanup_free_ char *t = NULL; + int k = sd_json_variant_format(*m, /* flags= */ 0, &t); + if (k < 0) + return log_error_errno(k, "Failed to format JSON: %m"); + + log_warning_errno(r, "Cannot parse metric, skipping: %s", t); + continue; + } + + r = table_add_many( + table, + TABLE_STRING, d.name, + TABLE_STRING, d.object, + TABLE_JSON, d.fields, + TABLE_SET_WEIGHT, 50U, + TABLE_JSON, d.value, + TABLE_SET_WEIGHT, 50U); + if (r < 0) + return table_log_add_error(r); + } + + *ret = TAKE_PTR(table); + return 0; +} + +static int metrics_output_describe(Context *context, Table **ret) { + int r; + + assert(context); + + _cleanup_(table_unrefp) Table *table = table_new("family", "type", "description"); + if (!table) + return log_oom(); + + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + table_set_sort(table, (size_t) 0, (size_t) 1, (size_t) 2); + + FOREACH_ARRAY(m, context->metrics, context->n_metrics) { + struct { + const char *name; + const char *type; + const char *description; + } d = {}; + + static const sd_json_dispatch_field dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, name), SD_JSON_MANDATORY }, + { "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, type), SD_JSON_MANDATORY }, + { "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, description), 0 }, + {} + }; + + r = sd_json_dispatch(*m, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &d); + if (r < 0) { + _cleanup_free_ char *t = NULL; + int k = sd_json_variant_format(*m, /* flags= */ 0, &t); + if (k < 0) + return log_error_errno(k, "Failed to format JSON: %m"); + + log_warning_errno(r, "Cannot parse metric description, skipping: %s", t); + continue; + } + + r = table_add_many( + table, + TABLE_STRING, d.name, + TABLE_STRING, d.type, + TABLE_STRING, d.description, + TABLE_SET_WEIGHT, 50U); + if (r < 0) + return table_log_add_error(r); + } + + *ret = TAKE_PTR(table); + return 0; +} + +static int metrics_output(Context *context) { int r; assert(context); typesafe_qsort(context->metrics, context->n_metrics, metric_compare); - FOREACH_ARRAY(m, context->metrics, context->n_metrics) { - r = sd_json_variant_dump( - *m, - arg_json_format_flags | SD_JSON_FORMAT_FLUSH, - stdout, - /* prefix= */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to write JSON: %m"); + if (sd_json_format_enabled(arg_json_format_flags)) { + FOREACH_ARRAY(m, context->metrics, context->n_metrics) { + r = sd_json_variant_dump( + *m, + arg_json_format_flags | SD_JSON_FORMAT_FLUSH, + stdout, + /* prefix= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to write JSON: %m"); + } + + if (context->n_metrics == 0) + log_info("No metrics collected."); + + return 0; } - if (context->n_metrics == 0) - log_info("No metrics collected."); + _cleanup_(table_unrefp) Table *table = NULL; + switch(context->action) { + + case ACTION_LIST: + r = metrics_output_list(context, &table); + break; + + case ACTION_DESCRIBE: + r = metrics_output_describe(context, &table); + break; + + default: + assert_not_reached(); + } + if (r < 0) + return r; + + if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + if (r < 0) + return r; + } + + if (!sd_json_format_enabled(arg_json_format_flags)) { + if (table_isempty(table)) + printf("No metrics available.\n"); + else + printf("\n%zu metrics listed.\n", table_get_rows(table) - 1); + } return 0; } @@ -320,8 +459,6 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { assert(argc == 1); assert(argv); - arg_json_format_flags &= ~SD_JSON_FORMAT_OFF; - if (streq_ptr(argv[0], "metrics")) action = ACTION_LIST; else { @@ -373,7 +510,7 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Failed to run event loop: %m"); - r = metrics_output_sorted(&context); + r = metrics_output(&context); if (r < 0) return r; } From 442040e7168dda8cb13e490c0fcc8473638bec54 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 14:07:45 +0100 Subject: [PATCH 10/19] report: add -j shortcut json output is going to be used very frequently, hence provide a shortcut for it, like many our tools do it. --- man/systemd-report.xml | 1 + src/report/report.c | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 530dac1e14..010cf2e2f1 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -90,6 +90,7 @@ + diff --git a/src/report/report.c b/src/report/report.c index d6c52b6ff9..bf216dd2e3 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -607,6 +607,8 @@ static int verb_help(int argc, char *argv[], void *userdata) { " --system Connect to system service manager (default)\n" " --json=pretty|short\n" " Configure JSON output\n" + " -j Equivalent to --json=pretty (on TTY) or --json=short\n" + " (otherwise)\n" "\nSee the %2$s for details.\n", program_invocation_short_name, link, @@ -642,7 +644,7 @@ static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0) switch (c) { case 'h': return verb_help(/* argc= */ 0, /* argv= */ NULL, /* userdata= */ NULL); @@ -669,6 +671,10 @@ static int parse_argv(int argc, char *argv[]) { break; + case 'j': + arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; + break; + case '?': return -EINVAL; From 5617ebb096672b8284ffb02efe06e2577c6ba804 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 14:13:47 +0100 Subject: [PATCH 11/19] report: we don't use inline in .c files, the compiler can figure this out better on its own --- src/report/report.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/report/report.c b/src/report/report.c index bf216dd2e3..12a815814c 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -112,7 +112,7 @@ static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) return strcmp_ptr(fields_str_a, fields_str_b); } -static inline bool metric_startswith_prefix(const char *metric_name, const char *prefix) { +static bool metric_startswith_prefix(const char *metric_name, const char *prefix) { if (isempty(metric_name) || isempty(prefix)) return false; From ec2cd403f42c6a6a31b0adf3b94ae0acc9fd9add Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 14:43:07 +0100 Subject: [PATCH 12/19] report: tighten rules on metrics names Let's stay close to Varlink's naming rules and insist that metrics prefixes must be valid varlink interface names, and suffixes are valid varlink field names. The former rule is clear: because a metric . can only be provided by a varlink service , it is obvious we should validate them the same way. Validating the suffix via varlink field rules is not that obvious, but I think it makes sense to stay close to Varlink naming rules if we already started out at one place. --- src/report/report.c | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/report/report.c b/src/report/report.c index 12a815814c..562fe6653d 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -23,6 +23,7 @@ #include "string-util.h" #include "strv.h" #include "time-util.h" +#include "varlink-idl-util.h" #include "verbs.h" #define METRICS_MAX 1024U @@ -112,6 +113,29 @@ static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) return strcmp_ptr(fields_str_a, fields_str_b); } +static int metrics_name_valid(const char *metric_name) { + + /* Validates a metrics family name. Since the prefix shall match the Varlink service name, we'll + * enforce Varlink interface naming rules on it. Given how close we are to Varlink let's also enforce + * rules on metrics names similar to those of Varlink field names. */ + + const char *e = strrchr(metric_name, '.'); + if (!e) + return false; + + _cleanup_free_ char *j = strndup(metric_name, e - metric_name); + if (!j) + return -ENOMEM; + + if (!varlink_idl_interface_name_is_valid(j)) + return false; + + if (!varlink_idl_field_name_is_valid(e+1)) + return false; + + return true; +} + static bool metric_startswith_prefix(const char *metric_name, const char *prefix) { if (isempty(metric_name) || isempty(prefix)) return false; @@ -141,6 +165,16 @@ static bool metrics_validate_one(LinkInfo *li, sd_json_variant *metric) { return false; } + r = metrics_name_valid(metric_name); + if (r < 0) { + log_debug_errno(r, "Failed to determine if '%s' is a valid metric name: %m", metric_name); + return false; + } + if (!r) { + log_debug("Metric name '%s' is not valid, skipping.", metric_name); + return false; + } + return metric_startswith_prefix(metric_name, li->metric_prefix); } @@ -434,13 +468,16 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { else if (r < 0) return log_error_errno(r, "Failed to enumerate '%s': %m", sources_path); else { - /* Filter out non-sockets/non-symlinks entries */ + /* Filter out non-sockets/non-symlinks and badly named entries */ FOREACH_ARRAY(i, de->entries, de->n_entries) { struct dirent *d = *i; if (!IN_SET(d->d_type, DT_SOCK, DT_LNK)) continue; + if (!varlink_idl_interface_name_is_valid(d->d_name)) + continue; + de->entries[m++] = *i; } From e83cbc9372e66abacd9a8ecf45e1095010242127 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 14:53:08 +0100 Subject: [PATCH 13/19] report: implement filtering for metrics --- man/systemd-report.xml | 17 ++++-- src/report/report.c | 120 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 18 deletions(-) diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 010cf2e2f1..2b4dfeb51c 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -44,19 +44,26 @@ - metrics + metrics MATCH - Acquire a list of metrics values from all local services providing them, and write them to standard output. + Acquire a list of metrics values from all local services providing them, and write + them to standard output. Optionally takes one or more match expressions for filtering the metrics to + show. The expression either may be a literal metric family name to search for, or a prefix of one + (which will be matched only at dot boundaries). If multiple matches are specified as multiple + paramaters, any metric matching any of the specified matches are shown. - describe-metrics + describe-metrics MATCH Acquire a list of metric families from all local services providing them, and write - them to standard output. This returns information about metrics, their data types and human readable - description without values. + them to standard output. This returns primarily static information about metrics, their data types + and human readable description, without values. + + Match expressions similar to those supported by metrics are supported for + describe-metrics, too. diff --git a/src/report/report.c b/src/report/report.c index 562fe6653d..d59c3b9214 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -33,6 +33,9 @@ static PagerFlags arg_pager_flags = 0; static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; +static char **arg_matches = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep); typedef enum Action { ACTION_LIST, @@ -147,7 +150,13 @@ static bool metric_startswith_prefix(const char *metric_name, const char *prefix return !isempty(m) && m[0] == '.'; } -static bool metrics_validate_one(LinkInfo *li, sd_json_variant *metric) { +typedef enum { + VERDICT_INVALID, + VERDICT_MATCH, + VERDICT_MISMATCH, +} Verdict; + +static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) { int r; assert(li); @@ -162,20 +171,48 @@ static bool metrics_validate_one(LinkInfo *li, sd_json_variant *metric) { r = sd_json_dispatch(metric, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &metric_name); if (r < 0) { log_debug_errno(r, "Failed to get metric name, assuming name is not valid: %m"); - return false; + return VERDICT_INVALID; } + /* Validate metric name is generally valid */ r = metrics_name_valid(metric_name); if (r < 0) { log_debug_errno(r, "Failed to determine if '%s' is a valid metric name: %m", metric_name); - return false; + return VERDICT_INVALID; } if (!r) { log_debug("Metric name '%s' is not valid, skipping.", metric_name); - return false; + return VERDICT_INVALID; } - return metric_startswith_prefix(metric_name, li->metric_prefix); + /* Validate metric name matches the Varlink service it was found on */ + if (!metric_startswith_prefix(metric_name, li->metric_prefix)) { + log_debug("Metric name '%s' does not match service name '%s', skipping.", metric_name, li->metric_prefix); + return VERDICT_INVALID; + } + + /* Check it against any specified matches */ + bool matches; + if (strv_isempty(arg_matches)) + matches = true; + else { + matches = false; + + /* Allow exact matches or prefix matches */ + STRV_FOREACH(i, arg_matches) + if (streq(metric_name, *i) || + metric_startswith_prefix(metric_name, *i)) { + matches = true; + break; + } + } + + if (!matches) { + log_debug("Metric '%s' does not match search, ignoring.", metric_name); + return VERDICT_MISMATCH; + } + + return VERDICT_MATCH; } static int metrics_on_query_reply( @@ -206,10 +243,13 @@ static int metrics_on_query_reply( goto finish; } - if (!metrics_validate_one(li, parameters)) { + Verdict v = metrics_verdict(li, parameters); + if (v == VERDICT_INVALID) { context->n_invalid_metrics++; goto finish; } + if (v == VERDICT_MISMATCH) + goto finish; /* Collect metrics for later sorting */ if (!GREEDY_REALLOC(context->metrics, context->n_metrics + 1)) @@ -442,6 +482,52 @@ static int metrics_output(Context *context) { return 0; } +static int parse_metrics_matches(char **matches) { + int r; + + STRV_FOREACH(i, matches) { + r = metrics_name_valid(*i); + if (r < 0) + return log_error_errno(r, "Failed to determine if '%s' is a valid metric name: %m", *i); + if (!r && !varlink_idl_interface_name_is_valid(*i)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Match is not a valid family name or prefix: %s", *i); + + if (strv_extend(&arg_matches, *i) < 0) + return log_oom(); + } + + strv_sort_uniq(arg_matches); + return 0; +} + +static bool test_service_matches(const char *service) { + assert(service); + + if (strv_isempty(arg_matches)) + return true; + + /* Only contact services whose name is either a prefix of any of the specified metrics families, or + * if the specified metric families are a prefix of the service. + * + * Example: if user specifies "foo" we want to match sockets "foo" and "foo.bar". + * if user specifies "foo.waldo" we want to match sockets "foo" and "foo.waldo" as well as "foo.waldo.quux". + * + * in other words: it should be fine to specify any prefix of a service name to get all metrics from all matching services. + * it should also be fine to specify a full metric name, and then go directly to the relevant services, and ask for matching metrics. + */ + + STRV_FOREACH(i, arg_matches) { + if (streq(service, *i)) + return true; + + if (metric_startswith_prefix(*i, service) || + metric_startswith_prefix(service, *i)) + return true; + } + + return false; +} + static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { int r; @@ -478,6 +564,9 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { if (!varlink_idl_interface_name_is_valid(d->d_name)) continue; + if (!test_service_matches(d->d_name)) + continue; + de->entries[m++] = *i; } @@ -493,7 +582,7 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { Action action; int r; - assert(argc == 1); + assert(argc >= 1); assert(argv); if (streq_ptr(argv[0], "metrics")) @@ -503,6 +592,10 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { action = ACTION_DESCRIBE; } + r = parse_metrics_matches(argv + 1); + if (r < 0) + return r; + _cleanup_(context_done) Context context = { .action = action, }; @@ -633,8 +726,9 @@ static int verb_help(int argc, char *argv[], void *userdata) { printf("%1$s [OPTIONS...] COMMAND ...\n" "\n%5$sAcquire metrics from local sources.%6$s\n" "\n%3$sCommands:%4$s\n" - " metrics Acquire list of metrics and their values\n" - " describe-metrics Describe available metrics\n" + " metrics [MATCH...] Acquire list of metrics and their values\n" + " describe-metrics [MATCH...]\n" + " Describe available metrics\n" " list-sources Show list of known metrics sources\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" @@ -725,10 +819,10 @@ static int parse_argv(int argc, char *argv[]) { static int report_main(int argc, char *argv[]) { static const Verb verbs[] = { - { "help", VERB_ANY, 1, 0, verb_help }, - { "metrics", VERB_ANY, 1, 0, verb_metrics }, - { "describe-metrics", VERB_ANY, 1, 0, verb_metrics }, - { "list-sources", VERB_ANY, 1, 0, verb_list_sources }, + { "help", VERB_ANY, 1, 0, verb_help }, + { "metrics", VERB_ANY, VERB_ANY, 0, verb_metrics }, + { "describe-metrics", VERB_ANY, VERB_ANY, 0, verb_metrics }, + { "list-sources", VERB_ANY, 1, 0, verb_list_sources }, {} }; From bbb151da78cf1c5cf50953ead0a995ad69e612b6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 15:34:44 +0100 Subject: [PATCH 14/19] report: add --no-legend Like most of our other tools, add a --no-legend switch. --- man/systemd-report.xml | 1 + src/report/report.c | 37 +++++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 2b4dfeb51c..b8ca0a1a24 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -98,6 +98,7 @@ + diff --git a/src/report/report.c b/src/report/report.c index d59c3b9214..5f097a8b10 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -31,6 +31,7 @@ #define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */ static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; static char **arg_matches = NULL; @@ -443,7 +444,7 @@ static int metrics_output(Context *context) { return log_error_errno(r, "Failed to write JSON: %m"); } - if (context->n_metrics == 0) + if (context->n_metrics == 0 && arg_legend) log_info("No metrics collected."); return 0; @@ -467,12 +468,12 @@ static int metrics_output(Context *context) { return r; if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { - r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); if (r < 0) return r; } - if (!sd_json_format_enabled(arg_json_format_flags)) { + if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { if (table_isempty(table)) printf("No metrics available.\n"); else @@ -631,9 +632,10 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { } } - if (set_isempty(context.link_infos)) - log_info("No metrics sources found."); - else { + if (set_isempty(context.link_infos)) { + if (arg_legend) + log_info("No metrics sources found."); + } else { assert(context.event); r = sd_event_loop(context.event); @@ -700,12 +702,12 @@ static int verb_list_sources(int argc, char *argv[], void *userdata) { } if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { - r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); if (r < 0) return r; } - if (!sd_json_format_enabled(arg_json_format_flags)) { + if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { if (table_isempty(table)) printf("No sources available.\n"); else @@ -734,6 +736,7 @@ static int verb_help(int argc, char *argv[], void *userdata) { " -h --help Show this help\n" " --version Show package version\n" " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" " --user Connect to user service manager\n" " --system Connect to system service manager (default)\n" " --json=pretty|short\n" @@ -755,18 +758,20 @@ static int parse_argv(int argc, char *argv[]) { enum { ARG_VERSION = 0x100, ARG_NO_PAGER, + ARG_NO_LEGEND, ARG_USER, ARG_SYSTEM, ARG_JSON, }; static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "user", no_argument, NULL, ARG_USER }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "json", required_argument, NULL, ARG_JSON }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "user", no_argument, NULL, ARG_USER }, + { "system", no_argument, NULL, ARG_SYSTEM }, + { "json", required_argument, NULL, ARG_JSON }, {} }; @@ -787,6 +792,10 @@ static int parse_argv(int argc, char *argv[]) { arg_pager_flags |= PAGER_DISABLE; break; + case ARG_NO_LEGEND: + arg_legend = false; + break; + case ARG_USER: arg_runtime_scope = RUNTIME_SCOPE_USER; break; From fc5537635dbdef42c026a5990b4abc0e0fd7dea8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 15:35:13 +0100 Subject: [PATCH 15/19] report: fix log level of connection log messages Let's also rename the "metric_prefix" to "name", because it's actually the servce name, and by giving it this generic name we can use it reasonably in log messages. --- src/report/report.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/report/report.c b/src/report/report.c index 5f097a8b10..281491c577 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -56,7 +56,7 @@ typedef struct Context { typedef struct LinkInfo { Context *context; sd_varlink *link; - char *metric_prefix; + char *name; } LinkInfo; static LinkInfo* link_info_free(LinkInfo *li) { @@ -64,7 +64,7 @@ static LinkInfo* link_info_free(LinkInfo *li) { return NULL; sd_varlink_close_unref(li->link); - free(li->metric_prefix); + free(li->name); return mfree(li); } @@ -187,8 +187,8 @@ static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) { } /* Validate metric name matches the Varlink service it was found on */ - if (!metric_startswith_prefix(metric_name, li->metric_prefix)) { - log_debug("Metric name '%s' does not match service name '%s', skipping.", metric_name, li->metric_prefix); + if (!metric_startswith_prefix(metric_name, li->name)) { + log_debug("Metric name '%s' does not match service name '%s', skipping.", metric_name, li->name); return VERDICT_INVALID; } @@ -230,11 +230,11 @@ static int metrics_on_query_reply( if (error_id) { if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED)) - log_info("Varlink disconnected"); + log_warning("Varlink connection to '%s' disconnected prematurely, ignoring.", li->name); else if (streq(error_id, SD_VARLINK_ERROR_TIMEOUT)) - log_info("Varlink timed out"); + log_warning("Varlink connection to '%s' timed out, ignoring.", li->name); else - log_error("Varlink error: %s", error_id); + log_warning("Varlink error from '%s', ignoring: %s", li->name, error_id); goto finish; } @@ -306,10 +306,10 @@ static int metrics_call(Context *context, const char *name, const char *path) { *li = (LinkInfo) { .context = context, .link = sd_varlink_ref(vl), - .metric_prefix = strdup(name), + .name = strdup(name), }; - if (!li->metric_prefix) + if (!li->name) return log_oom(); if (set_ensure_put(&context->link_infos, &link_info_hash_ops, li) < 0) From cd961ea209ba5eaa25c3ebae8a10430a079d8edb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 15:48:46 +0100 Subject: [PATCH 16/19] report: use JSON-SEQ when outputing a series of json objects We do this in our other tools that output a large number of JSON objects in a potentially streamable way, hence do so here too. --- src/report/report.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/report/report.c b/src/report/report.c index 281491c577..3c2f89e002 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -586,6 +586,9 @@ static int verb_metrics(int argc, char *argv[], void *userdata) { assert(argc >= 1); assert(argv); + /* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */ + arg_json_format_flags |= SD_JSON_FORMAT_SEQ; + if (streq_ptr(argv[0], "metrics")) action = ACTION_LIST; else { From f3aff0c20a8b57519f7d6047da755b65fe921309 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 15:16:14 +0100 Subject: [PATCH 17/19] ci: add proper CI test for systemd-report --- test/units/TEST-74-AUX-UTILS.report.sh | 35 ++++++++++++++++++++++ test/units/TEST-74-AUX-UTILS.varlinkctl.sh | 9 ------ 2 files changed, 35 insertions(+), 9 deletions(-) create mode 100755 test/units/TEST-74-AUX-UTILS.report.sh diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh new file mode 100755 index 0000000000..3e1f2af114 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Unset $PAGER so we don't have to use --no-pager everywhere +export PAGER= + +systemd-report --help +systemd-report help +systemd-report --version +systemd-report --json=help + +systemd-report metrics +systemd-report metrics -j +systemd-report metrics --no-legend +systemd-report describe-metrics +systemd-report describe-metrics -j +systemd-report describe-metrics --no-legend +systemd-report list-sources +systemd-report list-sources -j +systemd-report list-sources --no-legend + +systemd-report metrics io +systemd-report metrics io.systemd piff +systemd-report metrics piff +systemd-report describe-metrics io +systemd-report describe-metrics io.systemd piff +systemd-report describe-metrics piff + +# test io.systemd.Network Metrics +varlinkctl info /run/systemd/report/io.systemd.Network +varlinkctl list-methods /run/systemd/report/io.systemd.Network +varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.List {} +varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.Describe {} diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh index d72acc5949..4308092240 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl.sh @@ -242,12 +242,3 @@ systemd-run --wait --pipe --user --machine testuser@ \ # test io.systemd.Unit in user manager systemd-run --wait --pipe --user --machine testuser@ \ varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}' - -# test report -systemd-report metrics - -# test io.systemd.Network Metrics -varlinkctl info /run/systemd/report/io.systemd.Network -varlinkctl list-methods /run/systemd/report/io.systemd.Network -varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.List {} -varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.Describe {} From 9277393d7b533bec3f560dd2093c12ea467e3fdb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 16:04:28 +0100 Subject: [PATCH 18/19] report: install systemd-report binary to /usr/lib/systemd/ for now The tool should not be considered stable, and those things we usually place in /usr/lib/systemd, and not in $PATH. We can move that to $PATH once we are confident it's gonna stay the way it is. --- src/report/meson.build | 2 +- test/units/TEST-74-AUX-UTILS.report.sh | 40 ++++++++++++++------------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/report/meson.build b/src/report/meson.build index ab1ac45acc..2813f9d033 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-2.1-or-later executables += [ - executable_template + { + libexec_template + { 'name' : 'systemd-report', 'public' : true, 'sources' : files('report.c'), diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index 3e1f2af114..876c5b2cad 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -6,27 +6,29 @@ set -o pipefail # Unset $PAGER so we don't have to use --no-pager everywhere export PAGER= -systemd-report --help -systemd-report help -systemd-report --version -systemd-report --json=help +REPORT=/usr/lib/systemd/systemd-report -systemd-report metrics -systemd-report metrics -j -systemd-report metrics --no-legend -systemd-report describe-metrics -systemd-report describe-metrics -j -systemd-report describe-metrics --no-legend -systemd-report list-sources -systemd-report list-sources -j -systemd-report list-sources --no-legend +"$REPORT" --help +"$REPORT" help +"$REPORT" --version +"$REPORT" --json=help -systemd-report metrics io -systemd-report metrics io.systemd piff -systemd-report metrics piff -systemd-report describe-metrics io -systemd-report describe-metrics io.systemd piff -systemd-report describe-metrics piff +"$REPORT" metrics +"$REPORT" metrics -j +"$REPORT" metrics --no-legend +"$REPORT" describe-metrics +"$REPORT" describe-metrics -j +"$REPORT" describe-metrics --no-legend +"$REPORT" list-sources +"$REPORT" list-sources -j +"$REPORT" list-sources --no-legend + +"$REPORT" metrics io +"$REPORT" metrics io.systemd piff +"$REPORT" metrics piff +"$REPORT" describe-metrics io +"$REPORT" describe-metrics io.systemd piff +"$REPORT" describe-metrics piff # test io.systemd.Network Metrics varlinkctl info /run/systemd/report/io.systemd.Network From 91ad0225e9e5cbf588e5dc3073b4b7276a5587c3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Feb 2026 14:10:15 +0100 Subject: [PATCH 19/19] update TODO --- TODO | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/TODO b/TODO index b413dc39e8..1edeaee48b 100644 --- a/TODO +++ b/TODO @@ -134,17 +134,12 @@ Features: * oci: support "data" in any OCI descriptor, not just manifest config. * report: - - should the list of metrics use JSON-SEQ? or maybe be wrapped in a json - array (the latter might be necessary, once we sign the combination) + - plug "facts" into systemd-report too, i.e. stuff that is more static, such as hostnames, ssh keys and so on. + - pass filtering hints to services, so that they can also be applied server-side, not just client side - metrics from pid1: suppress metrics form units that are inactive and have nothing to report - - how to plug facts into this? i.e. hostname, ssh keys, and so on - - switch to daan's suggested hierarchy? - - enforce naming rules: a backend can only report metrics with the prefix of its service name - - use that to optimize sorted report generation - - figure out report vs. metrics - - systemd-report: add prefix matching of metrics - add "hint-suppress-zero" flag (which suppresses all metrics which are zero) - add "hint-object" parameter (which only queries info about certain object) + - make systemd-report a varlink service * implement a varlink registry service, similar to the one of the reference implementation, backed by /run/varlink/registry/. Then, also implement