Add node metrics receiver for host-level system metrics - #364
Add node metrics receiver for host-level system metrics#364neeme-praks-sympower wants to merge 1 commit into
Conversation
b863080 to
4846cea
Compare
Introduce a new opt-in receiver (`--features node_metrics_receiver`) that periodically scrapes system metrics from Linux /proc and /sys interfaces and emits them as OpenTelemetry metrics using Prometheus node_exporter naming conventions. 19 collectors, 18 of them enabled by default: - CPU, load average, memory, network, filesystem, uname, time - Kernel stat counters, processes, disk I/O, vmstat - Network statistics (netstat + SNMP), socket statistics - File descriptor stats, CPU frequency, thermal zones and cooling devices - NVMe device info, hardware monitoring sensors (hwmon) - Textfile collector for custom Prometheus-format .prom files (opt-in) The scrape runs on a blocking thread and races against cancellation, so a slow or wedged filesystem cannot stall the runtime or overrun the agent's shutdown budget. Because tokio cannot abort a blocking task, a timed-out scrape is retained and not re-issued until it completes, which keeps a hung statfs from leaking a blocking thread every interval into the pool shared with the exporters. Network filesystem types are excluded by default for the same reason, while local FUSE mounts keep reporting. The pipeline send is bounded, so a backed-up exporter drops a batch rather than silently stopping all scraping, and a panicking collector is logged and counted rather than taking the agent down with it. Metrics are grouped by name and type, with duplicate label sets dropped, so a single OTLP metric never carries conflicting data points. Boot time is re-read every scrape, because the kernel's btime moves when the wall clock is stepped — a device without an RTC boots near the epoch and jumps when NTP syncs — while the start time stamped on cumulative counters stays latched so a clock step is not reported as a counter reset. The receiver's flags follow the `--<name>-receiver-*` convention used by the other receivers, durations are parsed with humantime like the rest of the CLI, and its internal telemetry reuses the shared rotel_receiver_accepted/refused_metric_points counters. Metric names, help strings, the default filesystem exclusion lists, the hwmon chip-naming approach and the textfile collector's semantics are derived from Prometheus node_exporter (Apache-2.0); this is recorded in the collector module and in each derived submodule. The CI test job now enables this feature, and the 32-bit ARM job builds it, since libc's statfs field widths differ there and AtomicU64 needs portable-atomic on targets without native 64-bit atomics. Without that, none of this code is compiled on a pull request and feature-gated code silently rots against dependency changes.
4846cea to
ebb0ea6
Compare
|
Thanks for the contribution! Do you have any remaining work on this or is it ready? I'm sharing my own agent's feedback below if it's helpful, but don't feel it's a requirement. I do wonder if "node metrics" could be confusing with node.js, instead of "host metrics" or "system metrics". However, I guess "host metrics" could be confused with the hostmetricsreceiver in OTel, especially since this is different. I don't have a strong opinion on the name and node metrics would be fine, curious what you think. 1. The textfile per-scrape sample cap is bypassable (verified)The PR description says "at most 100k samples are taken per scrape." That bound doesn't hold. In I confirmed this with a probe test: 5,000 The textfile directory is often the least-trusted input the collector touches. Suggested fix: count every metric pushed for a file against the cap (not just parsed samples), and add a file-count limit alongside the size limit. 2. A wedged scrape can prevent process exitYou disclose this, but I want to underline how it presents operationally. Two options: build the runtime explicitly with 3.
|
Adds an opt-in
node_metricsreceiver that periodically scrapes host system metrics from Linux/procand/sysand emits them as OTLP metrics, using Prometheusnode_exportermetric naming.This is the "Node metrics scraper" item from #299. It is gated behind the
node_metrics_receivercargo feature (off by default), so default builds are unaffected. The feature pulls in no new dependencies.Usage
Collectors
19 collectors, 18 enabled by default, each individually toggleable:
/proc/net/netstat+/proc/net/snmp), socket statistics (sockstat+sockstat6).promfiles (opt-in, needs a directory)Full option table and the emitted metric list are in README.md.
Attribution
Metric names, help strings, the default filesystem-type and mount-point exclusion lists, the virtual-device and partition heuristics, the hwmon chip-naming approach (from its
collector/hwmon.go) and the textfile collector's semantics are derived from Prometheusnode_exporter(Apache-2.0, copyright The Prometheus Authors). There is a notice incollector/mod.rsenumerating this per submodule, plus a pointer in each derived file so relocating one cannot lose it. Both projects are Apache-2.0, so this is licence-compatible — flagging it explicitly rather than leaving you to discover it. Say the word if you would rather have a top-levelNOTICEfile.Design notes for review
sysinfo(as originally floated in Interest in Rotel for embedded IoT + planned contributions #299): zero added dependencies, exactnode_exporterparity, and coverage of Linux-only sources (hwmon,thermal_zone,nvme,sockstat) thatsysinfodoes not expose.spawn_blockingand is raced against the cancellation token, as is the pipeline send. Since tokio cannot abort a blocking task, a timed-out scrape is retained and no new scrape starts until it finishes — otherwise a hungstatfswould leak one blocking thread per interval into the pool shared with the exporters. Note the honest limitation: this bounds the scrape loop, not process exit — a wedged blocking task can still delay runtime shutdown, since#[tokio::main]waits for in-flight blocking tasks.--node-metrics-receiver-rootfs-path, not just a bind-mounted/proc. The mount table is read from/proc/1/mountsrather than/proc/self/mounts(the latter is always the reading process's own mount namespace, whatever procfs it comes from), andstatfsis called on the rootfs-composed path while the label keeps the host's own path. Without this the receiver would report a container's own bind mounts labelled as though they were the host's filesystems — plausible, well-labelled and wrong. The README recipe was corrected accordingly.btimefrom a signed value as%llu, so a machine whose clock is near the epoch can print a wrapped value; accepting it published a boot time ~580 billion years out and latched it into every counter's start time.sensoris the raw sysfs name (temp1), with the human-readable text published separately asnode_hwmon_sensor_labeland joinable onchip+sensor. Using the label text as the identity would collide when two sensors on one chip share the same text, and would move series whenever a driver gained a label file.statfs64on Linux, since plainstatfsis the 32-bit interface on glibc and returnsEOVERFLOWfor a filesystem whose counts do not fit — which would drop large filesystems entirely on the 32-bit targets this receiver supports.statfson an unreachable server blocks uninterruptibly. Local FUSE filesystems still report. The list is not currently overridable — happy to add a regex flag like node_exporter's if you would prefer that.start_time_unix_nano(see the boot-time note below).--<name>-receiver-*prefix, durations parse withhumantime(60s,2m), paths and defaults live with the receiver config, and internal telemetry reusesrotel_receiver_accepted_metric_points/rotel_receiver_refused_metric_pointswith the same descriptions as the OTLP receivers (the descriptions are part of instrument identity, so a mismatch would create duplicate streams). Two counters are new:rotel_receiver_scrape_failuresandrotel_receiver_empty_scrapes.uname,timeandtextfilereturn anything. It is notcfg-gated to Linux the way kmsg is, since nothing here fails to build elsewhere — say the word if you would rather it were gated.AtomicU64goes throughportable-atomicon targets without native 64-bit atomics, matching whatfile/persistence/json_file.rsalready does — needed because the ARMv5 job below builds this feature.btimemoves when the wall clock is stepped, which matters on devices with no RTC that boot near the epoch and jump when NTP syncs. The start time stamped on cumulative counters stays latched at the first known value, so a clock step is not reported downstream as a counter reset.--kafka-receiver-check-crcsand--file-receiver-include-file-nameareboolargs withdefault_value = "true", which clap derives asSetTrue, so they cannot be disabled from the CLI either. That is the same bug this receiver had; fixing it changes user-visible behaviour in unrelated receivers, so it belongs in its own PR. Happy to open one.Tests
126 unit tests across the receiver, plus a real-kernel integration suite. Every collector is exercised through the injectable
procfs_path/sysfs_pathseams against temp-directory fixtures, asserting values, units, metric types and label sets — so they run identically on Linux and macOS rather than skipping. No unit test reads the host's/procor/sys.tests/node_metrics_integration_tests.rsis gated exactly like the kmsg suite (NODE_METRICS_INTEGRATION_TESTS=true+target_os = "linux"+ the feature). It asserts what fixtures cannot: that live/procand/sysformats still parse, that no value is NaN or infinite (and that byte/second/filesystem metrics are non-negative — temperatures legitimately are not), and that a real host's mount table and hwmon set do not produce two data points sharing an attribute set. It skips rather than fails on hosts with no thermal zones, no NVMe, an overlay-only root, or a clock that has not yet been set by NTP.Review history
This has been through six rounds of review. Things worth knowing that came out of them: the collector was split into
collector/{mod,procfs,sysfs,textfile,util}.rs(the largest,procfs.rs, is ~1.4k lines of code plus its tests); the exclusion lists were reworked so that local FUSE mounts and a volume mounted exactly at/var/lib/dockerare reported (only sub-paths are excluded, as node_exporter does); duplicate label sets are compared as sets rather than ordered lists; and each fix is covered by a test that was verified to fail when the fix is reverted.Two things are deliberately left unpinned, since testing them would cost more infrastructure than the bugs justify: the internal-telemetry counters are only observable through the global OTel meter, so the tests assert the corresponding log lines instead; and reaching the panicking-scrape branch would need an injectable scrape seam, so only the equivalent reaping path is covered.
CI changes included
testjob builds with--features node_metrics_receiver. Without it none of this code or its tests are compiled on a PR — which is how this branch silently broke whenopentelemetry-proto'sKeyValuegained akey_strindexfield.statfsfield widths differ there and that is a target this receiver is specifically aimed at.kmsg_receiverstill has the same CI gap; happy to extend the same treatment in a separate PR. If you would rather see per-feature coverage as its own job than as flags on that line, say so and I will restructure it.Happy to split
This is large for one commit.
collector/is already divided intomod/procfs/sysfs/textfile/util, so it splits cleanly into: (1) plumbing + config + convert + the procfs collectors, (2) the sysfs/hardware collectors, (3) the textfile collector and its exposition parser. Say the word and I will restack it that way.