report: add backend for reporting temperature sensors

This commit is contained in:
Lennart Poettering
2026-09-23 16:31:41 +02:00
committed by Luca Boccassi
parent 31bf94f87d
commit 186a8486fc
13 changed files with 527 additions and 0 deletions
+4
View File
@@ -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'],
+82
View File
@@ -0,0 +1,82 @@
<?xml version="1.0"?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-report-hwmon_.service" xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-report-hwmon@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-report-hwmon@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-report-hwmon@.service</refname>
<refname>systemd-report-hwmon.socket</refname>
<refname>systemd-report-hwmon</refname>
<refpurpose>Report hardware monitoring temperature sensor readings as system report metrics</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-report-hwmon@.service</filename></para>
<para><filename>systemd-report-hwmon.socket</filename></para>
<para><filename>/usr/lib/systemd/systemd-report-hwmon</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-report-hwmon@.service</command> is a metrics source for
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>. It
reports the current readings of the temperature sensors exposed by the kernel's hardware monitoring
(<literal>hwmon</literal>) subsystem, by implementing the <constant>io.systemd.Metrics</constant> Varlink
interface on a socket linked into the <filename>/run/systemd/report/</filename> directory.</para>
<para>All devices of the <literal>hwmon</literal> subsystem are enumerated, and for each of their
<filename>temp<replaceable>N</replaceable>_input</filename> attributes one
<literal>io.systemd.HWMon.TemperatureCelsius</literal> 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 <literal>coretemp.0</literal>, <literal>nvme0</literal> or
<literal>thermal_zone0</literal>). The following metric fields are set:</para>
<variablelist>
<varlistentry>
<term><varname>chip</varname></term>
<listitem><para>The name of the hwmon chip, i.e. the contents of its <filename>name</filename>
attribute, for example <literal>coretemp</literal> or <literal>nvme</literal>. Omitted if the
attribute cannot be read.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>sensor</varname></term>
<listitem><para>The sensor name within the chip, i.e.
<literal>temp<replaceable>N</replaceable></literal>. Always set.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>label</varname></term>
<listitem><para>The human readable sensor label, i.e. the contents of the
<filename>temp<replaceable>N</replaceable>_label</filename> attribute, for example
<literal>Package id 0</literal>. Omitted if the sensor carries no label.</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
<!-- Note: we do not document the command line switches here. The systemd-report-hwmon binary itself should
not be invoked by users, but only as a service. Use systemd-report(1) to acquire metrics. -->
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><ulink url="https://docs.kernel.org/hwmon/sysfs-interface.html">Naming and data format standards for sysfs files</ulink></member>
</simplelist></para>
</refsect1>
</refentry>
+1
View File
@@ -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
+7
View File
@@ -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(
+93
View File
@@ -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);
+224
View File
@@ -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<N>_input attributes, in millidegrees Celsius. Only the current reading is
* reported here. The various limits (temp<N>_min, temp<N>_max, temp<N>_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<N> 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<N>, 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);
}
+7
View File
@@ -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);
@@ -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
+31
View File
@@ -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<N>_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<N>
[[ "$(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
+3
View File
@@ -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"]
+2
View File
@@ -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'],
+29
View File
@@ -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
+43
View File
@@ -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