use crate::collectors::Collector;
use anyhow::Result;
use futures::future::BoxFuture;
use prometheus::{IntGaugeVec, Opts, Registry};
use sqlx::{PgPool, Row};
use tracing::{debug, info, info_span, instrument};
use tracing_futures::Instrument as _;
#[derive(Clone)]
pub struct VacuumStatsCollector {
tuples_dead: IntGaugeVec,
tuples_live: IntGaugeVec,
last_vacuum_age: IntGaugeVec,
last_autovacuum_age: IntGaugeVec,
last_analyze_age: IntGaugeVec,
last_autoanalyze_age: IntGaugeVec,
dead_ratio: IntGaugeVec,
}
impl Default for VacuumStatsCollector {
fn default() -> Self {
Self::new()
}
}
impl VacuumStatsCollector {
pub fn new() -> Self {
let tuples_dead = IntGaugeVec::new(
Opts::new("pg_table_tuples_dead", "Dead tuples per table"),
&["schema", "table"],
)
.expect("failed to create tuples_dead metric");
let tuples_live = IntGaugeVec::new(
Opts::new("pg_table_tuples_live", "Live tuples per table"),
&["schema", "table"],
)
.expect("failed to create tuples_live metric");
let last_vacuum_age = IntGaugeVec::new(
Opts::new(
"pg_table_last_vacuum_age_seconds",
"Age in seconds since last vacuum per table",
),
&["schema", "table"],
)
.expect("failed to create last_vacuum_age metric");
let last_autovacuum_age = IntGaugeVec::new(
Opts::new(
"pg_table_last_autovacuum_age_seconds",
"Age in seconds since last autovacuum per table",
),
&["schema", "table"],
)
.expect("failed to create last_autovacuum_age metric");
let last_analyze_age = IntGaugeVec::new(
Opts::new(
"pg_table_last_analyze_age_seconds",
"Age in seconds since last analyze per table",
),
&["schema", "table"],
)
.expect("failed to create last_analyze_age metric");
let last_autoanalyze_age = IntGaugeVec::new(
Opts::new(
"pg_table_last_autoanalyze_age_seconds",
"Age in seconds since last autoanalyze per table",
),
&["schema", "table"],
)
.expect("failed to create last_autoanalyze_age metric");
let dead_ratio = IntGaugeVec::new(
Opts::new(
"pg_table_dead_ratio",
"Dead tuples ratio: dead / (dead + live) (reported as percentage)",
),
&["schema", "table"],
)
.expect("failed to create dead_ratio metric");
Self {
tuples_dead,
tuples_live,
last_vacuum_age,
last_autovacuum_age,
last_analyze_age,
last_autoanalyze_age,
dead_ratio,
}
}
}
impl Collector for VacuumStatsCollector {
fn name(&self) -> &'static str {
"vacuum_stats"
}
#[instrument(
skip(self, registry),
level = "info",
err,
fields(collector = "vacuum_stats")
)]
fn register_metrics(&self, registry: &Registry) -> Result<()> {
registry.register(Box::new(self.tuples_dead.clone()))?;
registry.register(Box::new(self.tuples_live.clone()))?;
registry.register(Box::new(self.last_vacuum_age.clone()))?;
registry.register(Box::new(self.last_autovacuum_age.clone()))?;
registry.register(Box::new(self.last_analyze_age.clone()))?;
registry.register(Box::new(self.last_autoanalyze_age.clone()))?;
registry.register(Box::new(self.dead_ratio.clone()))?;
Ok(())
}
#[instrument(
skip(self, pool),
level = "info",
err,
fields(collector="vacuum_stats", otel.kind="internal")
)]
fn collect<'a>(&'a self, pool: &'a PgPool) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
let query_span = info_span!(
"db.query",
otel.kind = "client",
db.system = "postgresql",
db.operation = "SELECT",
db.statement = "SELECT schemaname, relname, n_live_tup, n_dead_tup, age fields FROM pg_stat_all_tables",
db.sql.table = "pg_stat_all_tables"
);
let rows = sqlx::query(
r#"
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
COALESCE(EXTRACT(EPOCH FROM (now() - last_vacuum))::bigint, -1) AS last_vacuum_age,
COALESCE(EXTRACT(EPOCH FROM (now() - last_autovacuum))::bigint, -1) AS last_autovacuum_age,
COALESCE(EXTRACT(EPOCH FROM (now() - last_analyze))::bigint, -1) AS last_analyze_age,
COALESCE(EXTRACT(EPOCH FROM (now() - last_autoanalyze))::bigint, -1) AS last_autoanalyze_age
FROM pg_stat_all_tables
"#,
)
.fetch_all(pool)
.instrument(query_span)
.await?;
info!("Collecting vacuum stats for {} tables", rows.len());
let apply_span = info_span!("vacuum_stats.apply_metrics", tables = rows.len());
let _g = apply_span.enter();
for row in rows {
let schema: String = row.try_get("schemaname")?;
let table: String = row.try_get("relname")?;
let live: i64 = row.try_get("n_live_tup").unwrap_or(0);
let dead: i64 = row.try_get("n_dead_tup").unwrap_or(0);
let last_vac: i64 = row.try_get("last_vacuum_age").unwrap_or(-1);
let last_autovac: i64 = row.try_get("last_autovacuum_age").unwrap_or(-1);
let last_analyze: i64 = row.try_get("last_analyze_age").unwrap_or(-1);
let last_autoanalyze: i64 = row.try_get("last_autoanalyze_age").unwrap_or(-1);
self.tuples_live
.with_label_values(&[&schema, &table])
.set(live);
self.tuples_dead
.with_label_values(&[&schema, &table])
.set(dead);
self.last_vacuum_age
.with_label_values(&[&schema, &table])
.set(last_vac);
self.last_autovacuum_age
.with_label_values(&[&schema, &table])
.set(last_autovac);
self.last_analyze_age
.with_label_values(&[&schema, &table])
.set(last_analyze);
self.last_autoanalyze_age
.with_label_values(&[&schema, &table])
.set(last_autoanalyze);
let ratio = if live + dead > 0 {
dead as f64 / (live + dead) as f64
} else {
0.0
};
self.dead_ratio
.with_label_values(&[&schema, &table])
.set((ratio * 100.0) as i64);
debug!(
schema = %schema,
table = %table,
live,
dead,
last_vac,
last_autovac,
last_analyze,
last_autoanalyze,
dead_ratio_pct = (ratio * 100.0),
"updated vacuum stats metrics"
);
}
Ok(())
})
}
fn enabled_by_default(&self) -> bool {
true
}
}