Files
foxhunt/trading_engine/src/metrics.rs
jgrusewski 6a8cafc091 lint: fix all 27 workspace warnings (0 remaining)
- trading_engine: replace 20 drop(Copy) with let _ = (drop on Copy is no-op)
- data: remove 4 unnecessary crate::error:: qualifications
- ml: remove stale #[allow] attribute on inference.rs
- web-gateway: allow dead_code on stub route body fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 02:49:11 +01:00

656 lines
23 KiB
Rust

//! Ultra-low latency metrics collection for HFT monitoring
//!
//! This module provides lock-free metrics collection infrastructure designed to integrate
//! with the existing timing system while adding <1ns overhead to critical trading paths.
//!
//! ## Architecture Overview
//!
//! ``text
//! Critical Trading Path Metrics Collection (Async)
//! ┌─────────────────────┐ ┌──────────────────────────┐
//! │ Order Processing │ --atomic--> │ MetricsRingBuffer<T> │
//! │ (14ns latency) │ write │ (Lock-free, SIMD) │
//! │ │ │ │
//! │ Risk Checks │ --atomic--> │ SharedMetricsSegment │
//! │ Market Data │ counters │ (Cross-process IPC) │
//! └─────────────────────┘ └──────────────────────────┘
//! │
//! v
//! ┌──────────────────────────┐
//! │ Prometheus Exporter │
//! │ (Dedicated thread) │
//! └──────────────────────────┘
//! ``
use crate::timing::{HftLatencyTracker, LatencyStats};
use crossbeam_utils::CachePadded;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
/// Branch prediction hint for performance optimization
/// Note: Rust's optimizer handles branch prediction well without manual hints
#[inline(always)]
fn likely(b: bool) -> bool {
b
}
/// Ring buffer size optimized for `HFT` workloads (must be power of 2)
const RING_BUFFER_SIZE: usize = 4096;
const RING_BUFFER_MASK: usize = RING_BUFFER_SIZE - 1;
/// Metric types for classification and routing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
/// MetricType
///
/// Auto-generated documentation placeholder - enhance with specifics
pub enum MetricType {
// Counter variant
Counter,
// Histogram variant
Histogram,
// Gauge variant
Gauge,
// Summary variant
Summary,
}
/// Individual metric data point with nanosecond precision
#[derive(Debug, Clone, Serialize, Deserialize)]
/// LatencyMetric
///
/// Auto-generated documentation placeholder - enhance with specifics
pub struct LatencyMetric {
/// Timestamp Ns
pub timestamp_ns: u64,
/// Name
pub name: String,
/// Value
pub value: f64,
/// Metric Type
pub metric_type: MetricType,
/// Labels
pub labels: Vec<(String, String)>,
/// Help
pub help: String,
}
impl LatencyMetric {
/// Create counter metric with pre-calculated timestamp (for critical path)
pub fn new_counter_with_timestamp(
name: &str,
value: f64,
timestamp_ns: u64,
labels: Vec<(String, String)>,
) -> Self {
Self {
timestamp_ns,
name: name.to_string(),
value,
metric_type: MetricType::Counter,
labels,
help: String::new(),
}
}
/// Create counter metric with current timestamp (for non-critical path)
pub fn new_counter(name: &str, value: f64, labels: Vec<(String, String)>) -> Self {
Self {
timestamp_ns: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0),
name: name.to_string(),
value,
metric_type: MetricType::Counter,
labels,
help: String::new(),
}
}
pub fn new_histogram(name: &str, value: f64, labels: Vec<(String, String)>) -> Self {
Self {
timestamp_ns: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0),
name: name.to_string(),
value,
metric_type: MetricType::Histogram,
labels,
help: String::new(),
}
}
pub fn new_gauge(name: &str, value: f64, labels: Vec<(String, String)>) -> Self {
Self {
timestamp_ns: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0),
name: name.to_string(),
value,
metric_type: MetricType::Gauge,
labels,
help: String::new(),
}
}
pub fn with_help(mut self, help: &str) -> Self {
self.help = help.to_string();
self
}
}
/// Lock-free ring buffer for ultra-fast metrics collection
///
/// This structure uses cache-padded atomic operations to prevent `false` sharing
/// and minimize contention between producer (trading threads) and consumer
/// (metrics collection thread).
#[derive(Debug)]
pub struct MetricsRingBuffer {
/// Ring buffer storage with cache padding to prevent `false` sharing
buffer: [CachePadded<AtomicU64>; RING_BUFFER_SIZE],
/// Producer head pointer (where new metrics are written)
head: CachePadded<AtomicUsize>,
/// Consumer tail pointer (where metrics are read from)
tail: CachePadded<AtomicUsize>,
/// Number of dropped metrics due to buffer overflow
dropped_count: CachePadded<AtomicU64>,
/// Serialized metrics storage for complex data
metrics_storage: parking_lot::RwLock<Vec<LatencyMetric>>,
}
impl Default for MetricsRingBuffer {
fn default() -> Self {
Self::new()
}
}
impl MetricsRingBuffer {
/// Create new ring buffer with optimized configuration
pub fn new() -> Self {
// Initialize buffer with zeros
const INIT: CachePadded<AtomicU64> = CachePadded::new(AtomicU64::new(0));
let buffer = [INIT; RING_BUFFER_SIZE];
Self {
buffer,
head: CachePadded::new(AtomicUsize::new(0)),
tail: CachePadded::new(AtomicUsize::new(0)),
dropped_count: CachePadded::new(AtomicU64::new(0)),
metrics_storage: parking_lot::RwLock::new(Vec::new()),
}
}
/// Push simple counter metric with minimal overhead
///
/// This is the ultra-fast path for critical trading metrics.
/// Time complexity: O(1) with ~0.5ns overhead (optimized)
#[inline(always)]
pub fn push_counter_fast(&self, value: u64) -> bool {
let head = self.head.load(Ordering::Relaxed);
let next_head = (head + 1) & RING_BUFFER_MASK;
let tail = self.tail.load(Ordering::Relaxed); // Changed to Relaxed for speed
// Check if buffer is full (branch prediction optimized - full buffer is rare)
if likely(next_head != tail) {
// Store value with release ordering to ensure visibility
self.buffer[head].store(value, Ordering::Release);
// Advance head pointer
self.head.store(next_head, Ordering::Release);
return true;
}
// Slow path - buffer full
self.dropped_count.fetch_add(1, Ordering::Relaxed);
false
}
/// Legacy method for compatibility
#[inline(always)]
pub fn push_counter(&self, value: u64) -> bool {
self.push_counter_fast(value)
}
/// Push complex metric (slower path for non-critical metrics)
pub fn push_metric(&self, metric: LatencyMetric) {
let mut storage = self.metrics_storage.write();
storage.push(metric);
}
/// Drain all metrics for export (called by metrics collection thread)
pub fn drain_metrics(&self, max_count: usize) -> Vec<LatencyMetric> {
let mut metrics = Vec::with_capacity(max_count);
let mut drained = 0;
// Pre-calculate timestamp once for all metrics in this batch
let batch_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0);
// Drain simple counters from ring buffer
while drained < max_count {
let tail = self.tail.load(Ordering::Relaxed);
let head = self.head.load(Ordering::Acquire);
if tail == head {
break; // Buffer is empty
}
let value = self.buffer[tail].load(Ordering::Acquire);
let next_tail = (tail + 1) & RING_BUFFER_MASK;
self.tail.store(next_tail, Ordering::Release);
// Convert raw counter to metric using pre-calculated timestamp
metrics.push(LatencyMetric::new_counter_with_timestamp(
"trading_counter_total",
#[allow(clippy::as_conversions)]
{
value as f64
},
batch_timestamp,
vec![("source".to_string(), "ring_buffer".to_string())],
));
drained += 1;
}
// Drain complex metrics from storage
if drained < max_count {
let mut storage = self.metrics_storage.write();
let additional_count = (max_count - drained).min(storage.len());
metrics.extend(storage.drain(0..additional_count));
}
metrics
}
/// Get buffer statistics for monitoring
pub fn stats(&self) -> RingBufferStats {
let head = self.head.load(Ordering::Relaxed);
let tail = self.tail.load(Ordering::Relaxed);
let used = if head >= tail {
head - tail
} else {
RING_BUFFER_SIZE - tail + head
};
RingBufferStats {
capacity: RING_BUFFER_SIZE,
used,
dropped_count: self.dropped_count.load(Ordering::Relaxed),
utilization_pct: (f64::from(u32::try_from(used).unwrap_or(0))
/ f64::from(u32::try_from(RING_BUFFER_SIZE).unwrap_or(1)))
* 100.0,
}
}
}
/// Ring buffer performance statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
/// RingBufferStats
///
/// Auto-generated documentation placeholder - enhance with specifics
pub struct RingBufferStats {
/// Capacity
pub capacity: usize,
/// Used
pub used: usize,
/// Dropped Count
pub dropped_count: u64,
/// Utilization Pct
pub utilization_pct: f64,
}
/// Enhanced `HFT` latency tracker with Prometheus export capabilities
///
/// This extends the existing HftLatencyTracker with metrics collection
/// and export functionality while maintaining the same performance characteristics.
#[derive(Debug)]
pub struct EnhancedHftLatencyTracker {
/// Original latency tracker (maintains compatibility)
inner: HftLatencyTracker,
/// Lock-free metrics collection
metrics_buffer: Arc<MetricsRingBuffer>,
/// Last export timestamp for rate limiting
last_export_ns: AtomicU64,
/// Export interval in nanoseconds (default: 1 second)
export_interval_ns: AtomicU64,
}
impl Default for EnhancedHftLatencyTracker {
fn default() -> Self {
Self::new()
}
}
impl EnhancedHftLatencyTracker {
pub fn new() -> Self {
Self {
inner: HftLatencyTracker::default(),
metrics_buffer: Arc::new(MetricsRingBuffer::new()),
last_export_ns: AtomicU64::new(0),
export_interval_ns: AtomicU64::new(1_000_000_000), // 1 second
}
}
/// Record order processing latency with metrics collection (CRITICAL PATH OPTIMIZED)
#[inline(always)]
pub fn record_order_processing(&self, latency_ns: u64) {
// Update original tracker (maintains compatibility)
self.inner.record_order_processing(latency_ns);
// Push to metrics buffer with ultra-fast path (<1ns overhead)
let _ = self.metrics_buffer.push_counter_fast(latency_ns);
}
/// Record order processing with optional tracing (for debugging only)
#[inline(always)]
pub fn record_order_processing_with_trace(&self, latency_ns: u64, trace_enabled: bool) {
// Always record to fast metrics
self.record_order_processing(latency_ns);
// Only add tracing overhead if explicitly enabled (debugging mode)
if trace_enabled {
let metric = LatencyMetric::new_histogram(
"trading_order_processing_seconds",
f64::from(u32::try_from(latency_ns).unwrap_or(0)) / 1_000_000_000.0,
vec![("service".to_string(), "trading".to_string())],
)
.with_help("Order processing latency with tracing");
self.metrics_buffer.push_metric(metric);
}
}
/// Record risk check latency with metrics collection
#[inline(always)]
pub fn record_risk_check(&self, latency_ns: u64) {
self.inner.record_risk_check(latency_ns);
let _ = self.metrics_buffer.push_counter_fast(latency_ns);
}
/// Record market data processing latency
#[inline(always)]
pub fn record_market_data(&self, latency_ns: u64) {
self.inner.record_market_data(latency_ns);
let _ = self.metrics_buffer.push_counter_fast(latency_ns);
}
/// Record total latency with histogram metrics
pub fn record_total_latency(&self, latency_ns: u64) {
self.inner.record_total_latency(latency_ns);
// Create histogram metric for Prometheus
let metric = LatencyMetric::new_histogram(
"trading_latency_total_seconds",
f64::from(u32::try_from(latency_ns).unwrap_or(0)) / 1_000_000_000.0,
vec![
("service".to_string(), "trading".to_string()),
("type".to_string(), "total".to_string()),
],
)
.with_help("Total trading latency from order receipt to execution");
self.metrics_buffer.push_metric(metric);
}
/// Export Prometheus metrics (called periodically by metrics collection thread)
pub fn export_prometheus_metrics(&self) -> Vec<PrometheusMetric> {
let now_ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0);
let last_export = self.last_export_ns.load(Ordering::Relaxed);
let export_interval = self.export_interval_ns.load(Ordering::Relaxed);
// Check if export is due
if now_ns.saturating_sub(last_export) < export_interval {
return Vec::new(); // Too early to export
}
// Update last export timestamp
self.last_export_ns.store(now_ns, Ordering::Relaxed);
// Get current stats
let stats = self.inner.get_stats();
let buffer_stats = self.metrics_buffer.stats();
// Convert to Prometheus metrics
vec![
PrometheusMetric {
name: "trading_order_processing_seconds".to_string(),
value: stats.order_processing_us / 1_000_000.0,
metric_type: MetricType::Gauge,
help: "Order processing latency in seconds".to_string(),
labels: vec![("service".to_string(), "trading".to_string())],
},
PrometheusMetric {
name: "trading_risk_check_seconds".to_string(),
value: stats.risk_check_us / 1_000_000.0,
metric_type: MetricType::Gauge,
help: "Risk check latency in seconds".to_string(),
labels: vec![("service".to_string(), "trading".to_string())],
},
PrometheusMetric {
name: "trading_market_data_seconds".to_string(),
value: stats.market_data_us / 1_000_000.0,
metric_type: MetricType::Gauge,
help: "Market data processing latency in seconds".to_string(),
labels: vec![("service".to_string(), "trading".to_string())],
},
PrometheusMetric {
name: "trading_total_latency_seconds".to_string(),
value: stats.total_latency_us / 1_000_000.0,
metric_type: MetricType::Gauge,
help: "Total trading latency in seconds".to_string(),
labels: vec![("service".to_string(), "trading".to_string())],
},
PrometheusMetric {
name: "trading_measurements_total".to_string(),
value: f64::from(u32::try_from(stats.measurements_count).unwrap_or(0)),
metric_type: MetricType::Counter,
help: "Total number of latency measurements".to_string(),
labels: vec![("service".to_string(), "trading".to_string())],
},
PrometheusMetric {
name: "metrics_buffer_utilization_percent".to_string(),
value: buffer_stats.utilization_pct,
metric_type: MetricType::Gauge,
help: "Metrics buffer utilization percentage".to_string(),
labels: vec![("buffer".to_string(), "ring".to_string())],
},
PrometheusMetric {
name: "metrics_dropped_total".to_string(),
value: f64::from(u32::try_from(buffer_stats.dropped_count).unwrap_or(0)),
metric_type: MetricType::Counter,
help: "Total number of dropped metrics due to buffer overflow".to_string(),
labels: vec![("buffer".to_string(), "ring".to_string())],
},
]
}
/// Get combined statistics including buffer stats
pub fn get_enhanced_stats(&self) -> EnhancedLatencyStats {
EnhancedLatencyStats {
latency_stats: self.inner.get_stats(),
buffer_stats: self.metrics_buffer.stats(),
}
}
/// Set export interval in nanoseconds
pub fn set_export_interval_ns(&self, interval_ns: u64) {
self.export_interval_ns
.store(interval_ns, Ordering::Relaxed);
}
}
/// Combined statistics for enhanced tracking
#[derive(Debug, Clone, Serialize, Deserialize)]
/// EnhancedLatencyStats
///
/// Auto-generated documentation placeholder - enhance with specifics
pub struct EnhancedLatencyStats {
/// Latency Stats
pub latency_stats: LatencyStats,
/// Buffer Stats
pub buffer_stats: RingBufferStats,
}
/// Prometheus metric format for export
#[derive(Debug, Clone, Serialize, Deserialize)]
/// PrometheusMetric
///
/// Auto-generated documentation placeholder - enhance with specifics
pub struct PrometheusMetric {
/// Name
pub name: String,
/// Value
pub value: f64,
/// Metric Type
pub metric_type: MetricType,
/// Help
pub help: String,
/// Labels
pub labels: Vec<(String, String)>,
}
impl PrometheusMetric {
/// Format as Prometheus exposition format
pub fn format_prometheus(&self) -> String {
use std::fmt::Write;
let mut result = String::new();
// Add help text
if !self.help.is_empty() {
let _ = write!(result, "# HELP {} {}\n", self.name, self.help);
}
// Add type
let type_str = match self.metric_type {
MetricType::Counter => "counter",
MetricType::Histogram => "histogram",
MetricType::Gauge => "gauge",
MetricType::Summary => "summary",
};
let _ = write!(result, "# TYPE {} {}\n", self.name, type_str);
// Add metric with labels
if self.labels.is_empty() {
let _ = write!(result, "{} {}\n", self.name, self.value);
} else {
let labels_str: Vec<String> = self
.labels
.iter()
.map(|(k, v)| format!("{}=\"{}\"", k, v))
.collect();
let _ = write!(
result,
"{}{{{}}} {}\n",
self.name,
labels_str.join(","),
self.value
);
}
result
}
}
/// Global metrics registry for the trading engine
static GLOBAL_METRICS_TRACKER: once_cell::sync::OnceCell<EnhancedHftLatencyTracker> =
once_cell::sync::OnceCell::new();
/// Get global metrics tracker instance
pub fn global_metrics_tracker() -> &'static EnhancedHftLatencyTracker {
GLOBAL_METRICS_TRACKER.get_or_init(EnhancedHftLatencyTracker::new)
}
/// Initialize global metrics with custom configuration
pub fn init_global_metrics(export_interval_ns: u64) -> &'static EnhancedHftLatencyTracker {
let tracker = GLOBAL_METRICS_TRACKER.get_or_init(EnhancedHftLatencyTracker::new);
tracker.set_export_interval_ns(export_interval_ns);
tracker
}
/// Convenience macro for recording latency with minimal overhead
#[macro_export]
macro_rules! record_latency {
($metric_type:ident, $latency_ns:expr) => {
$crate::metrics::global_metrics_tracker().$metric_type($latency_ns)
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_ring_buffer() {
let buffer = MetricsRingBuffer::new();
// Test push and drain
assert!(buffer.push_counter(100));
assert!(buffer.push_counter(200));
assert!(buffer.push_counter(300));
let metrics = buffer.drain_metrics(10);
assert_eq!(metrics.len(), 3);
let stats = buffer.stats();
assert_eq!(stats.used, 0); // Should be empty after drain
assert_eq!(stats.dropped_count, 0);
}
#[test]
fn test_enhanced_latency_tracker() {
let tracker = EnhancedHftLatencyTracker::new();
// Record some latency measurements
tracker.record_order_processing(1000); // 1 microsecond
tracker.record_risk_check(500); // 0.5 microseconds
tracker.record_market_data(2000); // 2 microseconds
tracker.record_total_latency(3500); // 3.5 microseconds
let stats = tracker.get_enhanced_stats();
assert!(stats.latency_stats.measurements_count > 0);
assert!(stats.buffer_stats.used > 0);
}
#[test]
fn test_prometheus_export() {
let tracker = EnhancedHftLatencyTracker::new();
tracker.record_order_processing(1000);
// Force export by setting interval to 0
tracker.set_export_interval_ns(0);
let metrics = tracker.export_prometheus_metrics();
assert!(!metrics.is_empty());
// Test Prometheus format
let formatted = metrics
.first()
.expect("metrics should not be empty")
.format_prometheus();
assert!(formatted.contains("# HELP"));
assert!(formatted.contains("# TYPE"));
}
#[test]
fn test_ring_buffer_overflow() {
let buffer = MetricsRingBuffer::new();
// Fill buffer beyond capacity
for i in 0..RING_BUFFER_SIZE + 100 {
buffer.push_counter(u64::try_from(i).unwrap_or(0));
}
let stats = buffer.stats();
assert!(stats.dropped_count > 0);
}
}