use crate::pool::Pool;
use ecat_data::{QUERY_TIMEOUTS, TRANSACTIONS_LEAKED};
use prometheus::core::{Collector, Desc};
use prometheus::proto::{Counter, Gauge, LabelPair, Metric, MetricFamily, MetricType};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
static POOL_TIMEOUTS: AtomicU64 = AtomicU64::new(0);
pub(crate) fn count_pool_timeout() {
POOL_TIMEOUTS.fetch_add(1, Ordering::Relaxed);
}
pub fn register_pool_metrics(backend: &'static str, pool: &Pool) {
let pools = POOLS.get_or_init(|| {
let pools = Arc::new(Pools::new());
let _ = ecat_metrics::registry().register(Box::new(Registered(Arc::clone(&pools))));
pools
});
pools.add(backend, pool.clone());
}
struct Registered(Arc<Pools>);
impl Collector for Registered {
fn desc(&self) -> Vec<&Desc> {
self.0.descs.iter().collect()
}
fn collect(&self) -> Vec<MetricFamily> {
self.0.collect()
}
}
static POOLS: OnceLock<Arc<Pools>> = OnceLock::new();
struct Pools {
pools: Mutex<Vec<(&'static str, Pool)>>,
descs: [Desc; 4],
}
impl Pools {
fn new() -> Self {
Self {
pools: Mutex::new(Vec::new()),
descs: [
desc(
"ecat_rdbms_pool_connections",
"Connections in the RDBMS connection pool",
&["backend", "state"],
),
desc(
"ecat_rdbms_pool_timeouts_total",
"Total RDBMS pool acquire timeouts",
&["backend"],
),
desc(
"ecat_rdbms_query_timeout_total",
"Total RDBMS query timeouts",
&["backend"],
),
desc(
"ecat_rdbms_transactions_leaked_total",
"Total RDBMS transactions dropped without commit or rollback",
&["backend"],
),
],
}
}
fn add(&self, backend: &'static str, pool: Pool) {
let mut pools = self.pools.lock().unwrap();
match pools.iter_mut().find(|(name, _)| *name == backend) {
Some(slot) => slot.1 = pool,
None => pools.push((backend, pool)),
}
}
}
impl Collector for Pools {
fn desc(&self) -> Vec<&Desc> {
self.descs.iter().collect()
}
fn collect(&self) -> Vec<MetricFamily> {
let pools = self.pools.lock().unwrap();
let mut connections = Vec::with_capacity(pools.len() * 2);
let mut pool_timeouts = Vec::with_capacity(pools.len());
let mut query_timeouts = Vec::with_capacity(pools.len());
let mut leaked = Vec::with_capacity(pools.len());
let timeouts = count(QUERY_TIMEOUTS.load(Ordering::Relaxed));
let leaks = count(TRANSACTIONS_LEAKED.load(Ordering::Relaxed));
let acquire_timeouts = count(POOL_TIMEOUTS.load(Ordering::Relaxed));
for (backend, pool) in pools.iter() {
let backend = *backend;
let size = pool.size();
let idle = pool.idle();
connections.push(gauge(
labels(&[("backend", backend), ("state", "idle")]),
f64::from(idle),
));
connections.push(gauge(
labels(&[("backend", backend), ("state", "active")]),
f64::from(size.saturating_sub(idle)),
));
pool_timeouts.push(counter(labels(&[("backend", backend)]), acquire_timeouts));
query_timeouts.push(counter(labels(&[("backend", backend)]), timeouts));
leaked.push(counter(labels(&[("backend", backend)]), leaks));
}
vec![
family(
"ecat_rdbms_pool_connections",
"Connections in the RDBMS connection pool",
MetricType::GAUGE,
connections,
),
family(
"ecat_rdbms_pool_timeouts_total",
"Total RDBMS pool acquire timeouts",
MetricType::COUNTER,
pool_timeouts,
),
family(
"ecat_rdbms_query_timeout_total",
"Total RDBMS query timeouts",
MetricType::COUNTER,
query_timeouts,
),
family(
"ecat_rdbms_transactions_leaked_total",
"Total RDBMS transactions dropped without commit or rollback",
MetricType::COUNTER,
leaked,
),
]
}
}
fn count(v: u64) -> f64 {
v as f64
}
fn desc(name: &str, help: &str, labels: &[&str]) -> Desc {
Desc::new(
name.to_string(),
help.to_string(),
labels.iter().map(|l| (*l).to_string()).collect(),
HashMap::new(),
)
.expect("指标名与标签名合法")
}
fn labels(pairs: &[(&str, &str)]) -> Vec<LabelPair> {
pairs
.iter()
.map(|(name, value)| {
let mut l = LabelPair::default();
l.set_name((*name).to_string());
l.set_value((*value).to_string());
l
})
.collect()
}
fn gauge(labels: Vec<LabelPair>, value: f64) -> Metric {
let mut m = Metric::default();
m.set_label(labels.into());
let mut g = Gauge::default();
g.set_value(value);
m.set_gauge(g);
m
}
fn counter(labels: Vec<LabelPair>, value: f64) -> Metric {
let mut m = Metric::default();
m.set_label(labels.into());
let mut c = Counter::default();
c.set_value(value);
m.set_counter(c);
m
}
fn family(name: &str, help: &str, kind: MetricType, metrics: Vec<Metric>) -> MetricFamily {
let mut f = MetricFamily::default();
f.set_name(name.to_string());
f.set_help(help.to_string());
f.set_field_type(kind);
f.set_metric(metrics.into());
f
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SqlxClient;
use ecat_data::RdbmsError;
async fn client() -> SqlxClient {
SqlxClient::connect("sqlite::memory:").await.unwrap()
}
fn sample(text: &str, prefix: &str) -> Option<f64> {
text.lines()
.find(|l| l.starts_with(prefix) && !l.starts_with('#'))
.and_then(|l| l.rsplit(' ').next())
.and_then(|v| v.parse().ok())
}
fn assert_bracketed(text: &str, prefix: &str, lo: u64, hi: u64) {
let v =
sample(text, prefix).unwrap_or_else(|| panic!("缺指标 {prefix},实际输出:\n{text}"));
assert!(
v >= lo as f64 && v <= hi as f64,
"{prefix} 应落在 [{lo}, {hi}],实际 {v} —— 必须直读进程级静态量"
);
}
#[tokio::test]
async fn registers_all_four_metrics_for_the_backend() {
let c = client().await;
register_pool_metrics("sqlx-sqlite-test", c.pool());
let q_lo = QUERY_TIMEOUTS.fetch_add(1, Ordering::Relaxed) + 1;
let l_lo = TRANSACTIONS_LEAKED.fetch_add(1, Ordering::Relaxed) + 1;
let text = ecat_metrics::metrics_text();
let q_hi = QUERY_TIMEOUTS.load(Ordering::Relaxed);
let l_hi = TRANSACTIONS_LEAKED.load(Ordering::Relaxed);
for name in [
"ecat_rdbms_pool_connections",
"ecat_rdbms_pool_timeouts_total",
"ecat_rdbms_query_timeout_total",
"ecat_rdbms_transactions_leaked_total",
] {
assert!(
text.contains(&format!("{name}{{backend=\"sqlx-sqlite-test\"")),
"缺指标 {name},实际输出:\n{text}"
);
}
assert_bracketed(
&text,
"ecat_rdbms_query_timeout_total{backend=\"sqlx-sqlite-test\"}",
q_lo,
q_hi,
);
assert_bracketed(
&text,
"ecat_rdbms_transactions_leaked_total{backend=\"sqlx-sqlite-test\"}",
l_lo,
l_hi,
);
}
#[tokio::test]
async fn gauge_reflects_live_pool_state() {
let c = client().await;
register_pool_metrics("sqlx-live-test", c.pool());
let idle = || {
sample(
&ecat_metrics::metrics_text(),
"ecat_rdbms_pool_connections{backend=\"sqlx-live-test\",state=\"idle\"}",
)
};
let active = || {
sample(
&ecat_metrics::metrics_text(),
"ecat_rdbms_pool_connections{backend=\"sqlx-live-test\",state=\"active\"}",
)
};
assert_eq!(idle(), Some(1.0), "刚建好池应当有一条空闲连接");
assert_eq!(active(), Some(0.0));
let guard = c.pool().acquire().await.unwrap();
assert_eq!(idle(), Some(0.0), "连接被借出后不应再算空闲");
assert_eq!(active(), Some(1.0));
drop(guard);
}
#[tokio::test]
async fn pool_timeout_is_counted_by_db_err() {
let before = POOL_TIMEOUTS.load(Ordering::Relaxed);
let err = crate::db_err(sqlx::Error::PoolTimedOut);
assert!(matches!(err, RdbmsError::Database(_)), "got: {err:?}");
let counted = POOL_TIMEOUTS.load(Ordering::Relaxed);
assert_eq!(counted, before + 1);
let err = crate::db_err(sqlx::Error::RowNotFound);
assert!(matches!(err, RdbmsError::Database(_)));
assert_eq!(
POOL_TIMEOUTS.load(Ordering::Relaxed),
counted,
"非 PoolTimedOut 不该计数"
);
let c = client().await;
register_pool_metrics("sqlx-timeout-test", c.pool());
let text = ecat_metrics::metrics_text();
let hi = POOL_TIMEOUTS.load(Ordering::Relaxed);
assert_bracketed(
&text,
"ecat_rdbms_pool_timeouts_total{backend=\"sqlx-timeout-test\"}",
counted,
hi,
);
}
#[tokio::test]
async fn same_backend_registration_replaces_instead_of_duplicating() {
let a = client().await;
let b = client().await;
register_pool_metrics("sqlx-dup-test", a.pool());
register_pool_metrics("sqlx-dup-test", b.pool());
let text = ecat_metrics::metrics_text();
let hits = text
.lines()
.filter(|l| {
l.starts_with(
"ecat_rdbms_pool_connections{backend=\"sqlx-dup-test\",state=\"idle\"}",
)
})
.count();
assert_eq!(hits, 1, "重复注册应覆盖而不是追加,实际输出:\n{text}");
}
}