From: Lennart Poettering Date: Mon, 15 Jun 2026 06:11:32 +0000 (+0200) Subject: report: add simple metrics provider that just reads files X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9c3e44302148bbd562d69a7b74c33be37fa12e12;p=thirdparty%2Fsystemd.git report: add simple metrics provider that just reads files --- diff --git a/man/rules/meson.build b/man/rules/meson.build index 68184c170b9..92238b717aa 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1185,6 +1185,11 @@ manpages = [ ['systemd-remount-fs.service', '8', ['systemd-remount-fs'], ''], ['systemd-repart', '8', ['systemd-repart.service'], 'ENABLE_REPART'], ['systemd-report', '1', [], ''], + ['systemd-report-files@.service', + '8', + ['systemd-report-files', + 'systemd-report-files.socket'], + ''], ['systemd-report-sign-plain@.service', '8', ['systemd-report-sign-plain', diff --git a/man/systemd-report-files@.service.xml b/man/systemd-report-files@.service.xml new file mode 100644 index 00000000000..9686e46cc12 --- /dev/null +++ b/man/systemd-report-files@.service.xml @@ -0,0 +1,72 @@ + + + + + + + + systemd-report-files@.service + systemd + + + + systemd-report-files@.service + 8 + + + + systemd-report-files@.service + systemd-report-files.socket + systemd-report-files + Report the contents of files as system report metrics + + + + systemd-report-files@.service + systemd-report-files.socket + /usr/lib/systemd/systemd-report-files + + + + Description + + systemd-report-files@.service is a metrics source for + systemd-report1. It + reports the contents of a configurable set of files as metrics, by implementing the + io.systemd.Metrics Varlink interface on a socket linked into the + /run/systemd/report/ directory. + + The files to report are picked up from report.files/ subdirectories of the + usual configuration directories, i.e. from + /etc/systemd/report.files/, + /run/systemd/report.files/, + /var/lib/systemd/report.files/, + /usr/local/lib/systemd/report.files/ and + /usr/lib/systemd/report.files/. Typically these directories contain symlinks pointing + to the actual files in the file system whose contents shall be reported. Entries with the same name in an + earlier directory override those in a later one. + + Each entry is reported as a metric named + io.systemd.Files.NAME, where + NAME is the name of the entry (which must be a valid metric field name). The + metric value is the verbatim contents of the file. Files that cannot be read (for example because a + symlink is dangling) are skipped, as are files whose contents are not valid UTF-8 text. + + Note that access to this service is unprivileged, but it runs privileged. This means take care when + symlinking files into the various report.files/ directories: this service might + provide read access even if the client does not have the required direct access rights itself. + + + + + + See Also + + systemd1 + systemd-report1 + + + + diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index fc9f55c5d3a..658fa1ce608 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -33,6 +33,7 @@ enable systemd-nsresourced.socket enable systemd-pstore.service enable systemd-report-basic.socket enable systemd-report-cgroup.socket +enable systemd-report-files.socket enable systemd-report-sign-plain.socket enable systemd-resolved.service enable systemd-sysext.service diff --git a/src/report/meson.build b/src/report/meson.build index c3f1cdadccb..c3e7cf959e4 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -27,6 +27,13 @@ executables += [ 'report-cgroup-server.c', ), }, + libexec_template + { + 'name' : 'systemd-report-files', + 'sources' : files( + 'report-files.c', + 'report-files-server.c', + ), + }, libexec_template + { 'name' : 'systemd-report-sign-plain', 'conditions' : [ diff --git a/src/report/report-files-server.c b/src/report/report-files-server.c new file mode 100644 index 00000000000..c1f90ccd7bb --- /dev/null +++ b/src/report/report-files-server.c @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink.h" + +#include "build.h" +#include "format-table.h" +#include "help-util.h" +#include "log.h" +#include "main-func.h" +#include "options.h" +#include "report-files.h" +#include "varlink-io.systemd.Metrics.h" +#include "varlink-util.h" + +static int vl_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL; + int r; + + r = varlink_server_new(&vs, /* flags= */ 0, /* 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_Metrics); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method_many( + vs, + "io.systemd.Metrics.List", vl_method_list_metrics, + "io.systemd.Metrics.Describe", vl_method_describe_metrics); + 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) { + _cleanup_(table_unrefp) Table *options = NULL; + int r; + + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + help_cmdline("[OPTIONS...]"); + help_abstract("Report the contents of files as system report metrics."); + help_section("Options"); + + r = table_print_or_warn(options); + if (r < 0) + return r; + + help_man_page_reference("systemd-report-files@.service", "8"); + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + int r; + + assert(argc >= 0); + assert(argv); + + OptionParser opts = { argc, argv }; + + FOREACH_OPTION_OR_RETURN(c, &opts) + switch (c) { + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + } + + if (option_parser_get_n_args(&opts) > 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "This program takes no arguments."); + + 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 log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "This program can only run as a Varlink service."); + return 1; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return vl_server(); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/report/report-files.c b/src/report/report-files.c new file mode 100644 index 00000000000..af2c38b5fe6 --- /dev/null +++ b/src/report/report-files.c @@ -0,0 +1,193 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-json.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "chase.h" +#include "conf-files.h" +#include "fd-util.h" +#include "fileio.h" +#include "log.h" +#include "metrics.h" +#include "path-util.h" +#include "report-files.h" +#include "string-util.h" +#include "strv.h" +#include "utf8.h" +#include "varlink-idl-util.h" + +/* Upper bounds, to protect against pathologically large directories or files. */ +#define REPORT_FILES_MAX 1024U +#define REPORT_FILE_SIZE_MAX (4U * 1024U * 1024U) + +/* The directories we look for files to report in. This is the usual CONF_PATHS() set (/etc/, /run/, + * /usr/local/lib/, /usr/lib/), plus an extra directory below /var/lib/ for persistent local additions. Files + * (typically symlinks to the actual files to report) dropped into any of these are reported as metrics, keyed + * by their name. Entries in earlier directories override identically named ones in later directories. */ +static const char* const report_files_dirs[] = { + "/etc/systemd/report.files", + "/run/systemd/report.files", + "/var/lib/systemd/report.files", + "/usr/local/lib/systemd/report.files", + "/usr/lib/systemd/report.files", + NULL, +}; + +static MetricFamily* metric_family_array_free(MetricFamily *families) { + if (!families) + return NULL; + + /* The array is NULL-name terminated. We own the name/description strings. */ + for (MetricFamily *mf = families; mf->name; mf++) { + free((char*) mf->name); + free((char*) mf->description); + } + + return mfree(families); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(MetricFamily*, metric_family_array_free); + +static int file_metric_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) { + int r; + + assert(mf && mf->name); + assert(link); + + /* Recover the file name from the metric family name: it is the part following the interface prefix. + * We look it up across our directories again (rather than caching the path found while building the + * metric list), and read the first instance we find, matching the precedence used at enumeration. */ + const char *field = startswith(mf->name, METRIC_IO_SYSTEMD_FILES_PREFIX); + assert(field); + + _cleanup_free_ char *buf = NULL, *discovered_path = NULL; + size_t size = 0; + STRV_FOREACH(d, report_files_dirs) { + _cleanup_free_ char *path = path_join(*d, field); + if (!path) + return log_oom(); + + _cleanup_free_ char *resolved = NULL; + _cleanup_close_ int fd = chase_and_open(path, /* root= */ NULL, CHASE_MUST_BE_REGULAR, O_RDONLY|O_CLOEXEC, &resolved); + if (fd == -ENOENT) /* Not in this directory (or dangling symlink): try the next one. */ + continue; + if (fd < 0) { + log_warning_errno(fd, "Failed to open '%s', skipping: %m", path); + return 0; + } + + r = read_full_file_full( + fd, + /* filename= */ NULL, + /* offset= */ UINT64_MAX, + REPORT_FILE_SIZE_MAX, + READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + &buf, + &size); + if (r < 0) { + log_warning_errno(r, "Failed to read '%s', skipping: %m", path); + return 0; + } + + discovered_path = TAKE_PTR(resolved); + break; + } + + if (!discovered_path) { + log_debug("File for metric '%s' unavailable, skipping.", mf->name); + return 0; + } + + /* Metric values are JSON strings, so we can only report text files. Skip anything that isn't valid, + * NUL-free UTF-8. */ + if (memchr(buf, 0, size) || !utf8_is_valid(buf)) { + log_debug("File for metric '%s' is not valid UTF-8 text, skipping.", mf->name); + return 0; + } + + return metric_build_send_string(mf, link, discovered_path, buf, /* fields= */ NULL); +} + +static int build_file_metrics(MetricFamily **ret) { + _cleanup_(metric_family_array_freep) MetricFamily *families = NULL; + _cleanup_strv_free_ char **files = NULL; + size_t n = 0; + int r; + + assert(ret); + + /* Enumerate the files to report across all our directories, deduplicated by name. The entry name is + * used as the metric field name. */ + r = conf_files_list_strv(&files, /* suffix= */ NULL, /* root= */ NULL, CONF_FILES_REGULAR, report_files_dirs); + if (r < 0) + return log_error_errno(r, "Failed to enumerate report files: %m"); + + STRV_FOREACH(f, files) { + _cleanup_free_ char *base = NULL; + r = path_extract_filename(*f, &base); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from '%s': %m", *f); + + /* The name becomes the metric field name, so it must be a valid one. */ + if (!varlink_idl_field_name_is_valid(base)) { + log_debug("Report file '%s' does not have a valid metric field name, skipping.", *f); + continue; + } + + if (n >= REPORT_FILES_MAX) { + log_warning("More than %u report files found, not reporting the rest.", REPORT_FILES_MAX); + break; + } + + _cleanup_free_ char *name = strjoin(METRIC_IO_SYSTEMD_FILES_PREFIX, base); + _cleanup_free_ char *description = strjoin("Contents of the '", base, "' report file"); + if (!name || !description) + return log_oom(); + + /* Room for the new entry plus the NULL-name terminator. */ + if (!GREEDY_REALLOC(families, n + 2)) + return log_oom(); + + families[n++] = (MetricFamily) { + .name = TAKE_PTR(name), + .description = TAKE_PTR(description), + .type = METRIC_FAMILY_TYPE_STRING, + .generate = file_metric_generate, + }; + families[n] = (MetricFamily) {}; /* terminator */ + } + + /* The metrics helpers expect a valid, terminated array even when empty. */ + if (!families) { + families = new0(MetricFamily, 1); + if (!families) + return log_oom(); + } + + *ret = TAKE_PTR(families); + return 0; +} + +int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + int r; + + _cleanup_(metric_family_array_freep) MetricFamily *families = NULL; + r = build_file_metrics(&families); + if (r < 0) + return r; + + return metrics_method_list(families, link, parameters, flags, /* userdata= */ NULL); +} + +int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + int r; + + _cleanup_(metric_family_array_freep) MetricFamily *families = NULL; + r = build_file_metrics(&families); + if (r < 0) + return r; + + return metrics_method_describe(families, link, parameters, flags, /* userdata= */ NULL); +} diff --git a/src/report/report-files.h b/src/report/report-files.h new file mode 100644 index 00000000000..0bdee8cb975 --- /dev/null +++ b/src/report/report-files.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +#define METRIC_IO_SYSTEMD_FILES_PREFIX "io.systemd.Files." + +int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); +int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); diff --git a/units/meson.build b/units/meson.build index af7fcdb8b2c..ae2e2c8ccf8 100644 --- a/units/meson.build +++ b/units/meson.build @@ -731,6 +731,8 @@ units = [ }, { 'file' : 'systemd-report-cgroup.socket' }, { 'file' : 'systemd-report-cgroup@.service.in' }, + { 'file' : 'systemd-report-files.socket' }, + { 'file' : 'systemd-report-files@.service.in' }, { 'file' : 'systemd-report-sign-plain.socket', 'conditions' : ['HAVE_OPENSSL'], diff --git a/units/systemd-report-files.socket b/units/systemd-report-files.socket new file mode 100644 index 00000000000..f37c508cf6b --- /dev/null +++ b/units/systemd-report-files.socket @@ -0,0 +1,26 @@ +# 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=Report Files Metrics Socket +Documentation=man:systemd-report-files@.service(8) +DefaultDependencies=no +Before=sockets.target shutdown.target +Conflicts=shutdown.target + +[Socket] +ListenStream=/run/systemd/report/io.systemd.Files +FileDescriptorName=varlink +SocketMode=0666 +Accept=yes +MaxConnectionsPerSource=16 +RemoveOnStop=yes + +[Install] +WantedBy=sockets.target diff --git a/units/systemd-report-files@.service.in b/units/systemd-report-files@.service.in new file mode 100644 index 00000000000..ae33d5d7358 --- /dev/null +++ b/units/systemd-report-files@.service.in @@ -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=Report Files Metrics +Documentation=man:systemd-report-files@.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +ExecStart={{LIBEXECDIR}}/systemd-report-files