use std::sync::{Arc, Weak};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use agentstategraph::Repository;
pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(60);
pub fn interval_from_env() -> Option<Duration> {
match std::env::var("ASG_STORE_GAUGES_INTERVAL") {
Err(_) => Some(DEFAULT_INTERVAL),
Ok(value) => match value.trim().parse::<u64>() {
Ok(0) => None,
Ok(seconds) => Some(Duration::from_secs(seconds)),
Err(_) => {
tracing::warn!(
value,
"ASG_STORE_GAUGES_INTERVAL isn't a number of seconds; sampling every 60"
);
Some(DEFAULT_INTERVAL)
}
},
}
}
pub fn describe() {
metrics::describe_gauge!(
"asg.store.available",
"1 if the store answered the last sample, else 0."
);
metrics::describe_gauge!("asg.store.objects", "Objects in the store.");
metrics::describe_gauge!("asg.store.commits", "Commits in the store.");
metrics::describe_gauge!(
"asg.store.size",
metrics::Unit::Bytes,
"The store's size on disk."
);
metrics::describe_gauge!(
"asg.history.undistilled_commits",
"Commits the history extractor hasn't distilled yet."
);
metrics::describe_gauge!(
"asg.gc.last_sweep.time",
metrics::Unit::Seconds,
"When the last garbage collection sweep this server ran finished, as Unix time."
);
metrics::describe_gauge!(
"asg.gc.last_sweep.objects_deleted",
"Objects the last garbage collection sweep deleted."
);
metrics::describe_gauge!(
"asg.gc.last_vacuum.bytes_reclaimed",
metrics::Unit::Bytes,
"Bytes the last vacuum returned to the operating system."
);
}
pub fn sample(repo: &Repository) -> bool {
let mut ok = true;
match repo.store_totals() {
Ok(Some(totals)) => {
metrics::gauge!("asg.store.objects").set(totals.objects as f64);
metrics::gauge!("asg.store.commits").set(totals.commits as f64);
if let Some(bytes) = totals.bytes {
metrics::gauge!("asg.store.size").set(bytes as f64);
}
}
Ok(None) => {}
Err(e) => {
ok = false;
tracing::debug!(error = %e, "store gauges: counting the store failed");
}
}
match repo.undistilled_commits() {
Ok(n) => metrics::gauge!("asg.history.undistilled_commits").set(n as f64),
Err(e) => {
ok = false;
tracing::debug!(error = %e, "store gauges: counting undistilled commits failed");
}
}
metrics::gauge!("asg.store.available").set(if ok { 1.0 } else { 0.0 });
ok
}
pub fn spawn(repo: &Arc<Repository>, interval: Duration) {
describe();
let repo: Weak<Repository> = Arc::downgrade(repo);
let spawned = std::thread::Builder::new()
.name("asg-store-gauges".into())
.spawn(move || {
let mut failing = false;
loop {
let Some(repo) = repo.upgrade() else { return };
let ok = sample(&repo);
drop(repo);
if !ok && !failing {
tracing::warn!("store gauges: couldn't count the store; they'll be stale");
}
failing = !ok;
std::thread::sleep(interval);
}
});
if let Err(e) = spawned {
tracing::warn!(error = %e, "store gauges: couldn't start the sampler");
}
}
pub fn record_sweep(report: &serde_json::Value) {
if report.get("mutated").and_then(|v| v.as_bool()) != Some(true) {
return;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
metrics::gauge!("asg.gc.last_sweep.time").set(now);
if let Some(deleted) = report.get("objects_deleted").and_then(|v| v.as_i64()) {
metrics::gauge!("asg.gc.last_sweep.objects_deleted").set(deleted as f64);
}
if let Some(vacuum) = report.get("vacuum") {
record_vacuum(vacuum);
}
}
pub fn record_vacuum(report: &serde_json::Value) {
if let Some(bytes) = report.get("bytes_reclaimed").and_then(|v| v.as_i64()) {
metrics::gauge!("asg.gc.last_vacuum.bytes_reclaimed").set(bytes as f64);
}
}