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