report: absorb "facts" into "metrics"

This gets rid of the duality in the cmdline interface and various
APIs. The general plan is collect both "facts" and "metrics" in a
single list. We have various producers which respond on the
io.systemd.Facts endpoint. Those will need to be adjusted to respond
to io.systemd.Metrics.

Cmdline interface:
  'metrics' (unchanged)
  'describe-metrics' → 'describe'
  'facts' → merged into 'metrics'
  'describe-facts' → merged into 'describe'
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-04-30 11:37:58 +02:00
parent 2e591bb1e7
commit e5687f689f
6 changed files with 27 additions and 265 deletions
+5 -5
View File
@@ -18,7 +18,7 @@
<refnamediv>
<refname>systemd-report</refname>
<refpurpose>Generate report of system facts and metrics</refpurpose>
<refpurpose>Generate report of system metrics</refpurpose>
</refnamediv>
<refsynopsisdiv>
@@ -33,7 +33,7 @@
<para><emphasis>Note: this command is experimental for now. While it is likely to become a regular
component of systemd, it might still change in behaviour and interface.</emphasis></para>
<para><command>systemd-report</command> requests facts and metrics from the system and writes them to
<para><command>systemd-report</command> requests metrics from the system and writes them to
standard output.</para>
</refsect1>
@@ -56,14 +56,14 @@
</varlistentry>
<varlistentry>
<term><command>describe-metrics</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
<term><command>describe</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
<listitem><para>Acquire a list of metric families from all local services providing them, and write
them to standard output. This returns primarily static information about metrics, their data types
and human readable description, without values.</para>
<para>Match expressions similar to those supported by <command>metrics</command> are supported for
<command>describe-metrics</command>, too.</para>
<para>Match expressions supported by <command>metrics</command> are supported by
<command>describe</command> too.</para>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
+1 -9
View File
@@ -59,18 +59,10 @@ static int build_json_report(Context *context, sd_json_variant **ret) {
usec_t ts = now(CLOCK_REALTIME);
int r;
const char *ident;
if (IN_SET(context->action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS))
ident = "metrics";
else if (IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS))
ident = "facts";
else
assert_not_reached();
r = sd_json_buildo(ret,
SD_JSON_BUILD_PAIR("timestamp",
SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))),
SD_JSON_BUILD_PAIR(ident,
SD_JSON_BUILD_PAIR("metrics",
SD_JSON_BUILD_VARIANT_ARRAY(context->metrics, context->n_metrics)));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
+10 -215
View File
@@ -28,8 +28,8 @@
#include "verbs.h"
#include "web-util.h"
#define METRICS_OR_FACTS_MAX 4096U
#define METRICS_OR_FACTS_LINKS_MAX 128U
#define METRICS_MAX 4096U
#define METRICS_LINKS_MAX 128U
#define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */
static PagerFlags arg_pager_flags = 0;
@@ -87,8 +87,6 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
static const char* const action_method_table[] = {
[ACTION_LIST_METRICS] = "io.systemd.Metrics.List",
[ACTION_DESCRIBE_METRICS] = "io.systemd.Metrics.Describe",
[ACTION_LIST_FACTS] = "io.systemd.Facts.List",
[ACTION_DESCRIBE_FACTS] = "io.systemd.Facts.Describe",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(action_method, Action);
@@ -249,7 +247,7 @@ static int on_query_reply(
goto finish;
}
if (context->n_metrics >= METRICS_OR_FACTS_MAX) {
if (context->n_metrics >= METRICS_MAX) {
context->n_skipped_metrics++;
goto finish;
}
@@ -436,107 +434,6 @@ static int output_collected_describe(Context *context, Table **ret) {
return 0;
}
static int facts_output_list(Context *context, Table **ret) {
int r;
assert(context);
assert(ret);
_cleanup_(table_unrefp) Table *table = table_new("family", "object", "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);
FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
struct {
const char *name;
const char *object;
sd_json_variant *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 },
{ "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 fact, skipping: %s", t);
continue;
}
r = table_add_many(
table,
TABLE_STRING, d.name,
TABLE_STRING, d.object,
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 facts_output_describe(Context *context, Table **ret) {
int r;
assert(context);
assert(ret);
_cleanup_(table_unrefp) Table *table = table_new("family", "description");
if (!table)
return log_oom();
table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
table_set_sort(table, (size_t) 0, (size_t) 1);
FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
struct {
const char *name;
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 },
{ "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 fact description, skipping: %s", t);
continue;
}
r = table_add_many(
table,
TABLE_STRING, d.name,
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 output_collected(Context *context) {
int r;
@@ -555,13 +452,8 @@ static int output_collected(Context *context) {
return log_error_errno(r, "Failed to write JSON: %m");
}
if (context->n_metrics == 0 && arg_legend) {
if (IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS))
log_info("No facts collected.");
else
log_info("No metrics collected.");
}
if (context->n_metrics == 0 && arg_legend)
log_info("No metrics collected.");
return 0;
}
@@ -577,14 +469,6 @@ static int output_collected(Context *context) {
r = output_collected_describe(context, &table);
break;
case ACTION_LIST_FACTS:
r = facts_output_list(context, &table);
break;
case ACTION_DESCRIBE_FACTS:
r = facts_output_describe(context, &table);
break;
default:
assert_not_reached();
}
@@ -598,12 +482,10 @@ static int output_collected(Context *context) {
}
if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) {
const char *type = IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS) ? "facts" : "metrics";
if (table_isempty(table))
printf("No %s available.\n", type);
printf("No metrics available.\n");
else
printf("\n%zu %s listed.\n", table_get_rows(table) - 1, type);
printf("\n%zu metrics listed.\n", table_get_rows(table) - 1);
}
return 0;
@@ -707,7 +589,7 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
VERB_FULL(verb_metrics, "metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_METRICS,
"Acquire list of metrics and their values");
VERB_FULL(verb_metrics, "describe-metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS,
VERB_FULL(verb_metrics, "describe", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS,
"Describe available metrics");
static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) {
Action action = data;
@@ -746,7 +628,7 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
FOREACH_ARRAY(i, de->entries, de->n_entries) {
struct dirent *d = *i;
if (set_size(context.link_infos) >= METRICS_OR_FACTS_LINKS_MAX) {
if (set_size(context.link_infos) >= METRICS_LINKS_MAX) {
n_skipped_sources++;
break;
}
@@ -792,93 +674,6 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
return 0;
}
VERB_FULL(verb_facts, "facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_FACTS,
"Acquire list of facts and their values");
VERB_FULL(verb_facts, "describe-facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_FACTS,
"Describe available facts");
static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) {
Action action = data;
int r;
assert(argc >= 1);
assert(argv);
assert(IN_SET(action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS));
/* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */
arg_json_format_flags |= SD_JSON_FORMAT_SEQ;
r = parse_metrics_matches(argv + 1);
if (r < 0)
return r;
_cleanup_(context_done) Context context = {
.action = action,
};
size_t n_skipped_sources = 0;
_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_OR_FACTS_LINKS_MAX) {
n_skipped_sources++;
break;
}
_cleanup_free_ char *p = path_join(sources_path, d->d_name);
if (!p)
return log_oom();
(void) call_collect(&context, d->d_name, p);
}
}
if (set_isempty(context.link_infos)) {
if (arg_legend)
log_info("No facts 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");
if (arg_url)
r = upload_collected(&context);
else
r = output_collected(&context);
if (r < 0)
return r;
}
if (n_skipped_sources > 0)
return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN),
"Too many facts sources, only %u sources contacted, %zu sources skipped.",
set_size(context.link_infos), n_skipped_sources);
if (context.n_invalid_metrics > 0)
return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN),
"%zu facts are not valid.",
context.n_invalid_metrics);
if (context.n_skipped_metrics > 0)
return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN),
"Too many facts, only %zu facts collected, %zu facts skipped.",
context.n_metrics, context.n_skipped_metrics);
return 0;
}
VERB_NOARG(verb_list_sources, "list-sources", "Show list of known metrics sources");
static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
@@ -955,7 +750,7 @@ static int help(void) {
(void) table_sync_column_widths(0, options, verbs);
printf("%s [OPTIONS...] COMMAND ...\n"
"\n%sAcquire metrics and facts from local sources.%s\n"
"\n%sAcquire metrics from local sources.%s\n"
"\n%sCommands:%s\n",
program_invocation_short_name,
ansi_highlight(),
+2 -5
View File
@@ -16,19 +16,16 @@ extern usec_t arg_network_timeout_usec;
typedef enum Action {
ACTION_LIST_METRICS,
ACTION_DESCRIBE_METRICS,
ACTION_LIST_FACTS,
ACTION_DESCRIBE_FACTS,
_ACTION_MAX,
_ACTION_INVALID = -EINVAL,
} Action;
/* The structure for collected "metrics" or "facts". The fields
* are prefixed with just "metrics" for brevity. */
/* The structure for collected "metrics". */
typedef struct Context {
Action action;
sd_event *event;
Set *link_infos;
sd_json_variant **metrics; /* Collected metrics or facts for sorting */
sd_json_variant **metrics; /* Collected metrics for sorting */
size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
struct iovec_wrapper upload_answer;
} Context;
@@ -35,8 +35,8 @@ class Handler(BaseHTTPRequestHandler):
print(f"JSON: {s if len(s := str(data)) < 80 else s[:40] + '…' + s[-40:]}")
if "metrics" not in data and "facts" not in data:
self.send_error(400, "Missing 'metrics' or 'facts' field")
if "metrics" not in data:
self.send_error(400, "Missing 'metrics' field")
return
response = json.dumps({"status": "ok"}).encode()
+7 -29
View File
@@ -16,9 +16,9 @@ REPORT=/usr/lib/systemd/systemd-report
"$REPORT" metrics
"$REPORT" metrics -j
"$REPORT" metrics --no-legend
"$REPORT" describe-metrics
"$REPORT" describe-metrics -j
"$REPORT" describe-metrics --no-legend
"$REPORT" describe
"$REPORT" describe -j
"$REPORT" describe --no-legend
"$REPORT" list-sources
"$REPORT" list-sources -j
"$REPORT" list-sources --no-legend
@@ -26,9 +26,9 @@ REPORT=/usr/lib/systemd/systemd-report
"$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
"$REPORT" describe io
"$REPORT" describe io.systemd piff
"$REPORT" describe piff
# test io.systemd.CGroup Metrics
systemctl start systemd-report-cgroup.socket
@@ -46,26 +46,6 @@ varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics
# Make sure the service for "system facts" is enabled
systemctl start systemd-report-basic.socket
# Test facts verbs
"$REPORT" facts
"$REPORT" facts -j
"$REPORT" facts --no-legend
"$REPORT" describe-facts
"$REPORT" describe-facts -j
"$REPORT" describe-facts --no-legend
# Test facts with match filters
"$REPORT" facts io
"$REPORT" facts io.systemd piff
"$REPORT" facts piff
"$REPORT" describe-facts io
"$REPORT" describe-facts io.systemd piff
"$REPORT" describe-facts piff
# Test facts via direct Varlink call on existing socket
varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.List {}
varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.Describe {}
# Test HTTP upload (plain http)
FAKE_SERVER=/usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py
CERTDIR=$(mktemp -d)
@@ -81,7 +61,6 @@ systemd-run -p Type=notify --unit=fake-report-server "$FAKE_SERVER"
systemctl status fake-report-server
"$REPORT" metrics --url=http://localhost:8089/
"$REPORT" facts --url=http://localhost:8089/
# Test HTTPS upload with generated TLS certificates
openssl req -x509 -newkey rsa:2048 -keyout "$CERTDIR/server.key" -out "$CERTDIR/server.crt" \
@@ -91,6 +70,5 @@ systemd-run -p Type=notify --unit=fake-report-server-tls \
"$FAKE_SERVER" --cert="$CERTDIR/server.crt" --key="$CERTDIR/server.key" --port=8090
systemctl status fake-report-server-tls
"$REPORT" metrics --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt"
"$REPORT" facts --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \
"$REPORT" metrics --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \
--extra-header='Authorization: Bearer magic string'