mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-16 22:52:43 +00:00
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.
231 lines
No EOL
8.4 KiB
Rust
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);
|
|
}
|
|
} |