From d604bed5a64dba1981d3d3c981eace19f6583aa0 Mon Sep 17 00:00:00 2001 From: Youssef Gharbi <57369993+joegharbi@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:09:01 +0200 Subject: [PATCH 1/9] Fix container detection for cgroups v2 (Debian 13) - Update regex patterns to support both cgroups v1 and v2 formats - Cgroups v2 paths have a hierarchy prefix (e.g., 0::) before the path - Docker, Kubernetes, and Containerd detection now work on systems using cgroups v2 Fixes #420 --- src/sensors/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sensors/utils.rs b/src/sensors/utils.rs index 2ba070ab..62d9e517 100644 --- a/src/sensors/utils.rs +++ b/src/sensors/utils.rs @@ -244,11 +244,11 @@ impl ProcessTracker { /// ``` pub fn new(max_records_per_process: u16) -> ProcessTracker { #[cfg(feature = "containers")] - let regex_cgroup_docker = Regex::new(r"^.*/docker.*$").unwrap(); + let regex_cgroup_docker = Regex::new(r"^(\d+::)?.*docker.*$").unwrap(); #[cfg(feature = "containers")] - let regex_cgroup_kubernetes = Regex::new(r"^/kubepods.*$").unwrap(); + let regex_cgroup_kubernetes = Regex::new(r"^(\d+::)?/?kubepods.*$").unwrap(); #[cfg(feature = "containers")] - let regex_cgroup_containerd = Regex::new("/system.slice/containerd.service/.*$").unwrap(); + let regex_cgroup_containerd = Regex::new(r"^(\d+::)?.*containerd\.service.*$").unwrap(); let mut system = System::new_all(); system.refresh_cpu_specifics(CpuRefreshKind::everything()); From 7de6dbe4e7685fc5c5573e9244c1f67b1fad9033 Mon Sep 17 00:00:00 2001 From: Youssef Gharbi <57369993+joegharbi@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:48:50 +0200 Subject: [PATCH 2/9] Refine regex patterns to avoid false positives and use non-capturing groups - Use non-capturing groups (?:...) instead of capturing groups to better communicate intent - Require proper path segments (e.g., /docker, /kubepods, /system.slice/containerd.service/) - Add non-greedy matching (*?) to avoid overly broad matches - Preserve original path-segment intent while supporting cgroups v2 Addresses review feedback from Copilot PR reviewer --- src/sensors/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sensors/utils.rs b/src/sensors/utils.rs index 62d9e517..43a85675 100644 --- a/src/sensors/utils.rs +++ b/src/sensors/utils.rs @@ -244,11 +244,11 @@ impl ProcessTracker { /// ``` pub fn new(max_records_per_process: u16) -> ProcessTracker { #[cfg(feature = "containers")] - let regex_cgroup_docker = Regex::new(r"^(\d+::)?.*docker.*$").unwrap(); + let regex_cgroup_docker = Regex::new(r"^(?:\d+::)?.*?/docker.*$").unwrap(); #[cfg(feature = "containers")] - let regex_cgroup_kubernetes = Regex::new(r"^(\d+::)?/?kubepods.*$").unwrap(); + let regex_cgroup_kubernetes = Regex::new(r"^(?:\d+::)?/?kubepods.*$").unwrap(); #[cfg(feature = "containers")] - let regex_cgroup_containerd = Regex::new(r"^(\d+::)?.*containerd\.service.*$").unwrap(); + let regex_cgroup_containerd = Regex::new(r"^(?:\d+::)?.*?/system\.slice/containerd\.service/.*$").unwrap(); let mut system = System::new_all(); system.refresh_cpu_specifics(CpuRefreshKind::everything()); From 14e6bd3faf70aa6c7519cf3b5646822b15aa78b3 Mon Sep 17 00:00:00 2001 From: ASAatUOM Date: Sun, 12 Jul 2026 21:15:38 +0200 Subject: [PATCH 3/9] doc: added sensor implementation page --- docs_src/SUMMARY.md | 1 + .../how-to-implement-a-new-sensor.md | 142 ++++++++++++++++++ docs_src/img/sensor_internal_structure.png | Bin 0 -> 45100 bytes 3 files changed, 143 insertions(+) create mode 100644 docs_src/how-to_guides/how-to-implement-a-new-sensor.md create mode 100644 docs_src/img/sensor_internal_structure.png diff --git a/docs_src/SUMMARY.md b/docs_src/SUMMARY.md index ed6332ee..26643706 100644 --- a/docs_src/SUMMARY.md +++ b/docs_src/SUMMARY.md @@ -15,6 +15,7 @@ - [Propagate power consumption metrics from hypervisor to virtual machines (Qemu/KVM)](how-to_guides/propagate-metrics-hypervisor-to-vm_qemu-kvm.md) - [Get process-level power consumption in my grafana dashboard](how-to_guides/get-process-level-power-in-grafana.md) - [Install Scaphandre with only Prometheus-push exporter compiled, for Prometheus Push Gateway, on RHEL 8 and 9](how-to_guides/install-prometheuspush-only-rhel.md) +- [How to implement a new sensor](how-to_guides/how-to-implement-a-new-sensor.md) # Explanations diff --git a/docs_src/how-to_guides/how-to-implement-a-new-sensor.md b/docs_src/how-to_guides/how-to-implement-a-new-sensor.md new file mode 100644 index 00000000..0c0f7c9d --- /dev/null +++ b/docs_src/how-to_guides/how-to-implement-a-new-sensor.md @@ -0,0 +1,142 @@ +# How To Start Implementing A New Sensor + +This page gives a brief overview on how to start implementing a new sensor. + +## Important Structs and Traits + +The following are the structs and traits defined in sensors/mod.rs. + +```rust +pub struct Record { + pub timestamp: Duration, + pub value: String, + pub unit: units::Unit, +} +``` +The record struct is used to represent an electricity consumption measurement. Exporters expect the records passed to them to be monotonically increasing. + +```rust +/// Sensor trait, the Sensor API. +pub trait Sensor { + fn get_topology(&self) -> Box>; + fn generate_topology(&self) -> Result>; +} +``` +The sensor trait is used by exporters to get an instance of a topology struct. + +```rust +/// Defines methods for Record instances creation +/// and storage. +pub trait RecordGenerator { + fn refresh_record(&mut self); + fn get_records_passive(&self) -> Vec; + fn clean_old_records(&mut self); +} +``` +The RecordGenerator is queried by exporters to obtain new records. RecordGenerator implementation is not specific to a sensor type, its purpose is to manage record buffers (structs implement it within sensors/mod.rs). It uses the RecordReader trait to generate new records itself. + +```rust +pub trait RecordReader { + fn read_record(&self) -> Result>; +} +``` +The RecordGenerator functions call the RecordReader trait's functions to read and create new records. The RecordReader trait is implemented within the specific sensor's .rs file and accounts for the differing behavior between sensors. +```rust +/// Topology struct represents the whole CPUSocket architecture, +/// from the electricity consumption point of view, +/// including the potentially multiple CPUSocket sockets. +/// Owns a vector of CPUSocket structs representing each socket. +#[derive(Debug, Clone)] +pub struct Topology { + /// The CPU sockets found on the host, represented as CPUSocket instances attached to this topology + pub sockets: Vec, + /// ProcessTrack instance that keeps track of processes running on the host and CPU stats associated + pub proc_tracker: ProcessTracker, + /// CPU usage stats buffer + pub stat_buffer: Vec, + /// Measurements of energy usage, stored as Record instances + pub record_buffer: Vec, + /// Maximum size in memory for the recor_buffer + pub buffer_max_kbytes: u16, + /// Sorted list of all domains names + pub domains_names: Option>, + /// Sensor-specific data needed in the topology + pub _sensor_data: HashMap, +} +``` +The topology struct implements RecordReader and RecordGenerator. Exporters use it to query records and access the ProcessTracker field, which attributes energy consumption to individual processes. The Topology struct is the main point of contact between sensors and exporters. + +```rust +/// CPUSocket struct represents a CPU socket (matches physical_id attribute in /proc/cpuinfo), +/// owning CPU cores (processor in /proc/cpuinfo). +#[derive(Debug, Clone)] +pub struct CPUSocket { + /// Numerical ID of the CPU socket (physical_id in /proc/cpuinfo) + pub id: u16, + /// RAPL domains attached to the socket + pub domains: Vec, + /// Text attributes linked to that socket, found in /proc/cpuinfo + pub attributes: Vec>>, + /// Path to the file that provides the counter for energy consumed by the socket, in microjoules. + pub counter_uj_path: String, + /// Comsumption records measured and stored by scaphandre for this socket. + pub record_buffer: Vec, + /// Maximum size of the record_buffer in kilobytes. + pub buffer_max_kbytes: u16, + /// CPU cores (core_id in /proc/cpuinfo) attached to the socket. + pub cpu_cores: Vec, + /// Usage statistics records stored for this socket. + pub stat_buffer: Vec, + /// Sensor-specific data that has been stored at the topology generation step. + #[allow(dead_code)] + pub sensor_data: HashMap, +} +``` +This struct represents a CPUSocket. It implements both RecordGenerator and RecordReader. It can be accessed by exporters via the topology struct to get CPUSocket specific records. + +```rust +/// Domain struct represents a part of a CPUSocket from the +/// electricity consumption point of view. +#[derive(Debug, Clone)] +pub struct Domain { + /// Numerical ID of the RAPL domain as indicated in /sys/class/powercap/intel-rapl* folders names + pub id: u16, + /// Name of the domain as found in /sys/class/powercap/intel-rapl:X:X/name + pub name: String, + /// Path to the domain's energy counter file, microjoules extracted + pub counter_uj_path: String, + /// History of energy consumption measurements, stored as Record instances + pub record_buffer: Vec, + /// Maximum size of record_buffer, in kilobytes + pub buffer_max_kbytes: u16, + /// Sensor-specific data that has been stored at the topology generation step. + #[allow(dead_code)] + sensor_data: HashMap, +} +``` +The Domain struct represents a part of the CPUSocket from the electricity consumption point of view, such as the core, uncore and DRAM. It implements RecordReader and RecordGenerator. + +## How It Fits Together + +![](../img/sensor_internal_structure.png) + +The above diagram shows how exporters access records and information from the sensor structs. +1. First the exporter uses the Sensor trait to get a topology struct. +2. The sensor used (powercap_rapl, msr_rapl or another) is determined by a conditional compilation (`#[cfg(..)]`). Only one sensor module can be included in the binary, otherwise there would be multiple RecordReader implementations. +3. The exporter calls the RecordGenerator trait on the Topology struct instance to retrieve records. +4. For the RAPL-based sensors, the Topology struct queries records from its list of CPUSockets, which in turn query their own Vector of Domains for records. + +## Implementing A New Sensor + +In essence, implementing a new sensor requires: + +- Creating a new .rs file in the sensors directory that: + + - Defines a struct implementing the Sensor trait. + - Provides RecordReader implementations for Topology, CPUSocket, and Domain. + - Declare it as a module in sensors/mod.rs, gated behind conditional compilation. + + +- Modifying the build_sensor function in main.rs to return the new sensor. +- Add a feature flag for the new sensor. +- Modifying the `#[cfg()]` conditions to guard against conflicting trait implementations and fixing any resulting definition errors. \ No newline at end of file diff --git a/docs_src/img/sensor_internal_structure.png b/docs_src/img/sensor_internal_structure.png new file mode 100644 index 0000000000000000000000000000000000000000..9fb21363d51756b2b4ac23a9542eb7a794b980a4 GIT binary patch literal 45100 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 E7ZTM|(EtDd literal 0 HcmV?d00001 From cce414a5ef5901af6eac59e98b587cccd9c3cd7a Mon Sep 17 00:00:00 2001 From: ASAatUOM Date: Sun, 12 Jul 2026 22:12:55 +0200 Subject: [PATCH 4/9] fix: made code blocks not runnable --- docs_src/SUMMARY.md | 2 +- .../how-to-implement-a-new-sensor.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs_src/SUMMARY.md b/docs_src/SUMMARY.md index 26643706..27b5d34c 100644 --- a/docs_src/SUMMARY.md +++ b/docs_src/SUMMARY.md @@ -15,7 +15,7 @@ - [Propagate power consumption metrics from hypervisor to virtual machines (Qemu/KVM)](how-to_guides/propagate-metrics-hypervisor-to-vm_qemu-kvm.md) - [Get process-level power consumption in my grafana dashboard](how-to_guides/get-process-level-power-in-grafana.md) - [Install Scaphandre with only Prometheus-push exporter compiled, for Prometheus Push Gateway, on RHEL 8 and 9](how-to_guides/install-prometheuspush-only-rhel.md) -- [How to implement a new sensor](how-to_guides/how-to-implement-a-new-sensor.md) +- [How to start implementing a new sensor](how-to_guides/how-to-implement-a-new-sensor.md) # Explanations diff --git a/docs_src/how-to_guides/how-to-implement-a-new-sensor.md b/docs_src/how-to_guides/how-to-implement-a-new-sensor.md index 0c0f7c9d..db99e337 100644 --- a/docs_src/how-to_guides/how-to-implement-a-new-sensor.md +++ b/docs_src/how-to_guides/how-to-implement-a-new-sensor.md @@ -1,12 +1,12 @@ # How To Start Implementing A New Sensor -This page gives a brief overview on how to start implementing a new sensor. +This page gives a brief overview on the internal structure of sensors and on how to start implementing a new sensor. ## Important Structs and Traits -The following are the structs and traits defined in sensors/mod.rs. +The following are the structs and traits defined in sensors/mod.rs. -```rust +```rust,noplayground pub struct Record { pub timestamp: Duration, pub value: String, @@ -15,7 +15,7 @@ pub struct Record { ``` The record struct is used to represent an electricity consumption measurement. Exporters expect the records passed to them to be monotonically increasing. -```rust +```rust,noplayground /// Sensor trait, the Sensor API. pub trait Sensor { fn get_topology(&self) -> Box>; @@ -24,7 +24,7 @@ pub trait Sensor { ``` The sensor trait is used by exporters to get an instance of a topology struct. -```rust +```rust,noplayground /// Defines methods for Record instances creation /// and storage. pub trait RecordGenerator { @@ -35,13 +35,13 @@ pub trait RecordGenerator { ``` The RecordGenerator is queried by exporters to obtain new records. RecordGenerator implementation is not specific to a sensor type, its purpose is to manage record buffers (structs implement it within sensors/mod.rs). It uses the RecordReader trait to generate new records itself. -```rust +```rust,noplayground pub trait RecordReader { fn read_record(&self) -> Result>; } ``` The RecordGenerator functions call the RecordReader trait's functions to read and create new records. The RecordReader trait is implemented within the specific sensor's .rs file and accounts for the differing behavior between sensors. -```rust +```rust,noplayground /// Topology struct represents the whole CPUSocket architecture, /// from the electricity consumption point of view, /// including the potentially multiple CPUSocket sockets. @@ -66,7 +66,7 @@ pub struct Topology { ``` The topology struct implements RecordReader and RecordGenerator. Exporters use it to query records and access the ProcessTracker field, which attributes energy consumption to individual processes. The Topology struct is the main point of contact between sensors and exporters. -```rust +```rust,noplayground /// CPUSocket struct represents a CPU socket (matches physical_id attribute in /proc/cpuinfo), /// owning CPU cores (processor in /proc/cpuinfo). #[derive(Debug, Clone)] @@ -94,7 +94,7 @@ pub struct CPUSocket { ``` This struct represents a CPUSocket. It implements both RecordGenerator and RecordReader. It can be accessed by exporters via the topology struct to get CPUSocket specific records. -```rust +```rust,noplayground /// Domain struct represents a part of a CPUSocket from the /// electricity consumption point of view. #[derive(Debug, Clone)] From c6d6653ab7a653549c26d87a3c18c877b3e3d990 Mon Sep 17 00:00:00 2001 From: ASAatUOM Date: Mon, 13 Jul 2026 17:00:05 +0200 Subject: [PATCH 5/9] fix: better wording --- .../how-to-implement-a-new-sensor.md | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/docs_src/how-to_guides/how-to-implement-a-new-sensor.md b/docs_src/how-to_guides/how-to-implement-a-new-sensor.md index db99e337..7509ae68 100644 --- a/docs_src/how-to_guides/how-to-implement-a-new-sensor.md +++ b/docs_src/how-to_guides/how-to-implement-a-new-sensor.md @@ -1,10 +1,10 @@ # How To Start Implementing A New Sensor -This page gives a brief overview on the internal structure of sensors and on how to start implementing a new sensor. +This guide provides an overview of the internal architecture of sensors and the steps to implement a new sensor without touching exporter code. ## Important Structs and Traits -The following are the structs and traits defined in sensors/mod.rs. +First an overview of the the structs and traits defined in sensors/mod.rs. ```rust,noplayground pub struct Record { @@ -13,7 +13,7 @@ pub struct Record { pub unit: units::Unit, } ``` -The record struct is used to represent an electricity consumption measurement. Exporters expect the records passed to them to be monotonically increasing. +`Record` is used to represent an electricity consumption measurement. Exporters expect the records passed to them to be monotonically increasing. ```rust,noplayground /// Sensor trait, the Sensor API. @@ -22,7 +22,7 @@ pub trait Sensor { fn generate_topology(&self) -> Result>; } ``` -The sensor trait is used by exporters to get an instance of a topology struct. +The `Sensor` trait is used by exporters to get an instance of a topology struct. ```rust,noplayground /// Defines methods for Record instances creation @@ -33,14 +33,14 @@ pub trait RecordGenerator { fn clean_old_records(&mut self); } ``` -The RecordGenerator is queried by exporters to obtain new records. RecordGenerator implementation is not specific to a sensor type, its purpose is to manage record buffers (structs implement it within sensors/mod.rs). It uses the RecordReader trait to generate new records itself. +`RecordGenerator` is queried by exporters to obtain new records. RecordGenerator implementations are not specific to a sensor type, its purpose is to manage the record buffers of the `Topology`, `CPUSocket` and `Domain` structs (structs implement `RecordGenerator` within sensors/mod.rs). It uses the `RecordReader` trait to generate new records itself. ```rust,noplayground pub trait RecordReader { fn read_record(&self) -> Result>; } ``` -The RecordGenerator functions call the RecordReader trait's functions to read and create new records. The RecordReader trait is implemented within the specific sensor's .rs file and accounts for the differing behavior between sensors. +`RecordReader` is responsible for obtaining measurements from the underlying sensor. The `RecordReader` trait is implemented within the specific sensor's .rs file and accounts for the differing behavior between sensors. ```rust,noplayground /// Topology struct represents the whole CPUSocket architecture, /// from the electricity consumption point of view, @@ -64,7 +64,10 @@ pub struct Topology { pub _sensor_data: HashMap, } ``` -The topology struct implements RecordReader and RecordGenerator. Exporters use it to query records and access the ProcessTracker field, which attributes energy consumption to individual processes. The Topology struct is the main point of contact between sensors and exporters. +The `Topology` struct implements `RecordReader` and `RecordGenerator`. +Exporters use it to query for records and can access its fields for more specific information. +The `ProcessTracker` field is particularly of note since it attributes energy consumption to individual processes. +The `Topology` struct is the main point of contact between sensors and exporters. ```rust,noplayground /// CPUSocket struct represents a CPU socket (matches physical_id attribute in /proc/cpuinfo), @@ -92,7 +95,7 @@ pub struct CPUSocket { pub sensor_data: HashMap, } ``` -This struct represents a CPUSocket. It implements both RecordGenerator and RecordReader. It can be accessed by exporters via the topology struct to get CPUSocket specific records. +This struct represents a CPUSocket. It implements both `RecordGenerator` and `RecordReader`. It can be accessed by exporters via the `Topology` struct to get CPUSocket specific records. ```rust,noplayground /// Domain struct represents a part of a CPUSocket from the @@ -114,9 +117,9 @@ pub struct Domain { sensor_data: HashMap, } ``` -The Domain struct represents a part of the CPUSocket from the electricity consumption point of view, such as the core, uncore and DRAM. It implements RecordReader and RecordGenerator. +The `Domain` struct represents a part of the CPUSocket from the electricity consumption point of view, such as the core, uncore and DRAM. It implements `RecordReader` and `RecordGenerator`. -## How It Fits Together +## How everything fits together ![](../img/sensor_internal_structure.png) @@ -124,19 +127,19 @@ The above diagram shows how exporters access records and information from the se 1. First the exporter uses the Sensor trait to get a topology struct. 2. The sensor used (powercap_rapl, msr_rapl or another) is determined by a conditional compilation (`#[cfg(..)]`). Only one sensor module can be included in the binary, otherwise there would be multiple RecordReader implementations. 3. The exporter calls the RecordGenerator trait on the Topology struct instance to retrieve records. -4. For the RAPL-based sensors, the Topology struct queries records from its list of CPUSockets, which in turn query their own Vector of Domains for records. +4. For the RAPL-based sensors, the Topology struct queries records from its list of CPUSockets, which in turn query `Vec` for records. ## Implementing A New Sensor In essence, implementing a new sensor requires: -- Creating a new .rs file in the sensors directory that: +- Adding a feature flag for the new sensor. +- Creating a new module under src/sensors/ that: - Defines a struct implementing the Sensor trait. - Provides RecordReader implementations for Topology, CPUSocket, and Domain. - Declare it as a module in sensors/mod.rs, gated behind conditional compilation. + - -- Modifying the build_sensor function in main.rs to return the new sensor. -- Add a feature flag for the new sensor. +- Modifying the `build_sensor` function in main.rs and `get_default_sensor` function in lib.rs to return the new sensor. - Modifying the `#[cfg()]` conditions to guard against conflicting trait implementations and fixing any resulting definition errors. \ No newline at end of file From f18e0f6ad8762eb44b599dcbc3d69472534c0c24 Mon Sep 17 00:00:00 2001 From: bpetit Date: Sun, 19 Jul 2026 20:14:24 +0200 Subject: [PATCH 6/9] doc: updating doc about compilation --- docs_src/tutorials/compilation-linux.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs_src/tutorials/compilation-linux.md b/docs_src/tutorials/compilation-linux.md index 2c2086f9..2141a8f2 100644 --- a/docs_src/tutorials/compilation-linux.md +++ b/docs_src/tutorials/compilation-linux.md @@ -1,11 +1,6 @@ # Compile scaphandre from source (GNU/Linux) -We recommand using this version of the rust toolchain or later: - - cargo --version - cargo 1.48.0 (65cbdd2dc 2020-10-14) - rustc --version - rustc 1.48.0 (7eac88abb 2020-11-16) +We recommand using the latest version available of the rust toolchain, as the tests are calling for the latest version before running. To be sure to be up to date, you may install rust from the [official website](https://www.rust-lang.org/) instead of your package manager. From fec390894fbb79c97c1dcf0fad6e9f3285299ea1 Mon Sep 17 00:00:00 2001 From: bpetit Date: Sun, 19 Jul 2026 20:29:07 +0200 Subject: [PATCH 7/9] doc: adding troubleshooting for openssl error during build --- docs_src/troubleshooting.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs_src/troubleshooting.md b/docs_src/troubleshooting.md index 4994892c..eabacd59 100644 --- a/docs_src/troubleshooting.md +++ b/docs_src/troubleshooting.md @@ -64,3 +64,7 @@ We verified that scaphandre (and especially the powercap_rapl sensor) works on A You need compiling tooling. On Ubuntu/Debian, run: sudo apt install build-essential + +### Building or running clippy fails complaining about OpenSSL not being found + +Ensure you have `libssl-dev` and `pkg-config` packages installed on Debian/Ubuntu. From 469978bb890f088997876d271473a31529b4df81 Mon Sep 17 00:00:00 2001 From: bpetit Date: Sun, 19 Jul 2026 20:30:32 +0200 Subject: [PATCH 8/9] fix: setting last release verion in cargo --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4187d427..b3cecb81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scaphandre" -version = "1.0.2" +version = "1.0.3" authors = ["Benoit Petit "] edition = "2024" license = "Apache-2.0" From 715563305deecd0d4634f92ea8211b197b1ba452 Mon Sep 17 00:00:00 2001 From: bpetit Date: Sun, 19 Jul 2026 20:33:31 +0200 Subject: [PATCH 9/9] style: clippy --- Cargo.lock | 2 +- src/exporters/prometheus.rs | 2 +- src/exporters/qemu.rs | 2 +- src/sensors/utils.rs | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc00d2e7..9738b256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1462,7 +1462,7 @@ checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "scaphandre" -version = "1.0.2" +version = "1.0.3" dependencies = [ "chrono", "clap", diff --git a/src/exporters/prometheus.rs b/src/exporters/prometheus.rs index 606bcb96..c4262320 100644 --- a/src/exporters/prometheus.rs +++ b/src/exporters/prometheus.rs @@ -158,7 +158,7 @@ async fn show_metrics( ) -> Result, Infallible> { trace!("{}", req.uri()); let mut body = String::new(); - if req.uri().path() == format!("/{}", &suffix) { + if req.uri().path() == format!("/{}", suffix) { let now = current_system_time_since_epoch(); match context.last_request.lock() { Ok(mut last_request) => { diff --git a/src/exporters/qemu.rs b/src/exporters/qemu.rs index e566df3e..ebe44700 100644 --- a/src/exporters/qemu.rs +++ b/src/exporters/qemu.rs @@ -69,7 +69,7 @@ impl QemuExporter { if fs::read_dir(&first_domain_path).is_err() { match fs::create_dir_all(&first_domain_path) { Ok(_) => info!("Created {} folder.", &path), - Err(error) => panic!("Couldn't create {}. Got: {}", &path, error), + Err(error) => panic!("Couldn't create {}. Got: {}", path, error), } } if let Some(ratio) = self diff --git a/src/sensors/utils.rs b/src/sensors/utils.rs index 1d97456a..4d7b2ca3 100644 --- a/src/sensors/utils.rs +++ b/src/sensors/utils.rs @@ -243,7 +243,8 @@ impl ProcessTracker { let regex_cgroup_docker = Regex::new(r"^(?:\d+::)?.*?/docker.*$").unwrap(); #[cfg(feature = "containers")] let regex_cgroup_kubernetes = Regex::new(r"(?:\d+::)?/?kubepods.*$").unwrap(); - let regex_cgroup_containerd = Regex::new(r"^(?:\d+::)?.*?/system\.slice/containerd\.service/.*$").unwrap(); + let regex_cgroup_containerd = + Regex::new(r"^(?:\d+::)?.*?/system\.slice/containerd\.service/.*$").unwrap(); let mut system = System::new_all(); system.refresh_cpu_specifics(CpuRefreshKind::everything());