From 35ab7aef1713faf87bb1a466026e2d99864bf643 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Tue, 5 May 2026 09:23:10 -0700 Subject: [PATCH] fix: retrying known incorrect passwords repeatedly --- pgdog-stats/src/pool.rs | 5 + pgdog-stats/src/server.rs | 2 + pgdog/src/admin/show_stats.rs | 4 +- .../backend/auth/azure_workload_identity.rs | 2 +- pgdog/src/backend/auth/rds_iam.rs | 2 +- pgdog/src/backend/error.rs | 8 ++ pgdog/src/backend/pool/address.rs | 111 ++++++++++++-- pgdog/src/backend/pool/mod.rs | 2 + pgdog/src/backend/pool/monitor.rs | 5 + pgdog/src/backend/pool/password.rs | 79 ++++++++++ pgdog/src/backend/pool/stats.rs | 10 ++ pgdog/src/backend/pool/test/mod.rs | 135 ++++++++++++++++++ pgdog/src/backend/server.rs | 19 ++- pgdog/src/net/messages/error_response.rs | 5 + pgdog/src/stats/pools.rs | 28 ++++ 15 files changed, 398 insertions(+), 19 deletions(-) create mode 100644 pgdog/src/backend/pool/password.rs diff --git a/pgdog-stats/src/pool.rs b/pgdog-stats/src/pool.rs index a1a196050..0271b9e90 100644 --- a/pgdog-stats/src/pool.rs +++ b/pgdog-stats/src/pool.rs @@ -60,6 +60,8 @@ pub struct Counts { pub reads: usize, /// Number of write transactions. pub writes: usize, + /// Password attempts. + pub auth_attempts: usize, } impl Sub for Counts { @@ -91,6 +93,7 @@ impl Sub for Counts { connect_count: self.connect_count.saturating_sub(rhs.connect_count), reads: self.reads.saturating_sub(rhs.reads), writes: self.writes.saturating_sub(rhs.writes), + auth_attempts: self.auth_attempts.saturating_sub(rhs.auth_attempts), } } } @@ -124,6 +127,7 @@ impl Add for Counts { connect_time: self.connect_time.saturating_add(rhs.connect_time), reads: self.reads.saturating_add(rhs.reads), writes: self.writes.saturating_add(rhs.writes), + auth_attempts: self.auth_attempts.saturating_add(rhs.auth_attempts), } } } @@ -161,6 +165,7 @@ impl Div for Counts { connect_count: self.connect_count.checked_div(rhs).unwrap_or(0), reads: self.reads.checked_div(rhs).unwrap_or(0), writes: self.writes.checked_div(rhs).unwrap_or(0), + auth_attempts: self.auth_attempts.checked_div(rhs).unwrap_or(0), } } } diff --git a/pgdog-stats/src/server.rs b/pgdog-stats/src/server.rs index 00be33452..1db2145df 100644 --- a/pgdog-stats/src/server.rs +++ b/pgdog-stats/src/server.rs @@ -52,10 +52,12 @@ impl Add for PoolCounts { errors: self.errors + rhs.errors, cleaned: self.cleaned + rhs.cleaned, prepared_sync: self.prepared_sync + rhs.prepared_sync, + // These are not counted by each server stats. connect_count: self.connect_count, connect_time: self.connect_time, writes: self.writes, reads: self.reads, + auth_attempts: self.auth_attempts, } } } diff --git a/pgdog/src/admin/show_stats.rs b/pgdog/src/admin/show_stats.rs index a2059f693..48eebdcf2 100644 --- a/pgdog/src/admin/show_stats.rs +++ b/pgdog/src/admin/show_stats.rs @@ -51,6 +51,7 @@ impl Command for ShowStats { Field::numeric(&format!("{}_connect_count", prefix)), Field::numeric(&format!("{}_reads", prefix)), Field::numeric(&format!("{}_writes", prefix)), + Field::numeric(&format!("{}_auth_attempts", prefix)), ] }) .collect::>(), @@ -99,7 +100,8 @@ impl Command for ShowStats { .add(millis(stat.connect_time)) .add(stat.connect_count) .add(stat.reads) - .add(stat.writes); + .add(stat.writes) + .add(stat.auth_attempts); } messages.push(dr.message()?); diff --git a/pgdog/src/backend/auth/azure_workload_identity.rs b/pgdog/src/backend/auth/azure_workload_identity.rs index 4e2036a67..8c7e044fa 100644 --- a/pgdog/src/backend/auth/azure_workload_identity.rs +++ b/pgdog/src/backend/auth/azure_workload_identity.rs @@ -89,7 +89,7 @@ mod tests { port: 5432, database_name: "postgres".into(), user: "db_user".into(), - passwords: vec![String::new()], + passwords: vec![String::new().into()], database_number: 0, server_auth: ServerAuth::AzureWorkloadIdentity, server_iam_region: None, diff --git a/pgdog/src/backend/auth/rds_iam.rs b/pgdog/src/backend/auth/rds_iam.rs index 6aa69dfd6..5c8e09bae 100644 --- a/pgdog/src/backend/auth/rds_iam.rs +++ b/pgdog/src/backend/auth/rds_iam.rs @@ -155,7 +155,7 @@ mod tests { port: 5432, database_name: "postgres".into(), user: "db_user".into(), - passwords: vec![String::new()], + passwords: vec![String::new().into()], database_number: 0, server_auth: ServerAuth::RdsIam, server_iam_region: Some("us-east-1".into()), diff --git a/pgdog/src/backend/error.rs b/pgdog/src/backend/error.rs index 65aa3e551..3fd5525e4 100644 --- a/pgdog/src/backend/error.rs +++ b/pgdog/src/backend/error.rs @@ -181,4 +181,12 @@ impl Error { _ => false, } } + + pub fn is_auth(&self) -> bool { + match self { + Self::Auth(_) => true, + Self::ConnectionError(err) => err.code == "28000" || err.is_bad_password(), + _ => false, + } + } } diff --git a/pgdog/src/backend/pool/address.rs b/pgdog/src/backend/pool/address.rs index 46c3e834e..37a28b3bc 100644 --- a/pgdog/src/backend/pool/address.rs +++ b/pgdog/src/backend/pool/address.rs @@ -1,11 +1,13 @@ //! Server address. use std::net::{SocketAddr, ToSocketAddrs}; +use std::ops::Deref; use pgdog_config::users::PasswordKind; use pgdog_config::Role; use serde::{Deserialize, Serialize}; use url::Url; +use super::Password; use crate::backend::{pool::dns_cache::DnsCache, Error}; use crate::config::{config, Database, ServerAuth, User}; @@ -21,7 +23,7 @@ pub struct Address { /// Username. pub user: String, /// Password. - pub passwords: Vec, + pub passwords: Vec, /// Server auth mode for backend connections. #[serde(default)] pub server_auth: ServerAuth, @@ -41,7 +43,7 @@ impl From
for pgdog_stats::Address { port: value.port, database_name: value.database_name, user: value.user, - passwords: value.passwords.clone(), + passwords: value.passwords.iter().map(|p| p.deref().clone()).collect(), server_auth: value.server_auth, server_iam_region: value.server_iam_region, database_number: value.database_number, @@ -72,14 +74,14 @@ impl Address { passwords: if server_auth.is_external_identity() { vec![] } else if let Some(password) = database.password.clone() { - vec![password] + vec![password.into()] } else if let Some(password) = user.server_password.clone() { - vec![password] + vec![password.into()] } else { user.passwords() .into_iter() .filter(|p| matches!(p, PasswordKind::Plain(_))) - .map(|p| p.to_string()) + .map(|p| p.to_string().into()) .collect() }, server_auth, @@ -89,14 +91,22 @@ impl Address { } } - pub async fn auth_secrets(&self) -> Result, Error> { - match self.server_auth { - ServerAuth::Password => Ok(self.passwords.clone()), - ServerAuth::RdsIam => Ok(vec![crate::backend::auth::rds_iam::token(self).await?]), - ServerAuth::AzureWorkloadIdentity => Ok(vec![ - crate::backend::auth::azure_workload_identity::token(self).await?, - ]), - } + /// Get address passwords, in valid order. + pub async fn auth_secrets(&self) -> Result, Error> { + let mut secrets = match self.server_auth { + ServerAuth::Password => self.passwords.clone(), + ServerAuth::RdsIam => vec![crate::backend::auth::rds_iam::token(self).await?.into()], + ServerAuth::AzureWorkloadIdentity => { + vec![crate::backend::auth::azure_workload_identity::token(self) + .await? + .into()] + } + }; + + // Give the valid password first. + secrets.sort_by_cached_key(|p| !p.is_valid()); + + Ok(secrets) } pub async fn addr(&self) -> Result { @@ -159,7 +169,7 @@ impl TryFrom for Address { Ok(Self { host, port, - passwords: vec![password], + passwords: vec![password.into()], user, database_name, server_auth: ServerAuth::Password, @@ -304,6 +314,79 @@ mod test { assert_eq!(secret, "token-from-iam"); } + #[tokio::test] + async fn test_auth_secrets_returns_valid_password_first() { + let mut addr = Address::new_test(); + let invalid1: Password = "invalid1".into(); + let invalid2: Password = "invalid2".into(); + let valid: Password = "valid".into(); + invalid1.valid(false); + invalid2.valid(false); + addr.passwords = vec![invalid1, valid, invalid2]; + + let secrets = addr.auth_secrets().await.unwrap(); + assert_eq!(secrets.len(), 3); + assert_eq!(secrets.first().unwrap(), "valid"); + assert!(secrets.first().unwrap().is_valid()); + + // Even if the valid password is last, it should still come first. + let mut addr = Address::new_test(); + let invalid1: Password = "invalid1".into(); + let invalid2: Password = "invalid2".into(); + let valid: Password = "valid".into(); + invalid1.valid(false); + invalid2.valid(false); + addr.passwords = vec![invalid1, invalid2, valid]; + + let secrets = addr.auth_secrets().await.unwrap(); + assert_eq!(secrets.first().unwrap(), "valid"); + + // With multiple valid passwords, a valid one is still first. + let mut addr = Address::new_test(); + let invalid: Password = "invalid".into(); + invalid.valid(false); + addr.passwords = vec![invalid, "valid_a".into(), "valid_b".into()]; + + let secrets = addr.auth_secrets().await.unwrap(); + let head = secrets.first().unwrap(); + assert!(head.is_valid()); + assert!(head == "valid_a" || head == "valid_b"); + + // Flipping validity at runtime changes which password comes first. + let mut addr = Address::new_test(); + let first: Password = "first".into(); + let second: Password = "second".into(); + addr.passwords = vec![first.clone(), second.clone()]; + + // Both valid: order is preserved (sort is stable on the !is_valid key). + let secrets = addr.auth_secrets().await.unwrap(); + assert_eq!(secrets.first().unwrap(), "first"); + assert_eq!(secrets.get(1).unwrap(), "second"); + + // Mark "first" invalid — "second" must now win. + first.valid(false); + let secrets = addr.auth_secrets().await.unwrap(); + assert_eq!(secrets.first().unwrap(), "second"); + assert!(secrets.first().unwrap().is_valid()); + assert_eq!(secrets.get(1).unwrap(), "first"); + assert!(!secrets.get(1).unwrap().is_valid()); + + // Mark "second" invalid too — no valid password, but order stays stable. + second.valid(false); + let secrets = addr.auth_secrets().await.unwrap(); + assert_eq!(secrets.first().unwrap(), "first"); + assert!(!secrets.first().unwrap().is_valid()); + assert_eq!(secrets.get(1).unwrap(), "second"); + + // Restore "first" — it should be returned first again. + first.valid(true); + let secrets = addr.auth_secrets().await.unwrap(); + assert_eq!(secrets.first().unwrap(), "first"); + assert!(secrets.first().unwrap().is_valid()); + assert_eq!(secrets.get(1).unwrap(), "second"); + assert!(!secrets.get(1).unwrap().is_valid()); + } + #[tokio::test] async fn test_auth_secret_azure_workload_identity_mode_uses_generator() { let mut addr = Address::new_test(); diff --git a/pgdog/src/backend/pool/mod.rs b/pgdog/src/backend/pool/mod.rs index 40a4bec39..ca45b3023 100644 --- a/pgdog/src/backend/pool/mod.rs +++ b/pgdog/src/backend/pool/mod.rs @@ -18,6 +18,7 @@ pub mod mapping; pub mod mirror_stats; pub mod monitor; pub mod oids; +pub mod password; pub mod pool_impl; pub mod request; pub mod shard; @@ -38,6 +39,7 @@ pub use lsn_monitor::LsnStats; pub use mirror_stats::MirrorStats; pub use monitor::Monitor; pub use oids::Oids; +pub use password::Password; pub use pool_impl::Pool; pub use request::Request; pub use shard::Shard; diff --git a/pgdog/src/backend/pool/monitor.rs b/pgdog/src/backend/pool/monitor.rs index 80b1f9541..13fb807f4 100644 --- a/pgdog/src/backend/pool/monitor.rs +++ b/pgdog/src/backend/pool/monitor.rs @@ -350,11 +350,16 @@ impl Monitor { let mut guard = pool.lock(); guard.stats.counts.connect_count += 1; guard.stats.counts.connect_time += elapsed; + guard.stats.counts.auth_attempts += conn.password_attempts(); } return Ok(conn); } Ok(Err(err)) => { + // We tried all passwords and they were all wrong. + if err.is_auth() { + pool.lock().stats.counts.auth_attempts += pool.addr().passwords.len(); + } error!( "{}error connecting to server: {} [{}]", if attempt > 0 { diff --git a/pgdog/src/backend/pool/password.rs b/pgdog/src/backend/pool/password.rs new file mode 100644 index 000000000..7b7de3507 --- /dev/null +++ b/pgdog/src/backend/pool/password.rs @@ -0,0 +1,79 @@ +use std::{ + hash::Hash, + ops::Deref, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Password { + pub(crate) password: String, + pub(crate) valid: Arc, +} + +impl From for Password { + fn from(password: String) -> Self { + Self { + password, + valid: Arc::new(AtomicBool::new(true)), + } + } +} + +impl From<&str> for Password { + fn from(password: &str) -> Self { + Self { + password: password.to_string(), + valid: Arc::new(AtomicBool::new(true)), + } + } +} + +impl Hash for Password { + fn hash(&self, state: &mut H) { + self.password.hash(state); + self.is_valid().hash(state); + } +} + +impl Eq for Password {} + +impl PartialEq<&str> for Password { + fn eq(&self, other: &&str) -> bool { + self.password.as_str() == *other + } +} + +impl PartialEq for Password { + fn eq(&self, other: &str) -> bool { + self.password.as_str() == other + } +} + +impl PartialEq for Password { + fn eq(&self, other: &Self) -> bool { + self.password == other.password + } +} + +impl Deref for Password { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.password + } +} + +impl Password { + pub(crate) fn is_valid(&self) -> bool { + self.valid.load(Ordering::Relaxed) + } + + pub(crate) fn valid(&self, valid: bool) { + self.valid.store(valid, Ordering::Relaxed) + } +} diff --git a/pgdog/src/backend/pool/stats.rs b/pgdog/src/backend/pool/stats.rs index 0ad625c79..4f99f8f4a 100644 --- a/pgdog/src/backend/pool/stats.rs +++ b/pgdog/src/backend/pool/stats.rs @@ -179,6 +179,7 @@ mod tests { connect_count: 8, reads: 25, writes: 50, + auth_attempts: 30, } .into(); @@ -205,6 +206,7 @@ mod tests { connect_count: 4, reads: 10, writes: 20, + auth_attempts: 20, } .into(); @@ -232,6 +234,7 @@ mod tests { assert_eq!(result.connect_count, 12); assert_eq!(result.reads, 35); assert_eq!(result.writes, 70); + assert_eq!(result.auth_attempts, 50); } #[test] @@ -259,6 +262,7 @@ mod tests { connect_count: 8, reads: 25, writes: 50, + auth_attempts: 50, } .into(); @@ -285,6 +289,7 @@ mod tests { connect_count: 4, reads: 10, writes: 20, + auth_attempts: 30, } .into(); @@ -312,6 +317,7 @@ mod tests { assert_eq!(result.connect_count, 4); assert_eq!(result.reads, 15); assert_eq!(result.writes, 30); + assert_eq!(result.auth_attempts, 20); } #[test] @@ -361,6 +367,7 @@ mod tests { connect_count: 4, reads: 10, writes: 20, + auth_attempts: 10, } .into(); @@ -388,6 +395,7 @@ mod tests { assert_eq!(result.connect_count, 2); assert_eq!(result.reads, 5); assert_eq!(result.writes, 10); + assert_eq!(result.auth_attempts, 5); } #[test] @@ -430,6 +438,7 @@ mod tests { connect_count: 8, reads: 10, writes: 25, + auth_attempts: 100, } .into(); @@ -477,6 +486,7 @@ mod tests { assert_eq!(result.connect_time, Duration::from_secs(1)); assert_eq!(result.reads, 10); assert_eq!(result.writes, 25); + assert_eq!(result.auth_attempts, 100); } #[test] diff --git a/pgdog/src/backend/pool/test/mod.rs b/pgdog/src/backend/pool/test/mod.rs index 53800a8dc..a65829a16 100644 --- a/pgdog/src/backend/pool/test/mod.rs +++ b/pgdog/src/backend/pool/test/mod.rs @@ -10,6 +10,7 @@ use tokio::task::yield_now; use tokio::time::{sleep, timeout, Instant}; use tokio_util::task::TaskTracker; +use crate::backend::ConnectReason; use crate::net::ProtocolMessage; use crate::net::{Parse, Protocol, Query, Sync}; use crate::state::State; @@ -769,6 +770,140 @@ async fn test_move_conns_destination_serves_after_launch() { assert_eq!(destination.lock().idle(), 1); } +fn auth_pool(passwords: Vec) -> Pool { + let config = Config { + inner: pgdog_stats::Config { + max: 1, + min: 0, + connect_attempts: 1, + ..Config::default().inner + }, + }; + + Pool::new(&PoolConfig { + address: Address { + host: "127.0.0.1".into(), + port: 5432, + database_name: "pgdog".into(), + user: "pgdog".into(), + passwords, + ..Default::default() + }, + config, + }) +} + +#[tokio::test] +async fn test_auth_attempts_single_good_password() { + crate::logger(); + + let pool = auth_pool(vec!["pgdog".into()]); + assert_eq!(pool.state().stats.counts.auth_attempts, 0); + + let conn = pool.standalone(ConnectReason::Other).await.unwrap(); + drop(conn); + + // Single valid password — exactly one attempt, which succeeded. + assert_eq!(pool.state().stats.counts.auth_attempts, 1); + assert!(pool.addr().passwords[0].is_valid()); +} + +#[tokio::test] +async fn test_auth_attempts_good_first_among_bad() { + crate::logger(); + + let pool = auth_pool(vec!["pgdog".into(), "wrong1".into(), "wrong2".into()]); + + let conn = pool.standalone(ConnectReason::Other).await.unwrap(); + drop(conn); + + // First password worked on attempt #1; the bad ones were never tried. + assert_eq!(pool.state().stats.counts.auth_attempts, 1); + assert!(pool.addr().passwords[0].is_valid()); + assert!(pool.addr().passwords[1].is_valid()); + assert!(pool.addr().passwords[2].is_valid()); +} + +#[tokio::test] +async fn test_auth_attempts_good_last_among_bad() { + crate::logger(); + + let pool = auth_pool(vec!["wrong1".into(), "wrong2".into(), "pgdog".into()]); + + let conn = pool.standalone(ConnectReason::Other).await.unwrap(); + drop(conn); + + // Each bad password was tried before the good one worked on attempt #3. + assert_eq!(pool.state().stats.counts.auth_attempts, 3); + let pwds = &pool.addr().passwords; + assert!( + !pwds[0].is_valid(), + "first wrong password should be invalid" + ); + assert!( + !pwds[1].is_valid(), + "second wrong password should be invalid" + ); + assert!(pwds[2].is_valid(), "good password should remain valid"); + + // Second connect: auth_secrets() sorts the valid one first, so we + // succeed on the very first try — counter only bumps by 1. + let conn = pool.standalone(ConnectReason::Other).await.unwrap(); + drop(conn); + assert_eq!(pool.state().stats.counts.auth_attempts, 4); +} + +#[tokio::test] +async fn test_auth_attempts_all_bad_passwords() { + crate::logger(); + + let pool = auth_pool(vec!["wrong1".into(), "wrong2".into(), "wrong3".into()]); + assert_eq!(pool.state().stats.counts.auth_attempts, 0); + + let err = pool.standalone(ConnectReason::Other).await; + assert!(err.is_err(), "all-bad-password connect must fail"); + + // Every password was tried and rejected. + assert_eq!(pool.state().stats.counts.auth_attempts, 3); + for pwd in &pool.addr().passwords { + assert!(!pwd.is_valid()); + } + + // A second attempt should bump the counter by another N — the pool has + // no valid password, so it must re-try them all. + let err = pool.standalone(ConnectReason::Other).await; + assert!(err.is_err()); + assert_eq!(pool.state().stats.counts.auth_attempts, 6); +} + +#[tokio::test] +async fn test_auth_attempts_single_bad_password() { + crate::logger(); + + let pool = auth_pool(vec!["wrong".into()]); + + let err = pool.standalone(ConnectReason::Other).await; + assert!(err.is_err()); + assert_eq!(pool.state().stats.counts.auth_attempts, 1); + assert!(!pool.addr().passwords[0].is_valid()); +} + +#[tokio::test] +async fn test_auth_attempts_recovers_after_password_added() { + crate::logger(); + + // Start with all-bad — first connect fails and marks them all invalid. + let pool = auth_pool(vec!["wrong1".into(), "wrong2".into()]); + + assert!(pool.standalone(ConnectReason::Other).await.is_err()); + assert_eq!(pool.state().stats.counts.auth_attempts, 2); + + // Marking one of them valid (e.g. password rotation discovered) must + // not retroactively change the counter. + pool.addr().passwords[0].valid(true); + assert_eq!(pool.state().stats.counts.auth_attempts, 2); +} + #[tokio::test] async fn test_lsn_monitor() { crate::logger(); diff --git a/pgdog/src/backend/server.rs b/pgdog/src/backend/server.rs index cf323788d..396c3da3c 100644 --- a/pgdog/src/backend/server.rs +++ b/pgdog/src/backend/server.rs @@ -64,6 +64,7 @@ pub struct Server { pooler_mode: PoolerMode, stream_buffer: MessageBuffer, disconnect_reason: Option, + password_attempts: usize, } impl MemoryUsage for Server { @@ -78,6 +79,7 @@ impl MemoryUsage for Server { + 7 * std::mem::size_of::() + std::mem::size_of::() + self.stream_buffer.capacity() + + self.password_attempts.memory_usage() } } @@ -99,9 +101,13 @@ impl Server { ) .await { - Ok(server) => return Ok(server), + Ok(mut server) => { + auth_secret.valid(true); + server.password_attempts = idx + 1; + return Ok(server); + } Err(Error::ConnectionError(error)) => { - if error.code == "28P01" { + if error.is_bad_password() { warn!( "{}/{} password is incorrect, {} password candidates remaining [{}]", idx + 1, @@ -109,6 +115,7 @@ impl Server { total - idx - 1, addr ); + auth_secret.valid(false); continue; } else { return Err(Error::ConnectionError(error)); @@ -334,6 +341,7 @@ impl Server { pooler_mode: PoolerMode::Transaction, stream_buffer: MessageBuffer::new(config.config.memory.message_buffer), disconnect_reason: None, + password_attempts: 1, // This is going to be changed by parent caller. }; server.stats.memory_used(server.memory_stats()); // Stream capacity. @@ -986,6 +994,12 @@ impl Server { &self.id } + /// Number of password attempts it took to authenticate this connection. + #[inline] + pub fn password_attempts(&self) -> usize { + self.password_attempts + } + /// How old this connection is. #[inline] pub fn age(&self, instant: Instant) -> Duration { @@ -1173,6 +1187,7 @@ pub mod test { disconnect_reason: None, statement_executed: false, sending_request: false, + password_attempts: 1, } } } diff --git a/pgdog/src/net/messages/error_response.rs b/pgdog/src/net/messages/error_response.rs index 581554c1a..f278d7e84 100644 --- a/pgdog/src/net/messages/error_response.rs +++ b/pgdog/src/net/messages/error_response.rs @@ -35,6 +35,11 @@ impl Default for ErrorResponse { } impl ErrorResponse { + /// True if this error response signals an invalid password (SQLSTATE 28P01). + pub fn is_bad_password(&self) -> bool { + self.code == "28P01" + } + /// Authentication error. pub fn auth(user: &str, database: &str) -> ErrorResponse { ErrorResponse { diff --git a/pgdog/src/stats/pools.rs b/pgdog/src/stats/pools.rs index b315f76ed..ce4f6f385 100644 --- a/pgdog/src/stats/pools.rs +++ b/pgdog/src/stats/pools.rs @@ -84,6 +84,8 @@ impl Pools { let mut total_writes = vec![]; let mut avg_writes = vec![]; let mut total_sv_xact_idle = vec![]; + let mut total_auth_attempts = vec![]; + let mut avg_auth_attempts = vec![]; let general = &crate::config::config().config.general; @@ -303,6 +305,16 @@ impl Pools { labels: labels.clone(), measurement: backend::stats::idle_in_transaction(&pool).into(), }); + + total_auth_attempts.push(Measurement { + labels: labels.clone(), + measurement: totals.auth_attempts.into(), + }); + + avg_auth_attempts.push(Measurement { + labels: labels.clone(), + measurement: averages.auth_attempts.into(), + }); } } } @@ -656,6 +668,22 @@ impl Pools { metric_type: None, })); + metrics.push(Metric::new(PoolMetric { + name: "total_auth_attempts".into(), + measurements: total_auth_attempts, + help: "Total number of server authentication attempts.".into(), + unit: None, + metric_type: Some("counter".into()), + })); + + metrics.push(Metric::new(PoolMetric { + name: "avg_auth_attempts".into(), + measurements: avg_auth_attempts, + help: "Average number of server authentication attempts per statistics period.".into(), + unit: None, + metric_type: None, + })); + Pools { metrics } }