From: Michael Tremer Date: Tue, 21 Oct 2025 15:52:53 +0000 (+0000) Subject: sources: Add source to collect disk IO stats X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4d61c6966c39b93376eadd8c4958cef89c46bf7e;p=collecty.git sources: Add source to collect disk IO stats Signed-off-by: Michael Tremer --- diff --git a/Makefile.am b/Makefile.am index 64fa34c..4d84ac8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -153,6 +153,8 @@ dist_telemetryd_SOURCES = \ src/daemon/sources/contextswitches.h \ src/daemon/sources/df.c \ src/daemon/sources/df.h \ + src/daemon/sources/disk.c \ + src/daemon/sources/disk.h \ src/daemon/sources/hostapd.c \ src/daemon/sources/hostapd.h \ src/daemon/sources/ipfrag4.c \ diff --git a/src/daemon/sources.c b/src/daemon/sources.c index 2d585ae..90d024f 100644 --- a/src/daemon/sources.c +++ b/src/daemon/sources.c @@ -32,6 +32,7 @@ #include "sources/conntrack.h" #include "sources/contextswitches.h" #include "sources/df.h" +#include "sources/disk.h" #include "sources/hostapd.h" #include "sources/ipfrag4.h" #include "sources/loadavg.h" @@ -56,6 +57,7 @@ static const td_source_impl* source_impls[] = { &conntrack_source, &contextswitches_source, &df_source, + &disk_source, &hostapd_source, &ipfrag4_source, &loadavg_source, diff --git a/src/daemon/sources/disk.c b/src/daemon/sources/disk.c new file mode 100644 index 0000000..75e901c --- /dev/null +++ b/src/daemon/sources/disk.c @@ -0,0 +1,338 @@ +/*############################################################################# +# # +# telemetryd - The IPFire Telemetry Collection Service # +# Copyright (C) 2025 IPFire Development Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +#############################################################################*/ + +#include +#include + +#include + +#include "../ctx.h" +#include "../file.h" +#include "../parse.h" +#include "../source.h" +#include "../string.h" +#include "disk.h" + +// Ignore these devices +const char* ignored_devices[] = { + "/dev/loop", + "/dev/ram", + "/dev/zram", + NULL, +}; + +static int is_ignored(const char* node) { + for (const char** n = ignored_devices; *n; n++) { + if (td_string_startswith(node, *n)) + return 1; + } + + return 0; +} + +typedef struct td_disk { + uint64_t read_ios; + uint64_t read_merges; + uint64_t read_sectors; + uint64_t read_ticks; + uint64_t write_ios; + uint64_t write_merges; + uint64_t write_sectors; + uint64_t write_ticks; + uint64_t in_flight; + uint64_t io_ticks; + uint64_t time_in_queue; + uint64_t discard_ios; + uint64_t discard_merges; + uint64_t discard_sectors; + uint64_t discard_ticks; +} td_disk; + +static int __disk_read_stat(td_ctx* ctx, td_file* file, unsigned long lineno, + char* line, size_t length, void* data) { + unsigned int token = 0; + uint64_t value = 0; + int r; + + td_disk* disk = data; + + // We only care about one line (there should not be more) + if (lineno > 1) + return -EINVAL; + + /* + https://www.kernel.org/doc/Documentation/block/stat.txt + + Name units description + ---- ----- ----------- + read I/Os requests number of read I/Os processed + read merges requests number of read I/Os merged with in-queue I/O + read sectors sectors number of sectors read + read ticks milliseconds total wait time for read requests + write I/Os requests number of write I/Os processed + write merges requests number of write I/Os merged with in-queue I/O + write sectors sectors number of sectors written + write ticks milliseconds total wait time for write requests + in_flight requests number of I/Os currently in flight + io_ticks milliseconds total time this block device has been active + time_in_queue milliseconds total wait time for all requests + discard I/Os requests number of discard I/Os processed + discard merges requests number of discard I/Os merged with in-queue I/O + discard sectors sectors number of sectors discarded + discard ticks milliseconds total wait time for discard requests + */ + + while (*line) { + r = td_parse_uint64(&line, &value); + if (r < 0) + return r; + + switch (token++) { + case 0: + disk->read_ios = value; + break; + + case 1: + disk->read_merges = value; + break; + + case 2: + disk->read_sectors = value; + break; + + case 3: + disk->read_ticks = value; + break; + + case 4: + disk->write_ios = value; + break; + + case 5: + disk->write_merges = value; + break; + + case 6: + disk->write_sectors = value; + break; + + case 7: + disk->write_ticks = value; + break; + + case 8: + disk->in_flight = value; + break; + + case 9: + disk->io_ticks = value; + break; + + case 10: + disk->time_in_queue = value; + break; + + case 11: + disk->discard_ios = value; + break; + + case 12: + disk->discard_merges = value; + break; + + case 13: + disk->discard_sectors = value; + break; + + case 14: + disk->discard_ticks = value; + break; + + default: + return 0; + } + } + + return 0; +} + +static int disk_read_stat(td_ctx* ctx, const char* syspath, td_disk* disk) { + td_file* file = NULL; + char path[PATH_MAX]; + int r; + + // Make the path + r = td_string_format(path, "%s/stat", syspath); + if (r < 0) + return r; + + // Open the stat file + r = td_file_open_path(&file, ctx, path); + if (r < 0) { + ERROR(ctx, "Failed to open %s: %s\n", path, strerror(-r)); + goto ERROR; + } + + // Walk through all lines + r = td_file_walk(file, __disk_read_stat, disk); + +ERROR: + if (file) + td_file_unref(file); + + return r; +} + +// Process a single device +static int disk_heartbeat_device(td_ctx* ctx, td_source* source, struct udev_device* dev) { + const char* syspath = NULL; + const char* serial = NULL; + const char* node = NULL; + td_disk disk = {}; + int r; + + // Fetch the device node + node = udev_device_get_devnode(dev); + if (!node) + return -ENOENT; + + // Should we ignore this device? + if (is_ignored(node)) + return 0; + + // Fetch the serial + serial = udev_device_get_property_value(dev, "ID_SERIAL"); + if (!serial) { + ERROR(ctx, "Ignoring block device %s without a serial\n", node); + return 0; + } + + // Fetch the sys path + syspath = udev_device_get_syspath(dev); + + // Parse stats + r = disk_read_stat(ctx, syspath, &disk); + if (r < 0) + return r; + + // Submit stats + return td_source_submit(source, serial, + "%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu", + disk.read_ios, disk.read_merges, disk.read_sectors, disk.read_ticks, + disk.write_ios, disk.write_merges, disk.write_sectors, disk.write_ticks, + disk.in_flight, disk.io_ticks, disk.time_in_queue, + disk.discard_ios, disk.discard_merges, disk.discard_sectors, disk.discard_ticks + ); +} + +static int disk_heartbeat(td_ctx* ctx, td_source* source) { + struct udev_enumerate* enumerate = NULL; + struct udev_list_entry* devices = NULL; + struct udev_list_entry* device = NULL; + struct udev_device* dev = NULL; + struct udev* udev = NULL; + const char* path = NULL; + int r; + + // Create a new enumerator + enumerate = udev_enumerate_new(udev); + if (!enumerate) { + ERROR(ctx, "Failed to create an udev enumerator: %m\n"); + r = -errno; + goto ERROR; + } + + // Match all block devices + r = udev_enumerate_add_match_subsystem(enumerate, "block"); + if (r < 0) + goto ERROR; + + // Only match disks (no partitions, etc.) + r = udev_enumerate_add_match_property(enumerate, "DEVTYPE", "disk"); + if (r < 0) + goto ERROR; + + // Scan for devices + r = udev_enumerate_scan_devices(enumerate); + if (r < 0) + goto ERROR; + + // Fetch all devices + devices = udev_enumerate_get_list_entry(enumerate); + + // Iterate through all devices + udev_list_entry_foreach(device, devices) { + // Fetch the path to the object + path = udev_list_entry_get_name(device); + if (!path) + continue; + + // Fetch the udev device + dev = udev_device_new_from_syspath(udev, path); + if (!dev) + continue; + + // Process the device + r = disk_heartbeat_device(ctx, source, dev); + if (r < 0) + goto ERROR; + + // Free the device + udev_device_unref(dev); + dev = NULL; + } + +ERROR: + if (enumerate) + udev_enumerate_unref(enumerate); + if (dev) + udev_device_unref(dev); + if (udev) + udev_unref(udev); + + return r; +} + +const td_source_impl disk_source = { + .name = "disk", + + // RRD Data Sources + .rrd_dss = { + { "read_ios", "DERIVE", 0, -1 }, + { "read_merges", "DERIVE", 0, -1 }, + { "read_sectors", "DERIVE", 0, -1 }, + { "read_ticks", "DERIVE", 0, -1 }, + { "write_ios", "DERIVE", 0, -1 }, + { "write_merges", "DERIVE", 0, -1 }, + { "write_sectors", "DERIVE", 0, -1 }, + { "write_ticks", "DERIVE", 0, -1 }, + { "in_flight", "DERIVE", 0, -1 }, + { "io_ticks", "DERIVE", 0, -1 }, + { "time_in_queue", "DERIVE", 0, -1 }, + { "discard_ios", "DERIVE", 0, -1 }, + { "discard_merges", "DERIVE", 0, -1 }, + { "discard_sectors", "DERIVE", 0, -1 }, + { "discard_ticks", "DERIVE", 0, -1 }, + { NULL }, + }, + + // Methods + .heartbeat = disk_heartbeat, +}; diff --git a/src/daemon/sources/disk.h b/src/daemon/sources/disk.h new file mode 100644 index 0000000..65d03c0 --- /dev/null +++ b/src/daemon/sources/disk.h @@ -0,0 +1,28 @@ +/*############################################################################# +# # +# telemetryd - The IPFire Telemetry Collection Service # +# Copyright (C) 2025 IPFire Development Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +#############################################################################*/ + +#ifndef TELEMETRY_SOURCE_DISK_H +#define TELEMETRY_SOURCE_DISK_H + +#include "../source.h" + +extern const td_source_impl disk_source; + +#endif /* TELEMETRY_SOURCE_DISK_H */ diff --git a/src/daemon/string.h b/src/daemon/string.h index 3caf5de..c9610fe 100644 --- a/src/daemon/string.h +++ b/src/daemon/string.h @@ -121,6 +121,14 @@ static inline int __td_string_setn(char* s, const size_t length, const char* val return l; } +static inline int td_string_startswith(const char* s, const char* prefix) { + // Validate input + if (!s || !prefix) + return -EINVAL; + + return !strncmp(s, prefix, strlen(prefix)); +} + static inline void td_string_lstrip(char* s) { if (!s) return;