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
409 lines
13 KiB
Rust
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);
|
|
}
|
|
} |