use std::time::Duration;
use crate::mechanism::Mechanism;
use crate::result::Outcome;
const SPAWNS_TOTAL: &str = "processkit.spawns.total";
const RUNS_TOTAL: &str = "processkit.runs.total";
const RUN_DURATION: &str = "processkit.run.duration_seconds";
const EXIT_CODE_TOTAL: &str = "processkit.exit_code.total";
const RETRIES_TOTAL: &str = "processkit.retries.total";
const RESTARTS_TOTAL: &str = "processkit.restarts.total";
const STORM_PAUSES_TOTAL: &str = "processkit.storm_pauses.total";
const TEARDOWN_TOTAL: &str = "processkit.teardown.total";
const TEARDOWN_DURATION: &str = "processkit.teardown.duration_seconds";
pub(crate) fn outcome_label(outcome: &Outcome, cancelled: bool) -> &'static str {
if cancelled {
return "cancelled";
}
match outcome {
Outcome::Exited(_) => "exited",
Outcome::Signalled(_) => "signalled",
Outcome::TimedOut => "timed_out",
}
}
pub(crate) fn record_spawn(program: &str, mechanism: Mechanism) {
metrics::counter!(
SPAWNS_TOTAL,
"program" => program.to_owned(),
"mechanism" => mechanism.name(),
)
.increment(1);
}
pub(crate) fn record_run(program: &str, outcome: &Outcome, cancelled: bool, elapsed: Duration) {
metrics::counter!(
RUNS_TOTAL,
"program" => program.to_owned(),
"outcome" => outcome_label(outcome, cancelled),
)
.increment(1);
metrics::histogram!(
RUN_DURATION,
"program" => program.to_owned(),
)
.record(elapsed.as_secs_f64());
if !cancelled && let Outcome::Exited(code) = outcome {
metrics::counter!(
EXIT_CODE_TOTAL,
"program" => program.to_owned(),
"code" => code.to_string(),
)
.increment(1);
}
}
pub(crate) fn record_retry(program: &str) {
metrics::counter!(RETRIES_TOTAL, "program" => program.to_owned()).increment(1);
}
pub(crate) fn record_restart() {
metrics::counter!(RESTARTS_TOTAL).increment(1);
}
pub(crate) fn record_storm_pause() {
metrics::counter!(STORM_PAUSES_TOTAL).increment(1);
}
pub(crate) fn record_teardown(phase: &'static str) {
metrics::counter!(TEARDOWN_TOTAL, "phase" => phase).increment(1);
}
pub(crate) fn record_teardown_duration(phase: &'static str, elapsed: Duration) {
metrics::histogram!(TEARDOWN_DURATION, "phase" => phase).record(elapsed.as_secs_f64());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outcome_labels_are_a_bounded_secret_free_vocabulary() {
assert_eq!(outcome_label(&Outcome::Exited(0), false), "exited");
assert_eq!(outcome_label(&Outcome::Exited(3), false), "exited");
assert_eq!(
outcome_label(&Outcome::Signalled(Some(9)), false),
"signalled"
);
assert_eq!(outcome_label(&Outcome::Signalled(None), false), "signalled");
assert_eq!(outcome_label(&Outcome::TimedOut, false), "timed_out");
assert_eq!(outcome_label(&Outcome::Signalled(None), true), "cancelled");
assert_eq!(outcome_label(&Outcome::Exited(0), true), "cancelled");
}
}
#[cfg(test)]
mod secret_hygiene {
use std::sync::{Mutex, OnceLock};
use metrics::{
Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit,
};
use crate::ProcessRunner;
fn captured() -> &'static Mutex<Vec<String>> {
static CAPTURED: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
CAPTURED.get_or_init(|| Mutex::new(Vec::new()))
}
#[derive(Debug)]
struct CapturingRecorder;
impl CapturingRecorder {
fn note(key: &Key) {
let mut buf = captured().lock().expect("capture buffer poisoned");
buf.push(key.name().to_owned());
for label in key.labels() {
buf.push(label.key().to_owned());
buf.push(label.value().to_owned());
}
}
}
impl Recorder for CapturingRecorder {
fn describe_counter(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
fn describe_gauge(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
fn describe_histogram(&self, _: KeyName, _: Option<Unit>, _: SharedString) {}
fn register_counter(&self, key: &Key, _: &Metadata<'_>) -> Counter {
Self::note(key);
Counter::noop()
}
fn register_gauge(&self, key: &Key, _: &Metadata<'_>) -> Gauge {
Self::note(key);
Gauge::noop()
}
fn register_histogram(&self, key: &Key, _: &Metadata<'_>) -> Histogram {
Self::note(key);
Histogram::noop()
}
}
#[tokio::test]
#[ignore = "exercises the real spawn path and installs a process-global metrics recorder"]
async fn labels_never_carry_argv_or_env_values() {
const SECRET_ARG: &str = "pk-metrics-secret-argv-9f3a2b7c";
const SECRET_ENV: &str = "pk-metrics-secret-env-7c1d4e8a";
metrics::set_global_recorder(CapturingRecorder)
.expect("this is the only test that installs a metrics recorder");
let command = if cfg!(windows) {
crate::Command::new("cmd")
.args(["/c", "exit", "0"])
.arg(SECRET_ARG)
.env("PK_METRICS_SECRET", SECRET_ENV)
} else {
crate::Command::new("sh")
.args(["-c", "exit 0", "pk_argv0", SECRET_ARG])
.env("PK_METRICS_SECRET", SECRET_ENV)
};
let _ = crate::JobRunner::new().output_string(&command).await;
let snapshot = captured().lock().expect("capture buffer poisoned").clone();
for entry in &snapshot {
assert!(
!entry.contains(SECRET_ARG),
"argv value leaked into a metric name/label: {entry:?}"
);
assert!(
!entry.contains(SECRET_ENV),
"env value leaked into a metric name/label: {entry:?}"
);
}
assert!(
snapshot
.iter()
.any(|entry| entry == "processkit.runs.total"),
"expected the run to emit `processkit.runs.total`; captured: {snapshot:?}"
);
}
}