]> git.ipfire.org Git - collecty.git/commitdiff
sources: Add source to collect disk IO stats
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 21 Oct 2025 15:52:53 +0000 (15:52 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 21 Oct 2025 15:52:53 +0000 (15:52 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/daemon/sources.c
src/daemon/sources/disk.c [new file with mode: 0644]
src/daemon/sources/disk.h [new file with mode: 0644]
src/daemon/string.h

index 64fa34c513c62c969615a15f3296f84c0a29435c..4d84ac878e3ccd004df2d0112ee942ce0a4da93f 100644 (file)
@@ -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 \
index 2d585aeec0be74231048d3e61450d44d9d70ec5b..90d024f1eca48c4ba99c57129fc6143abc9ce2d4 100644 (file)
@@ -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 (file)
index 0000000..75e901c
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+#include <errno.h>
+#include <limits.h>
+
+#include <libudev.h>
+
+#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 (file)
index 0000000..65d03c0
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+#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 */
index 3caf5de8b5391bf697e6631e446726c923025892..c9610fe862ff4d35f8814f8a42b5a5bb2fa302a2 100644 (file)
@@ -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;