From 186a8486fc3c9b00906b199c1ba123ab2bd2d5e1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 19 Sep 2026 09:10:10 +0200 Subject: [PATCH] report: add backend for reporting temperature sensors --- man/rules/meson.build | 4 + man/systemd-report-hwmon@.service.xml | 82 +++++++ presets/90-systemd.preset | 1 + src/report/meson.build | 7 + src/report/report-hwmon-server.c | 93 ++++++++ src/report/report-hwmon.c | 224 ++++++++++++++++++ src/report/report-hwmon.h | 7 + .../units/TEST-74-AUX-UTILS.introspect-cli.sh | 1 + test/units/TEST-74-AUX-UTILS.report.sh | 31 +++ tools/command_ignorelist | 3 + units/meson.build | 2 + units/systemd-report-hwmon.socket | 29 +++ units/systemd-report-hwmon@.service.in | 43 ++++ 13 files changed, 527 insertions(+) create mode 100644 man/systemd-report-hwmon@.service.xml create mode 100644 src/report/report-hwmon-server.c create mode 100644 src/report/report-hwmon.c create mode 100644 src/report/report-hwmon.h create mode 100644 units/systemd-report-hwmon.socket create mode 100644 units/systemd-report-hwmon@.service.in diff --git a/man/rules/meson.build b/man/rules/meson.build index 12dc58c6da..7f5d5af96f 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1213,6 +1213,10 @@ manpages = [ '8', ['systemd-report-files', 'systemd-report-files.socket'], ''], + ['systemd-report-hwmon@.service', + '8', + ['systemd-report-hwmon', 'systemd-report-hwmon.socket'], + ''], ['systemd-report-sign-plain@.service', '8', ['systemd-report-sign-plain', 'systemd-report-sign-plain.socket'], diff --git a/man/systemd-report-hwmon@.service.xml b/man/systemd-report-hwmon@.service.xml new file mode 100644 index 0000000000..135c695c33 --- /dev/null +++ b/man/systemd-report-hwmon@.service.xml @@ -0,0 +1,82 @@ + + + + + + + + systemd-report-hwmon@.service + systemd + + + + systemd-report-hwmon@.service + 8 + + + + systemd-report-hwmon@.service + systemd-report-hwmon.socket + systemd-report-hwmon + Report hardware monitoring temperature sensor readings as system report metrics + + + + systemd-report-hwmon@.service + systemd-report-hwmon.socket + /usr/lib/systemd/systemd-report-hwmon + + + + Description + + systemd-report-hwmon@.service is a metrics source for + systemd-report1. It + reports the current readings of the temperature sensors exposed by the kernel's hardware monitoring + (hwmon) subsystem, by implementing the io.systemd.Metrics Varlink + interface on a socket linked into the /run/systemd/report/ directory. + + All devices of the hwmon subsystem are enumerated, and for each of their + tempN_input attributes one + io.systemd.HWMon.TemperatureCelsius gauge metric is reported, with the current + temperature in degrees Celsius as value. The metric object is the name of the device the hwmon chip + belongs to (for example coretemp.0, nvme0 or + thermal_zone0). The following metric fields are set: + + + + chip + The name of the hwmon chip, i.e. the contents of its name + attribute, for example coretemp or nvme. Omitted if the + attribute cannot be read. + + + + sensor + The sensor name within the chip, i.e. + tempN. Always set. + + + + label + The human readable sensor label, i.e. the contents of the + tempN_label attribute, for example + Package id 0. Omitted if the sensor carries no label. + + + + + + + + See Also + + systemd1 + systemd-report1 + Naming and data format standards for sysfs files + + + + diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index 563f4d831f..c76cdd511e 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -36,6 +36,7 @@ enable systemd-pstore.service enable systemd-report-basic.socket enable systemd-report-cgroup.socket enable systemd-report-files.socket +enable systemd-report-hwmon.socket enable systemd-report-sign-plain.socket enable systemd-report-sign-tpm2.socket enable systemd-report-sign-tsm.socket diff --git a/src/report/meson.build b/src/report/meson.build index c471fd00e6..8c1ef69bad 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -36,6 +36,13 @@ executables += [ 'report-files-server.c', ), }, + libexec_template + { + 'name' : 'systemd-report-hwmon', + 'export' : files( + 'report-hwmon.c', + 'report-hwmon-server.c', + ), + }, libexec_template + { 'name' : 'systemd-report-sign-plain', 'export' : files( diff --git a/src/report/report-hwmon-server.c b/src/report/report-hwmon-server.c new file mode 100644 index 0000000000..646c39270c --- /dev/null +++ b/src/report/report-hwmon-server.c @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-json.h" +#include "sd-varlink.h" + +#include "build.h" +#include "log.h" +#include "main-func.h" +#include "report-hwmon.h" +#include "varlink-io.systemd.Metrics.h" +#include "varlink-util.h" +#include "verbs.h" + +COMMAND( + "systemd-report-hwmon\0", + "Report hwmon sensor metrics.", + .man_pages = "systemd-report-hwmon@.service(8)\0", +); + +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_INHERIT_USERDATA, /* 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 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 command_print_help(); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); + } + + 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-hwmon.c b/src/report/report-hwmon.c new file mode 100644 index 0000000000..c557d67259 --- /dev/null +++ b/src/report/report-hwmon.c @@ -0,0 +1,224 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-device.h" +#include "sd-json.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "device-private.h" +#include "device-util.h" +#include "json-util.h" +#include "log.h" +#include "metrics.h" +#include "parse-util.h" +#include "report-hwmon.h" +#include "sort-util.h" +#include "string-util.h" + +/* The kernel's hwmon sysfs interface (see https://docs.kernel.org/hwmon/sysfs-interface.html) exposes + * temperature sensors as temp_input attributes, in millidegrees Celsius. Only the current reading is + * reported here. The various limits (temp_min, temp_max, temp_crit, …) and alarm flags are static + * configuration rather than measurements, and hence deliberately not reported as metrics. */ + +static int parse_temperature_index(const char *attr, unsigned *ret) { + const char *e; + unsigned n; + int r; + + assert(attr); + assert(ret); + + e = startswith(attr, "temp"); + if (!e) + return 0; + + const char *u = endswith(e, "_input"); + if (!u) + return 0; + + _cleanup_free_ char *idx = strndup(e, u - e); + if (!idx) + return -ENOMEM; + + r = safe_atou_full(idx, SAFE_ATO_REFUSE_PLUS_MINUS|SAFE_ATO_REFUSE_LEADING_ZERO|SAFE_ATO_REFUSE_LEADING_WHITESPACE, &n); + if (r < 0) + return 0; + + *ret = n; + return 1; +} + +static int hwmon_temperature_sensor_send( + const MetricFamily *mf, + sd_varlink *link, + sd_device *device, + const char *object, + const char *chip, + unsigned index) { + + int r; + + assert(mf); + assert(link); + assert(device); + assert(object); + + _cleanup_free_ char *sensor = NULL; + if (asprintf(&sensor, "temp%u", index) < 0) + return log_oom(); + + /* A faulted sensor may still return a (garbage) reading, hence check the fault flag explicitly. Most + * drivers do not expose the flag at all, in which case we assume all is fine. */ + _cleanup_free_ char *fault_attr = strjoin(sensor, "_fault"); + if (!fault_attr) + return log_oom(); + + r = device_get_sysattr_bool(device, fault_attr); + if (r > 0) { + log_device_debug(device, "Sensor %s reports a fault, skipping.", sensor); + return 0; + } + + _cleanup_free_ char *input_attr = strjoin(sensor, "_input"); + if (!input_attr) + return log_oom(); + + /* Drivers return a variety of errors for sensors that are absent, disabled or temporarily not + * readable (-ENODATA, -ENODEV, -EIO, -EAGAIN, …). None of these are worth more than a debug message: + * a sensor we cannot read simply produces no data point. */ + int millidegrees; + r = device_get_sysattr_int(device, input_attr, &millidegrees); + if (r < 0) { + log_device_debug_errno(device, r, "Failed to read sensor %s, ignoring: %m", sensor); + return 0; + } + + _cleanup_free_ char *label_attr = strjoin(sensor, "_label"); + if (!label_attr) + return log_oom(); + + const char *label = NULL; + r = device_get_sysattr_safe_string(device, label_attr, &label); + if (r < 0 && r != -ENOENT) + log_device_debug_errno(device, r, "Failed to read label of sensor %s, reporting as unlabelled: %m", sensor); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *fields = NULL; + r = sd_json_buildo( + &fields, + JSON_BUILD_PAIR_STRING_NON_EMPTY("chip", chip), + SD_JSON_BUILD_PAIR_STRING("sensor", sensor), + JSON_BUILD_PAIR_STRING_NON_EMPTY("label", label)); + if (r < 0) + return log_error_errno(r, "Failed to build metric fields: %m"); + + return metric_build_send_double(mf, link, object, (double) millidegrees / 1000, fields); +} + +static int hwmon_device_send(const MetricFamily *mf, sd_varlink *link, sd_device *device) { + int r; + + assert(mf); + assert(link); + assert(device); + + /* Collect the sensor indices first, and sort them: the sysattr enumeration order is not stable, but + * we want deterministic output. Indices are sparse (coretemp for example numbers sensors by core ID), + * hence scanning the attribute list beats probing temp1…tempN. */ + _cleanup_free_ unsigned *indices = NULL; + size_t n_indices = 0; + + FOREACH_DEVICE_SYSATTR(device, attr) { + unsigned n; + + r = parse_temperature_index(attr, &n); + if (r < 0) + return log_oom(); + if (r == 0) + continue; + + if (!GREEDY_REALLOC(indices, n_indices + 1)) + return log_oom(); + + indices[n_indices++] = n; + } + + if (n_indices == 0) + return 0; + + typesafe_qsort(indices, n_indices, cmp_unsigned); + + /* Identify the sensor by the device the hwmon chip belongs to (e.g. "coretemp.0", "nvme0", + * "thermal_zone0"), as the hwmon names are assigned in probe order and hence not stable across + * boots, while the chip name (e.g. "nvme") is not unique. */ + const char *object = NULL; + sd_device *parent; + r = sd_device_get_parent(device, &parent); + if (r >= 0) + r = sd_device_get_sysname(parent, &object); + if (r < 0) { + r = sd_device_get_sysname(device, &object); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to get hwmon device name: %m"); + } + + const char *chip = NULL; + r = device_get_sysattr_safe_string(device, "name", &chip); + if (r < 0) + log_device_debug_errno(device, r, "Failed to read hwmon chip name, ignoring: %m"); + + FOREACH_ARRAY(i, indices, n_indices) { + r = hwmon_temperature_sensor_send(mf, link, device, object, chip, *i); + if (r < 0) + return r; + } + + return 0; +} + +static int hwmon_temperature_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) { + int r; + + assert(mf && mf->name); + assert(link); + + /* Note that a fresh enumerator (and hence fresh sd_device objects) is used for each call, so that + * the sysattr cache in sd-device never hands out stale readings. */ + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + r = sd_device_enumerator_new(&e); + if (r < 0) + return log_error_errno(r, "Failed to allocate device enumerator: %m"); + + r = sd_device_enumerator_add_match_subsystem(e, "hwmon", /* match= */ true); + if (r < 0) + return log_error_errno(r, "Failed to add hwmon subsystem match to enumerator: %m"); + + (void) sd_device_enumerator_allow_uninitialized(e); + + FOREACH_DEVICE(e, device) { + r = hwmon_device_send(mf, link, device); + if (r < 0) + return r; + } + + return 0; +} + +static const MetricFamily hwmon_metric_family_table[] = { + /* Keep metrics ordered alphabetically */ + { + "io.systemd.HWMon.TemperatureCelsius", + "Per hwmon sensor metric: current temperature in degrees Celsius " + "(object=parent device, chip=hwmon chip name, sensor=temp, label=sensor label if any)", + METRIC_FAMILY_TYPE_GAUGE, + .generate = hwmon_temperature_generate, + }, + {} +}; + +int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return metrics_method_describe(hwmon_metric_family_table, link, parameters, flags, userdata); +} + +int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return metrics_method_list(hwmon_metric_family_table, link, parameters, flags, userdata); +} diff --git a/src/report/report-hwmon.h b/src/report/report-hwmon.h new file mode 100644 index 0000000000..152f5bc846 --- /dev/null +++ b/src/report/report-hwmon.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "forward.h" + +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/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh index 00bde6b6b7..7ff9ec4f14 100755 --- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -110,6 +110,7 @@ INTROSPECTABLE=( systemd-report-basic systemd-report-cgroup systemd-report-files + systemd-report-hwmon systemd-report-sign-plain systemd-report-sign-tpm2 systemd-report-sign-tsm diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index 5cf8cb7ac8..a833cc7c17 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -62,6 +62,37 @@ net_metrics="$(varlinkctl call --more --json=short /run/systemd/report/io.system echo "$net_metrics" | grep '"name":"io.systemd.Network.Address"' | grep '"object":"lo"' | grep '"value":"192.0.2.1/32"' | grep '"family":"ipv4"' >/dev/null ip address del 192.0.2.1/32 dev lo +# test io.systemd.HWMon Metrics +systemctl start systemd-report-hwmon.socket +varlinkctl info /run/systemd/report/io.systemd.HWMon +varlinkctl list-methods /run/systemd/report/io.systemd.HWMon +hwmon_describe="$(varlinkctl call --more /run/systemd/report/io.systemd.HWMon io.systemd.Metrics.Describe {})" +echo "$hwmon_describe" | jq --seq -r 'select(.name == "io.systemd.HWMon.TemperatureCelsius") | .type' | grep -wx gauge >/dev/null +# An empty result set is signalled through the io.systemd.Metrics.NoSuchMetric sentinel error, which is what +# VMs without any hwmon devices produce, hence treat it as success. +hwmon_metrics="$(varlinkctl call --more --graceful=io.systemd.Metrics.NoSuchMetric /run/systemd/report/io.systemd.HWMon io.systemd.Metrics.List {})" +# At most one row per readable, non-faulted temp_input attribute in sysfs. A sensor may become +# transiently unreadable between the two samples, hence only check the bound. VMs typically expose no hwmon +# devices at all, in which case both counts are zero. +hwmon_expected=0 +for f in /sys/class/hwmon/hwmon*/temp*_input; do + hwmon_fault="${f%_input}_fault" + if [[ -f "$hwmon_fault" ]] && [[ "$(cat "$hwmon_fault")" == 1 ]]; then + continue + fi + if cat "$f" >/dev/null 2>&1; then + hwmon_expected=$((hwmon_expected + 1)) + fi +done +hwmon_reported="$(echo "$hwmon_metrics" | jq --seq -r 'select(.name == "io.systemd.HWMon.TemperatureCelsius") | .fields.sensor' | wc -l)" +[[ "$hwmon_reported" -le "$hwmon_expected" ]] +if [[ "$hwmon_reported" -gt 0 ]]; then + # Every row must carry a numeric value, an object and a sensor field of the form temp + [[ "$(echo "$hwmon_metrics" | jq --seq -r 'select(.name == "io.systemd.HWMon.TemperatureCelsius") | ((.value | type) == "number") and ((.object | type) == "string") and (.fields.sensor | test("^temp[0-9]+$")) | tostring' | sort -u)" == "true" ]] +fi +"$REPORT" describe io.systemd.HWMon +"$REPORT" metrics io.systemd.HWMon + # test io.systemd.Basic Metrics # ensure the socket is running, as some distros don't enable it by default systemctl start systemd-report-basic.socket diff --git a/tools/command_ignorelist b/tools/command_ignorelist index 93ee6082d0..e4c06b1c5c 100644 --- a/tools/command_ignorelist +++ b/tools/command_ignorelist @@ -585,3 +585,6 @@ loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secu loader.conf.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="secure-boot-enroll-action"]/listitem/variablelist/varlistentry[term="shutdown"] varlinkctl.xml ./refsect1[title="Options"]/variablelist/varlistentry[term="--system"] systemd.network.xml ./refsect1[title="[ModemManager] Section Options"]/variablelist/varlistentry[term="SimpleConnectProperties="] +systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="chip"] +systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="sensor"] +systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="label"] diff --git a/units/meson.build b/units/meson.build index 9f8f8d0e41..eba7a53215 100644 --- a/units/meson.build +++ b/units/meson.build @@ -796,6 +796,8 @@ units = [ { 'file' : 'systemd-report-cgroup@.service.in' }, { 'file' : 'systemd-report-files.socket' }, { 'file' : 'systemd-report-files@.service.in' }, + { 'file' : 'systemd-report-hwmon.socket' }, + { 'file' : 'systemd-report-hwmon@.service.in' }, { 'file' : 'systemd-report-sign-plain.socket', 'conditions' : ['HAVE_OPENSSL'], diff --git a/units/systemd-report-hwmon.socket b/units/systemd-report-hwmon.socket new file mode 100644 index 0000000000..910e651908 --- /dev/null +++ b/units/systemd-report-hwmon.socket @@ -0,0 +1,29 @@ +# 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=Hardware Monitor Report Varlink Socket +Documentation=man:systemd-report-hwmon@.service(8) +DefaultDependencies=no +Before=sockets.target shutdown.target +Conflicts=shutdown.target + +[Socket] +ListenStream=/run/systemd/report/io.systemd.HWMon +FileDescriptorName=varlink +SocketMode=0666 +Accept=yes +MaxConnectionsPerSource=16 +RemoveOnStop=yes +XAttrEntryPoint=user.varlink=entrypoint +XAttrListen=user.varlink=listen +XAttrAccept=user.varlink=server + +[Install] +WantedBy=sockets.target diff --git a/units/systemd-report-hwmon@.service.in b/units/systemd-report-hwmon@.service.in new file mode 100644 index 0000000000..11faad70dc --- /dev/null +++ b/units/systemd-report-hwmon@.service.in @@ -0,0 +1,43 @@ +# 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=Hardware Monitor Report Service +Documentation=man:systemd-report-hwmon@.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +CapabilityBoundingSet= +DeviceAllow= +DynamicUser=yes +IPAddressDeny=any +LockPersonality=yes +MemoryDenyWriteExecute=yes +PrivateDevices=yes +PrivateIPC=yes +PrivateNetwork=yes +PrivateTmp=disconnected +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +RuntimeMaxSec=1min +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service +ExecStart={{LIBEXECDIR}}/systemd-report-hwmon