--- /dev/null
+/*#############################################################################
+# #
+# 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,
+};