Files
foxhunt/storage/src/metrics.rs
jgrusewski 8950831817 🎉 MAJOR: Shared libraries architecture complete with Vault integration
COMPLETED:
 Created 3 shared libraries: common, config (foxhunt-config), storage
 Config library: PostgreSQL hot-reload, Vault integration, unified ConfigManager
 Storage library: S3 with Vault credentials, model checkpoints, zero hardcoded keys
 Common library: Shared types, database connections, error handling
 Fixed TLI protobuf compilation issues (duplicate health_check, Aad types)
 Trading Service migrated to use centralized config

SECURITY IMPROVEMENTS:
🔒 ALL AWS credentials now from Vault (no environment variables)
🔒 Circuit breaker patterns for external services
🔒 Secure error messages that don't leak credentials
🔒 Automatic credential refresh with 5-minute TTL

ARCHITECTURE:
- Single source of truth for configuration
- Zero code duplication for common functionality
- Hot-reload capability via PostgreSQL NOTIFY/LISTEN
- Multi-tier storage with compression and lifecycle management
- Type-safe configuration with comprehensive error handling

Next: Complete service migrations to use shared libraries
2025-09-25 09:23:52 +02:00

409 lines
13 KiB
Rust

//! Metrics and telemetry for storage operations
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug, info, warn};
/// Storage operation metrics
#[derive(Debug, Clone)]
pub struct StorageMetrics {
/// Operation counters
pub operations: Arc<OperationMetrics>,
/// Performance metrics
pub performance: Arc<PerformanceMetrics>,
/// Error metrics
pub errors: Arc<ErrorMetrics>,
}
impl StorageMetrics {
/// Create new metrics instance
pub fn new() -> Self {
Self {
operations: Arc::new(OperationMetrics::new()),
performance: Arc::new(PerformanceMetrics::new()),
errors: Arc::new(ErrorMetrics::new()),
}
}
/// Record a storage operation
pub fn record_operation(&self, operation: &str, provider: &str, duration: Duration, success: bool) {
// Record operation count
self.operations.increment(operation, provider);
// Record performance metrics
self.performance.record_duration(operation, provider, duration);
// Record errors if failed
if !success {
self.errors.increment(operation, provider, "operation_failed");
}
debug!(
"Storage operation recorded: {} on {} took {:?} (success: {})",
operation, provider, duration, success
);
}
/// Record data transfer metrics
pub fn record_transfer(&self, operation: &str, provider: &str, bytes: u64, duration: Duration) {
self.performance.record_transfer(operation, provider, bytes, duration);
let throughput_mbps = (bytes as f64 / (1024.0 * 1024.0)) / duration.as_secs_f64();
debug!(
"Data transfer recorded: {} on {} - {} bytes in {:?} ({:.2} MB/s)",
operation, provider, bytes, duration, throughput_mbps
);
}
/// Record authentication metrics
pub fn record_auth_event(&self, provider: &str, event: &str, success: bool) {
self.operations.increment("auth", provider);
if !success {
self.errors.increment("auth", provider, event);
}
debug!("Auth event recorded: {} on {} (success: {})", event, provider, success);
}
/// Get operation summary
pub fn get_summary(&self) -> MetricsSummary {
MetricsSummary {
total_operations: self.operations.get_total(),
total_errors: self.errors.get_total(),
average_latency_ms: self.performance.get_average_latency_ms(),
total_bytes_transferred: self.performance.get_total_bytes_transferred(),
operations_by_type: self.operations.get_by_type(),
errors_by_type: self.errors.get_by_type(),
performance_percentiles: self.performance.get_percentiles(),
}
}
/// Reset all metrics (useful for testing)
pub fn reset(&self) {
self.operations.reset();
self.performance.reset();
self.errors.reset();
info!("Storage metrics reset");
}
}
impl Default for StorageMetrics {
fn default() -> Self {
Self::new()
}
}
/// Operation counter metrics
#[derive(Debug)]
pub struct OperationMetrics {
/// Total operations by type and provider
counters: Arc<parking_lot::RwLock<HashMap<String, AtomicU64>>>,
}
impl OperationMetrics {
pub fn new() -> Self {
Self {
counters: Arc::new(parking_lot::RwLock::new(HashMap::new())),
}
}
pub fn increment(&self, operation: &str, provider: &str) {
let key = format!("{}:{}", operation, provider);
let counters = self.counters.read();
if let Some(counter) = counters.get(&key) {
counter.fetch_add(1, Ordering::Relaxed);
} else {
drop(counters);
let mut counters = self.counters.write();
counters.entry(key).or_insert_with(|| AtomicU64::new(0)).fetch_add(1, Ordering::Relaxed);
}
}
pub fn get(&self, operation: &str, provider: &str) -> u64 {
let key = format!("{}:{}", operation, provider);
self.counters.read()
.get(&key)
.map(|c| c.load(Ordering::Relaxed))
.unwrap_or(0)
}
pub fn get_total(&self) -> u64 {
self.counters.read()
.values()
.map(|c| c.load(Ordering::Relaxed))
.sum()
}
pub fn get_by_type(&self) -> HashMap<String, u64> {
self.counters.read()
.iter()
.map(|(key, counter)| (key.clone(), counter.load(Ordering::Relaxed)))
.collect()
}
pub fn reset(&self) {
let mut counters = self.counters.write();
counters.clear();
}
}
/// Performance metrics (latency, throughput)
#[derive(Debug)]
pub struct PerformanceMetrics {
/// Latency measurements
latencies: Arc<parking_lot::RwLock<HashMap<String, Vec<Duration>>>>,
/// Bytes transferred
bytes_transferred: Arc<parking_lot::RwLock<HashMap<String, AtomicU64>>>,
/// Transfer durations for throughput calculation
transfer_durations: Arc<parking_lot::RwLock<HashMap<String, Vec<Duration>>>>,
}
impl PerformanceMetrics {
pub fn new() -> Self {
Self {
latencies: Arc::new(parking_lot::RwLock::new(HashMap::new())),
bytes_transferred: Arc::new(parking_lot::RwLock::new(HashMap::new())),
transfer_durations: Arc::new(parking_lot::RwLock::new(HashMap::new())),
}
}
pub fn record_duration(&self, operation: &str, provider: &str, duration: Duration) {
let key = format!("{}:{}", operation, provider);
let mut latencies = self.latencies.write();
latencies.entry(key).or_default().push(duration);
// Keep only recent measurements (sliding window)
if let Some(durations) = latencies.get_mut(&key) {
if durations.len() > 1000 {
durations.drain(..500); // Keep latest 500
}
}
}
pub fn record_transfer(&self, operation: &str, provider: &str, bytes: u64, duration: Duration) {
let key = format!("{}:{}", operation, provider);
// Record bytes
let bytes_map = self.bytes_transferred.read();
if let Some(counter) = bytes_map.get(&key) {
counter.fetch_add(bytes, Ordering::Relaxed);
} else {
drop(bytes_map);
let mut bytes_map = self.bytes_transferred.write();
bytes_map.entry(key.clone()).or_insert_with(|| AtomicU64::new(0)).fetch_add(bytes, Ordering::Relaxed);
}
// Record duration for throughput calculation
let mut durations = self.transfer_durations.write();
durations.entry(key).or_default().push(duration);
}
pub fn get_average_latency_ms(&self) -> f64 {
let latencies = self.latencies.read();
let all_durations: Vec<Duration> = latencies.values().flatten().cloned().collect();
if all_durations.is_empty() {
0.0
} else {
let total_ms: f64 = all_durations.iter().map(|d| d.as_millis() as f64).sum();
total_ms / all_durations.len() as f64
}
}
pub fn get_total_bytes_transferred(&self) -> u64 {
self.bytes_transferred.read()
.values()
.map(|c| c.load(Ordering::Relaxed))
.sum()
}
pub fn get_percentiles(&self) -> PerformancePercentiles {
let latencies = self.latencies.read();
let mut all_durations: Vec<Duration> = latencies.values().flatten().cloned().collect();
if all_durations.is_empty() {
return PerformancePercentiles::default();
}
all_durations.sort();
let len = all_durations.len();
PerformancePercentiles {
p50_ms: all_durations[len * 50 / 100].as_millis() as f64,
p90_ms: all_durations[len * 90 / 100].as_millis() as f64,
p95_ms: all_durations[len * 95 / 100].as_millis() as f64,
p99_ms: all_durations[len * 99 / 100].as_millis() as f64,
min_ms: all_durations[0].as_millis() as f64,
max_ms: all_durations[len - 1].as_millis() as f64,
}
}
pub fn reset(&self) {
let mut latencies = self.latencies.write();
latencies.clear();
let mut bytes_transferred = self.bytes_transferred.write();
bytes_transferred.clear();
let mut transfer_durations = self.transfer_durations.write();
transfer_durations.clear();
}
}
/// Error tracking metrics
#[derive(Debug)]
pub struct ErrorMetrics {
/// Error counters by type and provider
error_counters: Arc<parking_lot::RwLock<HashMap<String, AtomicU64>>>,
}
impl ErrorMetrics {
pub fn new() -> Self {
Self {
error_counters: Arc::new(parking_lot::RwLock::new(HashMap::new())),
}
}
pub fn increment(&self, operation: &str, provider: &str, error_type: &str) {
let key = format!("{}:{}:{}", operation, provider, error_type);
let counters = self.error_counters.read();
if let Some(counter) = counters.get(&key) {
counter.fetch_add(1, Ordering::Relaxed);
} else {
drop(counters);
let mut counters = self.error_counters.write();
counters.entry(key).or_insert_with(|| AtomicU64::new(0)).fetch_add(1, Ordering::Relaxed);
}
warn!("Storage error recorded: {} on {} (type: {})", operation, provider, error_type);
}
pub fn get_total(&self) -> u64 {
self.error_counters.read()
.values()
.map(|c| c.load(Ordering::Relaxed))
.sum()
}
pub fn get_by_type(&self) -> HashMap<String, u64> {
self.error_counters.read()
.iter()
.map(|(key, counter)| (key.clone(), counter.load(Ordering::Relaxed)))
.collect()
}
pub fn reset(&self) {
let mut counters = self.error_counters.write();
counters.clear();
}
}
/// Performance percentiles for latency analysis
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PerformancePercentiles {
pub p50_ms: f64,
pub p90_ms: f64,
pub p95_ms: f64,
pub p99_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
}
/// Comprehensive metrics summary
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MetricsSummary {
pub total_operations: u64,
pub total_errors: u64,
pub average_latency_ms: f64,
pub total_bytes_transferred: u64,
pub operations_by_type: HashMap<String, u64>,
pub errors_by_type: HashMap<String, u64>,
pub performance_percentiles: PerformancePercentiles,
}
/// Global storage metrics instance
static GLOBAL_METRICS: std::sync::OnceLock<StorageMetrics> = std::sync::OnceLock::new();
/// Get the global metrics instance
pub fn get_metrics() -> &'static StorageMetrics {
GLOBAL_METRICS.get_or_init(StorageMetrics::new)
}
/// Macro for timing storage operations
#[macro_export]
macro_rules! time_storage_operation {
($operation:expr, $provider:expr, $code:block) => {{
let start = std::time::Instant::now();
let result = $code;
let duration = start.elapsed();
let success = result.is_ok();
$crate::metrics::get_metrics().record_operation($operation, $provider, duration, success);
result
}};
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_operation_metrics() {
let metrics = OperationMetrics::new();
metrics.increment("read", "s3");
metrics.increment("read", "s3");
metrics.increment("write", "local");
assert_eq!(metrics.get("read", "s3"), 2);
assert_eq!(metrics.get("write", "local"), 1);
assert_eq!(metrics.get_total(), 3);
}
#[test]
fn test_performance_metrics() {
let metrics = PerformanceMetrics::new();
metrics.record_duration("read", "s3", Duration::from_millis(100));
metrics.record_duration("read", "s3", Duration::from_millis(200));
metrics.record_transfer("upload", "s3", 1024, Duration::from_millis(50));
assert_eq!(metrics.get_total_bytes_transferred(), 1024);
let avg = metrics.get_average_latency_ms();
assert!(avg > 0.0);
}
#[test]
fn test_error_metrics() {
let metrics = ErrorMetrics::new();
metrics.increment("read", "s3", "timeout");
metrics.increment("write", "local", "permission_denied");
assert_eq!(metrics.get_total(), 2);
let by_type = metrics.get_by_type();
assert!(by_type.contains_key("read:s3:timeout"));
}
#[test]
fn test_storage_metrics_integration() {
let metrics = StorageMetrics::new();
metrics.record_operation("read", "s3", Duration::from_millis(100), true);
metrics.record_operation("write", "local", Duration::from_millis(50), false);
metrics.record_transfer("upload", "s3", 2048, Duration::from_millis(100));
let summary = metrics.get_summary();
assert_eq!(summary.total_operations, 2);
assert_eq!(summary.total_errors, 1);
assert_eq!(summary.total_bytes_transferred, 2048);
}
}