report: turn systemd-report itself into a varlink service

Add a Varlink API for requesting a report.
This commit is contained in:
Lennart Poettering
2026-06-19 05:23:18 +02:00
parent 9c3e443021
commit 7683c3281c
10 changed files with 274 additions and 55 deletions
+1
View File
@@ -35,6 +35,7 @@ enable systemd-report-basic.socket
enable systemd-report-cgroup.socket
enable systemd-report-files.socket
enable systemd-report-sign-plain.socket
enable systemd-report.socket
enable systemd-resolved.service
enable systemd-sysext.service
enable systemd-timesyncd.service
@@ -43,6 +43,7 @@
#include "varlink-io.systemd.PCRExtend.h"
#include "varlink-io.systemd.PCRLock.h"
#include "varlink-io.systemd.Repart.h"
#include "varlink-io.systemd.Report.h"
#include "varlink-io.systemd.Report.Signer.h"
#include "varlink-io.systemd.Report.Uploader.h"
#include "varlink-io.systemd.Resolve.h"
@@ -218,6 +219,7 @@ TEST(parse_format) {
&vl_interface_io_systemd_PCRExtend,
&vl_interface_io_systemd_PCRLock,
&vl_interface_io_systemd_Repart,
&vl_interface_io_systemd_Report,
&vl_interface_io_systemd_Report_Signer,
&vl_interface_io_systemd_Report_Uploader,
&vl_interface_io_systemd_Resolve,
+187 -49
View File
@@ -17,6 +17,7 @@
#include "recurse-dir.h"
#include "report.h"
#include "report-generate.h"
#include "report-sign.h"
#include "report-upload.h"
#include "runtime-scope.h"
#include "set.h"
@@ -25,6 +26,8 @@
#include "strv.h"
#include "time-util.h"
#include "varlink-idl-util.h"
#include "varlink-io.systemd.Report.h"
#include "varlink-util.h"
#include "verbs.h"
#include "web-util.h"
@@ -35,7 +38,6 @@
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
static char **arg_matches = NULL;
sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
char *arg_url = NULL;
char *arg_key = NULL;
@@ -45,7 +47,6 @@ char **arg_extra_headers = NULL;
usec_t arg_network_timeout_usec = TIMEOUT_USEC;
bool arg_sign = false;
STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_url, freep);
STATIC_DESTRUCTOR_REGISTER(arg_key, freep);
STATIC_DESTRUCTOR_REGISTER(arg_cert, freep);
@@ -73,6 +74,7 @@ static void context_done(Context *context) {
context->event = sd_event_unref(context->event);
context->link_infos = set_free(context->link_infos);
context->matches = strv_free(context->matches);
sd_json_variant_unref_many(context->metrics, context->n_metrics);
context->metrics = NULL;
context->n_metrics = 0;
@@ -193,13 +195,13 @@ static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) {
/* Check it against any specified matches */
bool matches;
if (strv_isempty(arg_matches))
if (strv_isempty(li->context->matches))
matches = true;
else {
matches = false;
/* Allow exact matches or prefix matches */
STRV_FOREACH(i, arg_matches)
STRV_FOREACH(i, li->context->matches)
if (streq(metric_name, *i) ||
metric_startswith_prefix(metric_name, *i)) {
matches = true;
@@ -493,28 +495,33 @@ static int output_collected(Context *context) {
return 0;
}
static int parse_metrics_matches(char **matches) {
static int parse_metrics_matches(char **input, char ***ret) {
int r;
STRV_FOREACH(i, matches) {
assert(ret);
_cleanup_strv_free_ char **matches = NULL;
STRV_FOREACH(i, input) {
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)
if (strv_extend(&matches, *i) < 0)
return log_oom();
}
strv_sort_uniq(arg_matches);
strv_sort_uniq(matches);
*ret = TAKE_PTR(matches);
return 0;
}
static bool test_service_matches(const char *service) {
static bool test_service_matches(const char *service, char **matches) {
assert(service);
if (strv_isempty(arg_matches))
if (strv_isempty(matches))
return true;
/* Only contact services whose name is either a prefix of any of the specified metrics families, or
@@ -527,7 +534,7 @@ static bool test_service_matches(const char *service) {
* 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) {
STRV_FOREACH(i, matches) {
if (streq(service, *i))
return true;
@@ -539,7 +546,7 @@ static bool test_service_matches(const char *service) {
return false;
}
static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
static int readdir_sources(char **matches, char **ret_directory, DirectoryEntries **ret) {
int r;
assert(ret_directory);
@@ -575,7 +582,7 @@ 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))
if (!test_service_matches(d->d_name, matches))
continue;
de->entries[m++] = *i;
@@ -589,6 +596,51 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
return m > 0;
}
static int context_collect_metrics(Context *context) {
int r;
/* Contacts all known metrics sources, issues the appropriate Varlink call on each and runs the
* event loop until all replies came in. Expects the caller to have set up context->event
* beforehand. The collected metrics end up in context->metrics. */
assert(context);
assert(context->event);
_cleanup_free_ DirectoryEntries *de = NULL;
_cleanup_free_ char *sources_path = NULL;
r = readdir_sources(context->matches, &sources_path, &de);
if (r < 0)
return r;
if (r == 0)
return 0;
FOREACH_ARRAY(i, de->entries, de->n_entries) {
struct dirent *d = *i;
if (set_size(context->link_infos) >= METRICS_LINKS_MAX) {
context->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);
}
context->n_contacted_sources = set_size(context->link_infos);
if (context->n_contacted_sources == 0)
return 0;
r = sd_event_loop(context->event);
if (r < 0)
return log_error_errno(r, "Failed to run event loop: %m");
return 1;
}
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", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS,
@@ -610,21 +662,14 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
* objects. In the report format, we return a single JSON object, so don't do this. */
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);
r = parse_metrics_matches(argv + 1, &context.matches);
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");
@@ -633,32 +678,13 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
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(sources_path, d->d_name);
if (!p)
return log_oom();
(void) call_collect(&context, d->d_name, p);
}
}
if (set_isempty(context.link_infos)) {
r = context_collect_metrics(&context);
if (r < 0)
return r;
if (r == 0) {
if (arg_legend)
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");
switch (action) {
case ACTION_LIST_METRICS:
@@ -681,10 +707,10 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
return r;
}
if (n_skipped_sources > 0)
if (context.n_skipped_sources > 0)
return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN),
"Too many metrics sources, only %u sources contacted, %zu sources skipped.",
set_size(context.link_infos), n_skipped_sources);
"Too many metrics sources, only %zu sources contacted, %zu sources skipped.",
context.n_contacted_sources, context.n_skipped_sources);
if (context.n_invalid_metrics > 0)
return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN),
"%zu metrics are not valid.",
@@ -706,7 +732,7 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user
_cleanup_free_ char *sources_path = NULL;
_cleanup_free_ DirectoryEntries *de = NULL;
r = readdir_sources(&sources_path, &de);
r = readdir_sources(/* matches= */ NULL, &sources_path, &de);
if (r < 0)
return r;
if (r > 0)
@@ -752,6 +778,110 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user
return 0;
}
static int vl_method_generate_internal(
sd_varlink *link,
sd_json_variant *parameters,
bool sign) {
int r;
assert(link);
assert(parameters);
_cleanup_strv_free_ char **input_matches = NULL;
static const sd_json_dispatch_field dispatch_table[] = {
{ "matches", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, 0, 0 },
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &input_matches);
if (r != 0)
return r;
_cleanup_(context_done) Context context = {
.action = ACTION_GENERATE,
};
r = parse_metrics_matches(input_matches, &context.matches);
if (r < 0)
return sd_varlink_error_invalid_parameter_name(link, "matches");
r = sd_event_new(&context.event);
if (r < 0)
return log_error_errno(r, "Failed to allocate event loop: %m");
r = context_collect_metrics(&context);
if (r < 0)
return r;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
r = context_build_report(&context, &report);
if (r < 0)
return r;
if (sign) {
/* Use compact JSON formatting (no pretty/color/seq flags), matching the on-the-wire format
* used for uploads. context_sign_report() adds the JSON-SEQ record separators itself. */
_cleanup_free_ char *s = NULL;
r = context_sign_report_as_string(&context, report, /* format_flags= */ 0, &s);
if (r < 0)
return r;
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_BASE64("reportData", s, strlen(s)));
}
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_VARIANT("report", report));
}
static int vl_method_generate(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
return vl_method_generate_internal(link, parameters, /* sign= */ false);
}
static int vl_method_generate_signed(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
return vl_method_generate_internal(link, parameters, /* sign= */ true);
}
static int vl_server(void) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL;
int r;
r = varlink_server_new(&vs, SD_VARLINK_SERVER_MYSELF_ONLY|SD_VARLINK_SERVER_ROOT_ONLY, /* userdata= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
r = sd_varlink_server_add_interface(vs, &vl_interface_io_systemd_Report);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
vs,
"io.systemd.Report.Generate", vl_method_generate,
"io.systemd.Report.GenerateSigned", vl_method_generate_signed);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = sd_varlink_server_loop_auto(vs);
if (r < 0)
return log_error_errno(r, "Failed to run Varlink event loop: %m");
return 0;
}
static int help(void) {
int r;
@@ -896,6 +1026,14 @@ static int run(int argc, char *argv[]) {
log_setup();
/* If invoked as a socket-activated Varlink service (Accept=yes), act as the io.systemd.Report
* server instead of running the command line interface. */
r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
if (r < 0)
return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
if (r > 0)
return vl_server();
r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
+2 -1
View File
@@ -29,8 +29,9 @@ typedef struct Context {
Action action;
sd_event *event;
Set *link_infos;
char **matches; /* Metric families to include, or NULL/empty for all */
sd_json_variant **metrics; /* Collected metrics for sorting */
size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
size_t n_metrics, n_skipped_metrics, n_invalid_metrics, n_contacted_sources, n_skipped_sources;
int upload_result;
struct iovec_wrapper upload_answer;
+1
View File
@@ -241,6 +241,7 @@ shared_sources = files(
'varlink-io.systemd.PCRExtend.c',
'varlink-io.systemd.PCRLock.c',
'varlink-io.systemd.Repart.c',
'varlink-io.systemd.Report.c',
'varlink-io.systemd.Report.Signer.c',
'varlink-io.systemd.Report.Uploader.c',
'varlink-io.systemd.Resolve.c',
+26
View File
@@ -0,0 +1,26 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.Report.h"
static SD_VARLINK_DEFINE_METHOD(
Generate,
SD_VARLINK_FIELD_COMMENT("Selects which metrics to include in the report, as an array of metric family names or prefixes thereof. If unset or empty all available metrics are included. This matches the [MATCH…] arguments of the systemd-report command line tool."),
SD_VARLINK_DEFINE_INPUT(matches, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The generated report as a JSON object. This mode does not sign the report, hence a precise binary formatting of the JSON data is not relevant."),
SD_VARLINK_DEFINE_OUTPUT(report, SD_VARLINK_OBJECT, 0));
static SD_VARLINK_DEFINE_METHOD(
GenerateSigned,
SD_VARLINK_FIELD_COMMENT("Selects which metrics to include in the report, as an array of metric family names or prefixes thereof. If unset or empty all available metrics are included. This matches the [MATCH…] arguments of the systemd-report command line tool."),
SD_VARLINK_DEFINE_INPUT(matches, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The generated, signed report in Base64. A precise binary formatting of the JSON data is important to authenticate the signature. This data contains a JSON-SEQ compliant stream of objects, the first being the report, the following ones signature objects."),
SD_VARLINK_DEFINE_OUTPUT(reportData, SD_VARLINK_STRING, 0));
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_Report,
"io.systemd.Report",
SD_VARLINK_INTERFACE_COMMENT("Frontend API for generating system reports. This interface is implemented by systemd-report, which aggregates the metrics exposed by the io.systemd.Metrics services linked into /run/systemd/report/."),
SD_VARLINK_SYMBOL_COMMENT("Generate a report and return it as a JSON object."),
&vl_method_Generate,
SD_VARLINK_SYMBOL_COMMENT("Generate a signed report and return it Base64 encoded."),
&vl_method_GenerateSigned);
+6
View File
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_Report;
+2
View File
@@ -633,6 +633,8 @@ units = [
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
'symlinks' : ['sysinit.target.wants/'],
},
{ 'file' : 'systemd-report.socket' },
{ 'file' : 'systemd-report@.service.in' },
{ 'file' : 'systemd-report-basic.socket' },
{ 'file' : 'systemd-report-basic@.service.in' },
{
+24
View File
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=System Report Socket
Documentation=man:systemd-report(1)
DefaultDependencies=no
Before=sockets.target
[Socket]
ListenStream=/run/systemd/io.systemd.Report
FileDescriptorName=varlink
SocketMode=0600
Accept=yes
MaxConnectionsPerSource=16
RemoveOnStop=yes
[Install]
WantedBy=sockets.target
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=System Report
Documentation=man:systemd-report(1)
DefaultDependencies=no
Conflicts=shutdown.target
Before=shutdown.target
[Service]
ExecStart={{LIBEXECDIR}}/systemd-report