1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-09-16 22:52:43 +00:00
vaultwarden/tests/metrics_integration_test.rs
Ross Golder 3cbe12aea6
feat: Add comprehensive Prometheus metrics support
Implements optional Prometheus metrics collection with secure endpoint for monitoring and observability.

Features:
- Disabled by default, enabled via ENABLE_METRICS environment variable
- Secure token-based authentication with Argon2 hashing support
- Comprehensive metrics collection across all system components
- Conditional compilation with enable_metrics feature flag
- HTTP request instrumentation with automatic path normalization
- Database connection pool and query performance monitoring
- Authentication attempt tracking and session management
- Business metrics for users, organizations, and vault items
- System uptime and build information tracking

Security:
- Token authentication required (METRICS_TOKEN configuration)
- Support for both plain text and Argon2 hashed tokens
- Path normalization prevents high cardinality metric explosion
- No-op implementations when metrics disabled for zero overhead
- Network access controls recommended for production deployment

Implementation:
- Added prometheus dependency with conditional compilation
- Created secure /metrics endpoint with request guard authentication
- Implemented HTTP middleware fairing for automatic instrumentation
- Added database metrics utilities with timing macros
- Comprehensive unit and integration test coverage
- Complete documentation with Prometheus, Grafana, and alerting examples

Files added:
- src/metrics.rs - Core metrics collection module
- src/api/metrics.rs - Secure metrics endpoint implementation
- src/api/middleware.rs - HTTP request instrumentation
- src/db/metrics.rs - Database timing utilities
- METRICS.md - Configuration and usage guide
- MONITORING.md - Complete monitoring setup documentation
- examples/metrics-config.env - Configuration examples
- scripts/test-metrics.sh - Automated testing script
- Comprehensive test suites for both enabled/disabled scenarios

This implementation follows security best practices with disabled-by-default
configuration and provides production-ready monitoring capabilities for
Vaultwarden deployments.
2025-08-17 14:16:46 +07:00

231 lines
No EOL
8.4 KiB
Rust

#[cfg(feature = "enable_metrics")]
mod metrics_integration_tests {
use rocket::local::blocking::Client;
use rocket::http::{Status, Header, ContentType};
use rocket::serde::json;
use vaultwarden::api::core::routes as core_routes;
use vaultwarden::api::metrics::routes as metrics_routes;
use vaultwarden::CONFIG;
use vaultwarden::metrics;
fn create_test_rocket() -> rocket::Rocket<rocket::Build> {
// Initialize metrics for testing
metrics::init_build_info();
rocket::build()
.mount("/", core_routes())
.mount("/", metrics_routes())
.attach(vaultwarden::api::middleware::MetricsFairing)
}
#[test]
fn test_metrics_endpoint_without_auth() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Test without authorization header
let response = client.get("/metrics").dispatch();
// Should return 401 Unauthorized when metrics token is required
if CONFIG.metrics_token().is_some() {
assert_eq!(response.status(), Status::Unauthorized);
} else {
// If no token is configured, it should work
assert_eq!(response.status(), Status::Ok);
}
}
#[test]
fn test_metrics_endpoint_with_bearer_token() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Test with Bearer token
if let Some(token) = CONFIG.metrics_token() {
let auth_header = Header::new("Authorization", format!("Bearer {}", token));
let response = client.get("/metrics").header(auth_header).dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().expect("response body");
assert!(body.contains("# HELP"));
assert!(body.contains("# TYPE"));
assert!(body.contains("vaultwarden_"));
}
}
#[test]
fn test_metrics_endpoint_with_query_parameter() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Test with query parameter
if let Some(token) = CONFIG.metrics_token() {
let response = client.get(format!("/metrics?token={}", token)).dispatch();
assert_eq!(response.status(), Status::Ok);
let body = response.into_string().expect("response body");
assert!(body.contains("# HELP"));
assert!(body.contains("# TYPE"));
}
}
#[test]
fn test_metrics_endpoint_with_invalid_token() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Test with invalid Bearer token
let auth_header = Header::new("Authorization", "Bearer invalid-token");
let response = client.get("/metrics").header(auth_header).dispatch();
assert_eq!(response.status(), Status::Unauthorized);
}
#[test]
fn test_metrics_content_format() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Setup authorization if needed
let mut request = client.get("/metrics");
if let Some(token) = CONFIG.metrics_token() {
let auth_header = Header::new("Authorization", format!("Bearer {}", token));
request = request.header(auth_header);
}
let response = request.dispatch();
if response.status() == Status::Ok {
let body = response.into_string().expect("response body");
// Verify Prometheus format
assert!(body.contains("# HELP"));
assert!(body.contains("# TYPE"));
// Verify expected metrics exist
assert!(body.contains("vaultwarden_build_info"));
assert!(body.contains("vaultwarden_uptime_seconds"));
// Verify metric types
assert!(body.contains("TYPE vaultwarden_build_info gauge"));
assert!(body.contains("TYPE vaultwarden_uptime_seconds gauge"));
}
}
#[test]
fn test_metrics_instrumentation() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Make some requests to generate HTTP metrics
let _response1 = client.get("/alive").dispatch();
let _response2 = client.post("/api/accounts/register")
.header(ContentType::JSON)
.body(r#"{"email":"test@example.com"}"#)
.dispatch();
// Now check metrics
let mut metrics_request = client.get("/metrics");
if let Some(token) = CONFIG.metrics_token() {
let auth_header = Header::new("Authorization", format!("Bearer {}", token));
metrics_request = metrics_request.header(auth_header);
}
let response = metrics_request.dispatch();
if response.status() == Status::Ok {
let body = response.into_string().expect("response body");
// Should contain HTTP request metrics
assert!(body.contains("vaultwarden_http_requests_total"));
assert!(body.contains("vaultwarden_http_request_duration_seconds"));
}
}
#[test]
fn test_multiple_concurrent_requests() {
use std::thread;
use std::sync::Arc;
let client = Arc::new(Client::tracked(create_test_rocket()).expect("valid rocket instance"));
// Spawn multiple threads making requests
let handles: Vec<_> = (0..5).map(|_| {
let client = Arc::clone(&client);
thread::spawn(move || {
client.get("/alive").dispatch();
})
}).collect();
// Wait for all requests to complete
for handle in handles {
handle.join().unwrap();
}
// Check that metrics were collected
let mut metrics_request = client.get("/metrics");
if let Some(token) = CONFIG.metrics_token() {
let auth_header = Header::new("Authorization", format!("Bearer {}", token));
metrics_request = metrics_request.header(auth_header);
}
let response = metrics_request.dispatch();
assert!(response.status() == Status::Ok || response.status() == Status::Unauthorized);
}
#[test]
fn test_metrics_performance() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
let start = std::time::Instant::now();
let mut metrics_request = client.get("/metrics");
if let Some(token) = CONFIG.metrics_token() {
let auth_header = Header::new("Authorization", format!("Bearer {}", token));
metrics_request = metrics_request.header(auth_header);
}
let response = metrics_request.dispatch();
let duration = start.elapsed();
// Metrics endpoint should respond quickly (under 1 second)
assert!(duration.as_secs() < 1);
if response.status() == Status::Ok {
let body = response.into_string().expect("response body");
// Should return meaningful content
assert!(body.len() > 100);
}
}
}
#[cfg(not(feature = "enable_metrics"))]
mod metrics_disabled_tests {
use rocket::local::blocking::Client;
use rocket::http::Status;
use vaultwarden::api::core::routes as core_routes;
fn create_test_rocket() -> rocket::Rocket<rocket::Build> {
rocket::build()
.mount("/", core_routes())
// Note: metrics routes should not be mounted when feature is disabled
}
#[test]
fn test_metrics_endpoint_not_available() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Metrics endpoint should not exist when feature is disabled
let response = client.get("/metrics").dispatch();
assert_eq!(response.status(), Status::NotFound);
}
#[test]
fn test_normal_endpoints_still_work() {
let client = Client::tracked(create_test_rocket()).expect("valid rocket instance");
// Normal endpoints should still work
let response = client.get("/alive").dispatch();
assert_eq!(response.status(), Status::Ok);
}
}