@ -1,5 +1,3 @@
#![ allow(dead_code, unused_imports) ]
use std ::time ::SystemTime ;
use std ::time ::SystemTime ;
#[ cfg(feature = " enable_metrics " ) ]
#[ cfg(feature = " enable_metrics " ) ]
@ -10,9 +8,11 @@ use prometheus::{
HistogramVec , IntCounterVec , IntGaugeVec , TextEncoder ,
HistogramVec , IntCounterVec , IntGaugeVec , TextEncoder ,
} ;
} ;
use crate ::{ db ::DbConn , error ::Error , CONFIG } ;
use crate ::{ db ::DbConn , error ::Error } ;
#[ cfg(feature = " enable_metrics " ) ]
use crate ::CONFIG ;
#[ cfg(feature = " enable_metrics " ) ]
#[ cfg(feature = " enable_metrics " ) ]
use std ::sync ::{ Arc , RwLock } ;
use std ::sync ::RwLock ;
#[ cfg(feature = " enable_metrics " ) ]
#[ cfg(feature = " enable_metrics " ) ]
use std ::time ::UNIX_EPOCH ;
use std ::time ::UNIX_EPOCH ;
@ -51,17 +51,6 @@ static DB_CONNECTIONS_IDLE: Lazy<IntGaugeVec> = Lazy::new(|| {
. unwrap ( )
. unwrap ( )
} ) ;
} ) ;
#[ cfg(feature = " enable_metrics " ) ]
static DB_QUERY_DURATION_SECONDS : Lazy < HistogramVec > = Lazy ::new ( | | {
register_histogram_vec ! (
"vaultwarden_db_query_duration_seconds" ,
"Database query duration in seconds" ,
& [ "operation" ] ,
vec ! [ 0.001 , 0.005 , 0.01 , 0.025 , 0.05 , 0.1 , 0.25 , 0.5 , 1.0 , 2.5 , 5.0 ]
)
. unwrap ( )
} ) ;
// Authentication metrics
// Authentication metrics
#[ cfg(feature = " enable_metrics " ) ]
#[ cfg(feature = " enable_metrics " ) ]
static AUTH_ATTEMPTS_TOTAL : Lazy < IntCounterVec > = Lazy ::new ( | | {
static AUTH_ATTEMPTS_TOTAL : Lazy < IntCounterVec > = Lazy ::new ( | | {
@ -73,12 +62,6 @@ static AUTH_ATTEMPTS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
. unwrap ( )
. unwrap ( )
} ) ;
} ) ;
#[ cfg(feature = " enable_metrics " ) ]
static USER_SESSIONS_ACTIVE : Lazy < IntGaugeVec > = Lazy ::new ( | | {
register_int_gauge_vec ! ( "vaultwarden_user_sessions_active" , "Number of active user sessions" , & [ "user_type" ] )
. unwrap ( )
} ) ;
// Business metrics
// Business metrics
#[ cfg(feature = " enable_metrics " ) ]
#[ cfg(feature = " enable_metrics " ) ]
static USERS_TOTAL : Lazy < IntGaugeVec > =
static USERS_TOTAL : Lazy < IntGaugeVec > =
@ -129,24 +112,14 @@ pub fn update_db_connections(database: &str, active: i64, idle: i64) {
DB_CONNECTIONS_IDLE . with_label_values ( & [ database ] ) . set ( idle ) ;
DB_CONNECTIONS_IDLE . with_label_values ( & [ database ] ) . set ( idle ) ;
}
}
/// Observe database query duration
/// Increment authentication attempts (success/failure tracking)
#[ cfg(feature = " enable_metrics " ) ]
/// Tracks authentication success/failure by method (password, client_credentials, SSO, etc.)
pub fn observe_db_query_duration ( operation : & str , duration_seconds : f64 ) {
/// Called from src/api/identity.rs login() after each authentication attempt
DB_QUERY_DURATION_SECONDS . with_label_values ( & [ operation ] ) . observe ( duration_seconds ) ;
}
/// Increment authentication attempts
#[ cfg(feature = " enable_metrics " ) ]
#[ cfg(feature = " enable_metrics " ) ]
pub fn increment_auth_attempts ( method : & str , status : & str ) {
pub fn increment_auth_attempts ( method : & str , status : & str ) {
AUTH_ATTEMPTS_TOTAL . with_label_values ( & [ method , status ] ) . inc ( ) ;
AUTH_ATTEMPTS_TOTAL . with_label_values ( & [ method , status ] ) . inc ( ) ;
}
}
/// Update active user sessions
#[ cfg(feature = " enable_metrics " ) ]
pub fn update_user_sessions ( user_type : & str , count : i64 ) {
USER_SESSIONS_ACTIVE . with_label_values ( & [ user_type ] ) . set ( count ) ;
}
/// Cached business metrics data
/// Cached business metrics data
#[ cfg(feature = " enable_metrics " ) ]
#[ cfg(feature = " enable_metrics " ) ]
#[ derive(Clone) ]
#[ derive(Clone) ]
@ -285,23 +258,20 @@ pub fn gather_metrics() -> Result<String, Error> {
// No-op implementations when metrics are disabled
// No-op implementations when metrics are disabled
#[ cfg(not(feature = " enable_metrics " )) ]
#[ cfg(not(feature = " enable_metrics " )) ]
#[ allow(dead_code) ]
pub fn increment_http_requests ( _method : & str , _path : & str , _status : u16 ) { }
pub fn increment_http_requests ( _method : & str , _path : & str , _status : u16 ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
#[ cfg(not(feature = " enable_metrics " )) ]
#[ allow(dead_code) ]
pub fn observe_http_request_duration ( _method : & str , _path : & str , _duration_seconds : f64 ) { }
pub fn observe_http_request_duration ( _method : & str , _path : & str , _duration_seconds : f64 ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
#[ cfg(not(feature = " enable_metrics " )) ]
#[ allow(dead_code) ]
pub fn update_db_connections ( _database : & str , _active : i64 , _idle : i64 ) { }
pub fn update_db_connections ( _database : & str , _active : i64 , _idle : i64 ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
pub fn observe_db_query_duration ( _operation : & str , _duration_seconds : f64 ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
#[ cfg(not(feature = " enable_metrics " )) ]
pub fn increment_auth_attempts ( _method : & str , _status : & str ) { }
pub fn increment_auth_attempts ( _method : & str , _status : & str ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
pub fn update_user_sessions ( _user_type : & str , _count : i64 ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
#[ cfg(not(feature = " enable_metrics " )) ]
pub async fn update_business_metrics ( _conn : & mut DbConn ) -> Result < ( ) , Error > {
pub async fn update_business_metrics ( _conn : & mut DbConn ) -> Result < ( ) , Error > {
Ok ( ( ) )
Ok ( ( ) )
@ -311,9 +281,176 @@ pub async fn update_business_metrics(_conn: &mut DbConn) -> Result<(), Error> {
pub fn init_build_info ( ) { }
pub fn init_build_info ( ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
#[ cfg(not(feature = " enable_metrics " )) ]
#[ allow(dead_code) ]
pub fn update_uptime ( _start_time : SystemTime ) { }
pub fn update_uptime ( _start_time : SystemTime ) { }
#[ cfg(not(feature = " enable_metrics " )) ]
#[ cfg(not(feature = " enable_metrics " )) ]
pub fn gather_metrics ( ) -> Result < String , Error > {
pub fn gather_metrics ( ) -> Result < String , Error > {
Ok ( "Metrics not enabled" . to_string ( ) )
Ok ( "Metrics not enabled" . to_string ( ) )
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ cfg(feature = " enable_metrics " ) ]
mod metrics_enabled_tests {
use super ::* ;
#[ test ]
fn test_http_metrics_collection ( ) {
increment_http_requests ( "GET" , "/api/sync" , 200 ) ;
increment_http_requests ( "POST" , "/api/accounts/register" , 201 ) ;
increment_http_requests ( "GET" , "/api/sync" , 500 ) ;
observe_http_request_duration ( "GET" , "/api/sync" , 0.150 ) ;
observe_http_request_duration ( "POST" , "/api/accounts/register" , 0.300 ) ;
let metrics = gather_metrics ( ) . expect ( "Failed to gather metrics" ) ;
assert ! ( metrics . contains ( "vaultwarden_http_requests_total" ) ) ;
assert ! ( metrics . contains ( "vaultwarden_http_request_duration_seconds" ) ) ;
}
#[ test ]
fn test_database_metrics_collection ( ) {
update_db_connections ( "sqlite" , 5 , 10 ) ;
update_db_connections ( "postgresql" , 8 , 2 ) ;
let metrics = gather_metrics ( ) . expect ( "Failed to gather metrics" ) ;
assert ! ( metrics . contains ( "vaultwarden_db_connections_active" ) ) ;
assert ! ( metrics . contains ( "vaultwarden_db_connections_idle" ) ) ;
}
#[ test ]
fn test_authentication_metrics ( ) {
increment_auth_attempts ( "password" , "success" ) ;
increment_auth_attempts ( "password" , "failed" ) ;
increment_auth_attempts ( "webauthn" , "success" ) ;
let metrics = gather_metrics ( ) . expect ( "Failed to gather metrics" ) ;
assert ! ( metrics . contains ( "vaultwarden_auth_attempts_total" ) ) ;
assert ! ( metrics . contains ( "method=\"password\"" ) ) ;
assert ! ( metrics . contains ( "status=\"success\"" ) ) ;
assert ! ( metrics . contains ( "status=\"failed\"" ) ) ;
}
#[ test ]
fn test_build_info_initialization ( ) {
init_build_info ( ) ;
let start_time = SystemTime ::now ( ) ;
update_uptime ( start_time ) ;
let metrics = gather_metrics ( ) . expect ( "Failed to gather metrics" ) ;
assert ! ( metrics . contains ( "vaultwarden_build_info" ) ) ;
assert ! ( metrics . contains ( "vaultwarden_uptime_seconds" ) ) ;
}
#[ test ]
fn test_metrics_gathering ( ) {
increment_http_requests ( "GET" , "/api/sync" , 200 ) ;
update_db_connections ( "sqlite" , 1 , 5 ) ;
init_build_info ( ) ;
let metrics_output = gather_metrics ( ) ;
assert ! ( metrics_output . is_ok ( ) , "gather_metrics should succeed" ) ;
let metrics_text = metrics_output . unwrap ( ) ;
assert ! ( ! metrics_text . is_empty ( ) , "metrics output should not be empty" ) ;
assert ! ( metrics_text . contains ( "# HELP" ) , "metrics should have HELP lines" ) ;
assert ! ( metrics_text . contains ( "# TYPE" ) , "metrics should have TYPE lines" ) ;
assert ! ( metrics_text . contains ( "vaultwarden_" ) , "metrics should contain vaultwarden prefix" ) ;
}
#[ tokio::test ]
async fn test_business_metrics_collection_noop ( ) {
init_build_info ( ) ;
let metrics = gather_metrics ( ) . expect ( "Failed to gather metrics" ) ;
assert ! ( metrics . contains ( "vaultwarden_" ) , "Business metrics should be accessible" ) ;
}
#[ test ]
fn test_path_normalization ( ) {
increment_http_requests ( "GET" , "/api/sync" , 200 ) ;
increment_http_requests ( "GET" , "/api/accounts/123/profile" , 200 ) ;
increment_http_requests ( "POST" , "/api/organizations/456/users" , 201 ) ;
increment_http_requests ( "PUT" , "/api/ciphers/789" , 200 ) ;
let result = gather_metrics ( ) ;
assert ! ( result . is_ok ( ) , "gather_metrics should succeed with various paths" ) ;
let metrics_text = result . unwrap ( ) ;
assert ! ( ! metrics_text . is_empty ( ) , "metrics output should not be empty" ) ;
assert ! ( metrics_text . contains ( "vaultwarden_http_requests_total" ) , "should have http request metrics" ) ;
}
#[ test ]
fn test_concurrent_metrics_collection ( ) {
use std ::thread ;
let handles : Vec < _ > = ( 0 . . 10 ) . map ( | i | {
thread ::spawn ( move | | {
increment_http_requests ( "GET" , "/api/sync" , 200 ) ;
observe_http_request_duration ( "GET" , "/api/sync" , 0.1 + ( i as f64 * 0.01 ) ) ;
update_db_connections ( "sqlite" , i , 10 - i ) ;
} )
} ) . collect ( ) ;
for handle in handles {
handle . join ( ) . expect ( "Thread panicked" ) ;
}
let result = gather_metrics ( ) ;
assert ! ( result . is_ok ( ) , "metrics collection should be thread-safe" ) ;
assert ! ( ! result . unwrap ( ) . is_empty ( ) , "concurrent access should not corrupt metrics" ) ;
}
}
#[ cfg(not(feature = " enable_metrics " )) ]
mod metrics_disabled_tests {
use super ::* ;
#[ test ]
fn test_no_op_implementations ( ) {
increment_http_requests ( "GET" , "/api/sync" , 200 ) ;
observe_http_request_duration ( "GET" , "/api/sync" , 0.150 ) ;
update_db_connections ( "sqlite" , 5 , 10 ) ;
increment_auth_attempts ( "password" , "success" ) ;
init_build_info ( ) ;
let start_time = SystemTime ::now ( ) ;
update_uptime ( start_time ) ;
let result = gather_metrics ( ) ;
assert ! ( result . is_ok ( ) , "disabled metrics should return ok" ) ;
assert_eq ! ( result . unwrap ( ) , "Metrics not enabled" , "should return disabled message" ) ;
}
#[ tokio::test ]
async fn test_business_metrics_no_op ( ) {
let result = gather_metrics ( ) ;
assert ! ( result . is_ok ( ) , "disabled metrics should not panic" ) ;
assert_eq ! ( result . unwrap ( ) , "Metrics not enabled" , "should return disabled message" ) ;
}
#[ test ]
fn test_concurrent_no_op_calls ( ) {
use std ::thread ;
let handles : Vec < _ > = ( 0 . . 5 ) . map ( | i | {
thread ::spawn ( move | | {
increment_http_requests ( "GET" , "/test" , 200 ) ;
observe_http_request_duration ( "GET" , "/test" , 0.1 ) ;
update_db_connections ( "test" , i , 5 - i ) ;
increment_auth_attempts ( "password" , "success" ) ;
} )
} ) . collect ( ) ;
for handle in handles {
handle . join ( ) . expect ( "Thread panicked" ) ;
}
let result = gather_metrics ( ) ;
assert ! ( result . is_ok ( ) , "disabled metrics should be thread-safe" ) ;
assert_eq ! ( result . unwrap ( ) , "Metrics not enabled" , "disabled metrics should always return same message" ) ;
}
}
}