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/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" diff --git a/docs_src/SUMMARY.md b/docs_src/SUMMARY.md index ed6332ee..27b5d34c 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 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 new file mode 100644 index 00000000..7509ae68 --- /dev/null +++ b/docs_src/how-to_guides/how-to-implement-a-new-sensor.md @@ -0,0 +1,145 @@ +# 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 + +First an overview of the the structs and traits defined in sensors/mod.rs. + +```rust,noplayground +pub struct Record { + pub timestamp: Duration, + pub value: String, + pub unit: units::Unit, +} +``` +`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. +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,noplayground +/// 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); +} +``` +`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>; +} +``` +`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, +/// 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 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), +/// 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,noplayground +/// 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 everything 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 `Vec` for records. + +## Implementing A New Sensor + +In essence, implementing a new sensor requires: + +- 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 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 diff --git a/docs_src/img/sensor_internal_structure.png b/docs_src/img/sensor_internal_structure.png new file mode 100644 index 00000000..9fb21363 Binary files /dev/null and b/docs_src/img/sensor_internal_structure.png differ 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. 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. 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 d6d39d9e..4d7b2ca3 100644 --- a/src/sensors/utils.rs +++ b/src/sensors/utils.rs @@ -240,11 +240,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(); - #[cfg(feature = "containers")] - let regex_cgroup_containerd = Regex::new("/system.slice/containerd.service/.*$").unwrap(); + let regex_cgroup_kubernetes = Regex::new(r"(?:\d+::)?/?kubepods.*$").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());