Skip to content

Commit a56e509

Browse files
feat: expose averaged process CPU and memory per cluster node (#1743)
* feat: expose averaged process CPU and memory per cluster node * rename: clarify process CPU/memory metrics as lifetime averages * EWMA instead of lifetime avg * coderabbit suggestion --------- Co-authored-by: Anant Vindal <anant.v09@protonmail.com>
1 parent 03a85dd commit a56e509

3 files changed

Lines changed: 133 additions & 1 deletion

File tree

‎src/handlers/http/resource_check.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ use tokio::{
3232
use tracing::{info, trace, warn};
3333

3434
use crate::analytics::{SYS_INFO, refresh_sys_info};
35+
use crate::metrics::record_process_metrics_sample;
3536
use crate::parseable::PARSEABLE;
3637

38+
const PROCESS_METRICS_SAMPLE_INTERVAL: Duration = Duration::from_secs(5);
39+
3740
static RESOURCE_CHECK_ENABLED: LazyLock<Arc<AtomicBool>> =
3841
LazyLock::new(|| Arc::new(AtomicBool::new(false)));
3942

@@ -42,6 +45,7 @@ pub fn spawn_resource_monitor(shutdown_rx: tokio::sync::oneshot::Receiver<()>) {
4245
tokio::spawn(async move {
4346
let resource_check_interval = PARSEABLE.options.resource_check_interval;
4447
let mut check_interval = interval(Duration::from_secs(resource_check_interval));
48+
let mut process_metrics_interval = interval(PROCESS_METRICS_SAMPLE_INTERVAL);
4549
let mut shutdown_rx = shutdown_rx;
4650

4751
let cpu_threshold = PARSEABLE.options.cpu_utilization_threshold;
@@ -106,6 +110,20 @@ pub fn spawn_resource_monitor(shutdown_rx: tokio::sync::oneshot::Receiver<()>) {
106110
}
107111
}
108112
},
113+
_ = process_metrics_interval.tick() => {
114+
refresh_sys_info();
115+
let process_metrics = tokio::task::spawn_blocking(|| {
116+
let sys = SYS_INFO.lock().unwrap();
117+
sysinfo::get_current_pid()
118+
.ok()
119+
.and_then(|pid| sys.process(pid))
120+
.map(|process| (process.cpu_usage() as f64, process.memory()))
121+
}).await.unwrap();
122+
123+
if let Some((cpu_usage, memory_bytes)) = process_metrics {
124+
record_process_metrics_sample(cpu_usage, memory_bytes);
125+
}
126+
},
109127
_ = &mut shutdown_rx => {
110128
trace!("Resource monitor shutting down");
111129
break;

‎src/metrics/mod.rs‎

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*/
1818

1919
pub mod prom_utils;
20+
use std::sync::OnceLock;
21+
2022
use crate::{
2123
handlers::{TelemetryType, http::metrics_path},
2224
stats::FullStats,
@@ -25,7 +27,10 @@ use actix_web::Responder;
2527
use actix_web_prometheus::{PrometheusMetrics, PrometheusMetricsBuilder};
2628
use error::MetricsError;
2729
use once_cell::sync::Lazy;
28-
use prometheus::{HistogramOpts, HistogramVec, IntCounterVec, IntGaugeVec, Opts, Registry};
30+
use prometheus::{
31+
Gauge, HistogramOpts, HistogramVec, IntCounterVec, IntGaugeVec, Opts, Registry,
32+
core::{Atomic, AtomicF64},
33+
};
2934

3035
pub const METRICS_NAMESPACE: &str = env!("CARGO_PKG_NAME");
3136

@@ -175,6 +180,97 @@ pub static STAGING_FILES: Lazy<IntGaugeVec> = Lazy::new(|| {
175180
.expect("metric can be created")
176181
});
177182

183+
pub static PROCESS_CPU_USAGE_PERCENT_AVG: Lazy<Gauge> = Lazy::new(|| {
184+
Gauge::with_opts(
185+
Opts::new(
186+
"process_cpu_usage_percent_avg",
187+
"Lifetime average CPU usage percent for this Parseable process",
188+
)
189+
.namespace(METRICS_NAMESPACE),
190+
)
191+
.expect("metric can be created")
192+
});
193+
194+
pub static PROCESS_MEMORY_BYTES_AVG: Lazy<Gauge> = Lazy::new(|| {
195+
Gauge::with_opts(
196+
Opts::new(
197+
"process_memory_bytes_avg",
198+
"Lifetime average resident memory used by this Parseable process in bytes",
199+
)
200+
.namespace(METRICS_NAMESPACE),
201+
)
202+
.expect("metric can be created")
203+
});
204+
pub static PROCESS_METRICS_INIT: OnceLock<(f64, u64)> = OnceLock::new();
205+
struct ProcessMetricsAccumulator {
206+
cpu_usage_avg: AtomicF64,
207+
memory_bytes_avg: AtomicF64,
208+
}
209+
210+
impl Default for ProcessMetricsAccumulator {
211+
fn default() -> Self {
212+
// PROCESS_METRICS_INIT must be initialized by now
213+
let (cpu, mem) = *PROCESS_METRICS_INIT.get().unwrap();
214+
Self {
215+
cpu_usage_avg: AtomicF64::new(cpu),
216+
memory_bytes_avg: AtomicF64::new(mem as f64),
217+
}
218+
}
219+
}
220+
221+
impl ProcessMetricsAccumulator {
222+
fn record(&self, cpu_usage_percent: f64, memory_bytes: u64) -> (f64, f64) {
223+
// Exponentially Weighted Moving Average is better than
224+
// a lifetime average
225+
// A spike which occurred 5 days ago should not affect the average utilization
226+
// for the last minute
227+
// α = 1 - exp(-Δt / τ) = 1 - exp(-5/60) ≈ 0.0800
228+
// S_new = S_old + α * (x_new - S_old)
229+
let s_cpu_old = self.cpu_usage_avg.get();
230+
let s_cpu_new = s_cpu_old + 0.08 * (cpu_usage_percent - s_cpu_old);
231+
232+
let s_mem_old = self.memory_bytes_avg.get();
233+
let s_mem_new = s_mem_old + 0.08 * (memory_bytes as f64 - s_mem_old);
234+
235+
// update accumulator
236+
self.cpu_usage_avg.set(s_cpu_new);
237+
self.memory_bytes_avg.set(s_mem_new);
238+
239+
(s_cpu_new, s_mem_new)
240+
}
241+
}
242+
243+
static PROCESS_METRICS_ACCUMULATOR: Lazy<ProcessMetricsAccumulator> =
244+
Lazy::new(ProcessMetricsAccumulator::default);
245+
246+
pub fn record_process_metrics_sample(cpu_usage_percent: f64, memory_bytes: u64) {
247+
if PROCESS_METRICS_INIT.get().is_none() {
248+
// first measurement
249+
let _ = PROCESS_METRICS_INIT.set((cpu_usage_percent, memory_bytes));
250+
}
251+
let (average_cpu_usage, average_memory_bytes) =
252+
PROCESS_METRICS_ACCUMULATOR.record(cpu_usage_percent, memory_bytes);
253+
PROCESS_CPU_USAGE_PERCENT_AVG.set(average_cpu_usage);
254+
PROCESS_MEMORY_BYTES_AVG.set(average_memory_bytes);
255+
}
256+
257+
#[cfg(test)]
258+
mod process_metrics_tests {
259+
use crate::metrics::PROCESS_METRICS_INIT;
260+
261+
use super::ProcessMetricsAccumulator;
262+
263+
#[test]
264+
fn averages_process_metric_samples() {
265+
// init PROCESS_METRICS_INIT
266+
PROCESS_METRICS_INIT.get_or_init(|| (10.0, 100));
267+
let accumulator = ProcessMetricsAccumulator::default();
268+
269+
assert_eq!(accumulator.record(10.0, 100), (10.0, 100.0));
270+
assert_eq!(accumulator.record(20.0, 300), (10.8, 116.0));
271+
}
272+
}
273+
178274
pub static QUERY_EXECUTE_TIME: Lazy<HistogramVec> = Lazy::new(|| {
179275
HistogramVec::new(
180276
HistogramOpts::new("query_execute_time", "Query execute time").namespace(METRICS_NAMESPACE),
@@ -663,6 +759,12 @@ fn custom_metrics(registry: &Registry) {
663759
registry
664760
.register(Box::new(STAGING_FILES.clone()))
665761
.expect("metric can be registered");
762+
registry
763+
.register(Box::new(PROCESS_CPU_USAGE_PERCENT_AVG.clone()))
764+
.expect("metric can be registered");
765+
registry
766+
.register(Box::new(PROCESS_MEMORY_BYTES_AVG.clone()))
767+
.expect("metric can be registered");
666768
registry
667769
.register(Box::new(QUERY_EXECUTE_TIME.clone()))
668770
.expect("metric can be registered");

‎src/metrics/prom_utils.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub struct Metrics {
5353
event_time: NaiveDateTime,
5454
commit: String,
5555
staging: String,
56+
process_cpu_usage_percent_avg: f64,
57+
process_memory_bytes_avg: f64,
5658
}
5759

5860
#[derive(Debug, Serialize, Default, Clone)]
@@ -89,6 +91,8 @@ impl Default for Metrics {
8991
event_time: Utc::now().naive_utc(),
9092
commit: "".to_string(),
9193
staging: "".to_string(),
94+
process_cpu_usage_percent_avg: 0.0,
95+
process_memory_bytes_avg: 0.0,
9296
}
9397
}
9498
}
@@ -113,6 +117,8 @@ impl Metrics {
113117
event_time: Utc::now().naive_utc(),
114118
commit: "".to_string(),
115119
staging: "".to_string(),
120+
process_cpu_usage_percent_avg: 0.0,
121+
process_memory_bytes_avg: 0.0,
116122
}
117123
}
118124
}
@@ -187,6 +193,12 @@ impl Metrics {
187193
"process_resident_memory_bytes" => {
188194
prom_dress.process_resident_memory_bytes += val
189195
}
196+
"parseable_process_cpu_usage_percent_avg" => {
197+
prom_dress.process_cpu_usage_percent_avg += val
198+
}
199+
"parseable_process_memory_bytes_avg" => {
200+
prom_dress.process_memory_bytes_avg += val
201+
}
190202
"parseable_storage_size" => {
191203
if sample.labels.get("type").expect("type is present") == "staging" {
192204
prom_dress.parseable_storage_size.staging += val;

0 commit comments

Comments
 (0)