use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use super::{OperationType, ErrorCode};
#[derive(Debug)]
pub struct GraphQLMetrics {
query_stats: QueryStats,
operations: Mutex<HashMap<String, OperationMetrics>>,
error_counts: Mutex<HashMap<ErrorCode, u64>>,
created_at: Instant,
}
impl GraphQLMetrics {
pub fn new() -> Self {
Self {
query_stats: QueryStats::new(),
operations: Mutex::new(HashMap::new()),
error_counts: Mutex::new(HashMap::new()),
created_at: Instant::now(),
}
}
pub fn record_query(&self, duration: Duration, operation_type: OperationType) {
self.query_stats.record(duration, operation_type);
}
pub fn record_operation(&self, name: &str, duration: Duration, operation_type: OperationType) {
let mut operations = self.operations.lock().unwrap();
let metrics = operations
.entry(name.to_string())
.or_insert_with(|| OperationMetrics::new(operation_type));
metrics.record(duration);
}
pub fn record_error(&self, error: &super::GraphQLError) {
let mut counts = self.error_counts.lock().unwrap();
*counts.entry(error.code).or_insert(0) += 1;
}
pub fn query_stats(&self) -> &QueryStats {
&self.query_stats
}
pub fn operation_metrics(&self, name: &str) -> Option<OperationMetrics> {
self.operations.lock().unwrap().get(name).cloned()
}
pub fn all_operations(&self) -> HashMap<String, OperationMetrics> {
self.operations.lock().unwrap().clone()
}
pub fn error_counts(&self) -> HashMap<ErrorCode, u64> {
self.error_counts.lock().unwrap().clone()
}
pub fn uptime(&self) -> Duration {
self.created_at.elapsed()
}
pub fn reset(&self) {
self.query_stats.reset();
self.operations.lock().unwrap().clear();
self.error_counts.lock().unwrap().clear();
}
pub fn to_prometheus(&self) -> String {
let mut output = String::new();
output.push_str("# HELP helios_graphql_queries_total Total GraphQL queries\n");
output.push_str("# TYPE helios_graphql_queries_total counter\n");
output.push_str(&format!(
"helios_graphql_queries_total{{type=\"query\"}} {}\n",
self.query_stats.query_count.load(Ordering::Relaxed)
));
output.push_str(&format!(
"helios_graphql_queries_total{{type=\"mutation\"}} {}\n",
self.query_stats.mutation_count.load(Ordering::Relaxed)
));
output.push_str(&format!(
"helios_graphql_queries_total{{type=\"subscription\"}} {}\n",
self.query_stats.subscription_count.load(Ordering::Relaxed)
));
output.push_str("\n# HELP helios_graphql_latency_ms Query latency in milliseconds\n");
output.push_str("# TYPE helios_graphql_latency_ms gauge\n");
if let Some(avg) = self.query_stats.average_duration() {
output.push_str(&format!(
"helios_graphql_latency_ms{{quantile=\"avg\"}} {}\n",
avg.as_millis()
));
}
if let Some(min) = self.query_stats.min_duration() {
output.push_str(&format!(
"helios_graphql_latency_ms{{quantile=\"min\"}} {}\n",
min.as_millis()
));
}
if let Some(max) = self.query_stats.max_duration() {
output.push_str(&format!(
"helios_graphql_latency_ms{{quantile=\"max\"}} {}\n",
max.as_millis()
));
}
output.push_str("\n# HELP helios_graphql_errors_total Total GraphQL errors\n");
output.push_str("# TYPE helios_graphql_errors_total counter\n");
for (code, count) in self.error_counts() {
output.push_str(&format!(
"helios_graphql_errors_total{{code=\"{:?}\"}} {}\n",
code, count
));
}
output.push_str("\n# HELP helios_graphql_operation_calls Operation call counts\n");
output.push_str("# TYPE helios_graphql_operation_calls counter\n");
for (name, metrics) in self.all_operations() {
output.push_str(&format!(
"helios_graphql_operation_calls{{name=\"{}\"}} {}\n",
name,
metrics.call_count.load(Ordering::Relaxed)
));
}
output
}
}
impl Default for GraphQLMetrics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct QueryStats {
pub query_count: AtomicU64,
pub mutation_count: AtomicU64,
pub subscription_count: AtomicU64,
total_duration_us: AtomicU64,
min_duration_us: AtomicU64,
max_duration_us: AtomicU64,
latency_buckets: Mutex<LatencyHistogram>,
}
impl QueryStats {
pub fn new() -> Self {
Self {
query_count: AtomicU64::new(0),
mutation_count: AtomicU64::new(0),
subscription_count: AtomicU64::new(0),
total_duration_us: AtomicU64::new(0),
min_duration_us: AtomicU64::new(u64::MAX),
max_duration_us: AtomicU64::new(0),
latency_buckets: Mutex::new(LatencyHistogram::new()),
}
}
pub fn record(&self, duration: Duration, operation_type: OperationType) {
let duration_us = duration.as_micros() as u64;
match operation_type {
OperationType::Query => {
self.query_count.fetch_add(1, Ordering::Relaxed);
}
OperationType::Mutation => {
self.mutation_count.fetch_add(1, Ordering::Relaxed);
}
OperationType::Subscription => {
self.subscription_count.fetch_add(1, Ordering::Relaxed);
}
}
self.total_duration_us.fetch_add(duration_us, Ordering::Relaxed);
let mut current_min = self.min_duration_us.load(Ordering::Relaxed);
while duration_us < current_min {
match self.min_duration_us.compare_exchange_weak(
current_min,
duration_us,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(x) => current_min = x,
}
}
let mut current_max = self.max_duration_us.load(Ordering::Relaxed);
while duration_us > current_max {
match self.max_duration_us.compare_exchange_weak(
current_max,
duration_us,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(x) => current_max = x,
}
}
self.latency_buckets.lock().unwrap().record(duration_us);
}
pub fn total_count(&self) -> u64 {
self.query_count.load(Ordering::Relaxed)
+ self.mutation_count.load(Ordering::Relaxed)
+ self.subscription_count.load(Ordering::Relaxed)
}
pub fn average_duration(&self) -> Option<Duration> {
let total = self.total_count();
if total == 0 {
return None;
}
let total_us = self.total_duration_us.load(Ordering::Relaxed);
Some(Duration::from_micros(total_us / total))
}
pub fn min_duration(&self) -> Option<Duration> {
let min = self.min_duration_us.load(Ordering::Relaxed);
if min == u64::MAX {
None
} else {
Some(Duration::from_micros(min))
}
}
pub fn max_duration(&self) -> Option<Duration> {
let max = self.max_duration_us.load(Ordering::Relaxed);
if max == 0 {
None
} else {
Some(Duration::from_micros(max))
}
}
pub fn percentile(&self, p: f64) -> Option<Duration> {
self.latency_buckets.lock().unwrap().percentile(p)
}
pub fn reset(&self) {
self.query_count.store(0, Ordering::Relaxed);
self.mutation_count.store(0, Ordering::Relaxed);
self.subscription_count.store(0, Ordering::Relaxed);
self.total_duration_us.store(0, Ordering::Relaxed);
self.min_duration_us.store(u64::MAX, Ordering::Relaxed);
self.max_duration_us.store(0, Ordering::Relaxed);
self.latency_buckets.lock().unwrap().reset();
}
}
impl Default for QueryStats {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct OperationMetrics {
pub operation_type: OperationType,
pub call_count: AtomicU64,
pub total_duration_us: AtomicU64,
pub error_count: AtomicU64,
}
impl Clone for OperationMetrics {
fn clone(&self) -> Self {
Self {
operation_type: self.operation_type,
call_count: AtomicU64::new(self.call_count.load(Ordering::Relaxed)),
total_duration_us: AtomicU64::new(self.total_duration_us.load(Ordering::Relaxed)),
error_count: AtomicU64::new(self.error_count.load(Ordering::Relaxed)),
}
}
}
impl OperationMetrics {
pub fn new(operation_type: OperationType) -> Self {
Self {
operation_type,
call_count: AtomicU64::new(0),
total_duration_us: AtomicU64::new(0),
error_count: AtomicU64::new(0),
}
}
pub fn record(&self, duration: Duration) {
self.call_count.fetch_add(1, Ordering::Relaxed);
self.total_duration_us.fetch_add(duration.as_micros() as u64, Ordering::Relaxed);
}
pub fn record_error(&self) {
self.error_count.fetch_add(1, Ordering::Relaxed);
}
pub fn average_duration(&self) -> Option<Duration> {
let count = self.call_count.load(Ordering::Relaxed);
if count == 0 {
return None;
}
let total_us = self.total_duration_us.load(Ordering::Relaxed);
Some(Duration::from_micros(total_us / count))
}
pub fn error_rate(&self) -> f64 {
let count = self.call_count.load(Ordering::Relaxed);
if count == 0 {
return 0.0;
}
self.error_count.load(Ordering::Relaxed) as f64 / count as f64
}
}
impl Clone for QueryStats {
fn clone(&self) -> Self {
Self {
query_count: AtomicU64::new(self.query_count.load(Ordering::Relaxed)),
mutation_count: AtomicU64::new(self.mutation_count.load(Ordering::Relaxed)),
subscription_count: AtomicU64::new(self.subscription_count.load(Ordering::Relaxed)),
total_duration_us: AtomicU64::new(self.total_duration_us.load(Ordering::Relaxed)),
min_duration_us: AtomicU64::new(self.min_duration_us.load(Ordering::Relaxed)),
max_duration_us: AtomicU64::new(self.max_duration_us.load(Ordering::Relaxed)),
latency_buckets: Mutex::new(self.latency_buckets.lock().unwrap().clone()),
}
}
}
#[derive(Debug, Clone)]
struct LatencyHistogram {
boundaries: Vec<u64>,
counts: Vec<u64>,
values: Vec<u64>,
max_values: usize,
}
impl LatencyHistogram {
fn new() -> Self {
let boundaries = vec![
100, 500, 1_000, 5_000, 10_000, 50_000, 100_000, 500_000, 1_000_000, 5_000_000,
];
let counts = vec![0u64; boundaries.len() + 1];
Self {
boundaries,
counts,
values: Vec::new(),
max_values: 10000,
}
}
fn record(&mut self, value_us: u64) {
let bucket = self.boundaries.iter()
.position(|&b| value_us <= b)
.unwrap_or(self.boundaries.len());
self.counts[bucket] += 1;
if self.values.len() < self.max_values {
self.values.push(value_us);
} else {
let idx = rand_index(self.values.len() + 1);
if idx < self.values.len() {
self.values[idx] = value_us;
}
}
}
fn percentile(&self, p: f64) -> Option<Duration> {
if self.values.is_empty() {
return None;
}
let mut sorted = self.values.clone();
sorted.sort_unstable();
let idx = ((p / 100.0) * (sorted.len() - 1) as f64) as usize;
Some(Duration::from_micros(sorted[idx]))
}
fn reset(&mut self) {
for count in &mut self.counts {
*count = 0;
}
self.values.clear();
}
}
fn rand_index(max: usize) -> usize {
use std::time::SystemTime;
let seed = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.subsec_nanos() as usize;
seed % max
}
pub trait MetricsReporter: Send + Sync {
fn report(&self, metrics: &GraphQLMetrics);
}
pub struct ConsoleReporter;
impl MetricsReporter for ConsoleReporter {
fn report(&self, metrics: &GraphQLMetrics) {
let stats = metrics.query_stats();
println!("=== GraphQL Metrics ===");
println!("Queries: {}", stats.query_count.load(Ordering::Relaxed));
println!("Mutations: {}", stats.mutation_count.load(Ordering::Relaxed));
println!("Subscriptions: {}", stats.subscription_count.load(Ordering::Relaxed));
if let Some(avg) = stats.average_duration() {
println!("Avg latency: {:?}", avg);
}
if let Some(min) = stats.min_duration() {
println!("Min latency: {:?}", min);
}
if let Some(max) = stats.max_duration() {
println!("Max latency: {:?}", max);
}
println!("Errors: {:?}", metrics.error_counts());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_stats_recording() {
let stats = QueryStats::new();
stats.record(Duration::from_millis(10), OperationType::Query);
stats.record(Duration::from_millis(20), OperationType::Query);
stats.record(Duration::from_millis(5), OperationType::Mutation);
assert_eq!(stats.query_count.load(Ordering::Relaxed), 2);
assert_eq!(stats.mutation_count.load(Ordering::Relaxed), 1);
assert_eq!(stats.total_count(), 3);
}
#[test]
fn test_query_stats_duration() {
let stats = QueryStats::new();
stats.record(Duration::from_millis(10), OperationType::Query);
stats.record(Duration::from_millis(20), OperationType::Query);
stats.record(Duration::from_millis(30), OperationType::Query);
assert_eq!(stats.min_duration(), Some(Duration::from_millis(10)));
assert_eq!(stats.max_duration(), Some(Duration::from_millis(30)));
assert_eq!(stats.average_duration(), Some(Duration::from_millis(20)));
}
#[test]
fn test_graphql_metrics() {
let metrics = GraphQLMetrics::new();
metrics.record_query(Duration::from_millis(10), OperationType::Query);
metrics.record_operation("GetUsers", Duration::from_millis(10), OperationType::Query);
assert_eq!(metrics.query_stats().total_count(), 1);
assert!(metrics.operation_metrics("GetUsers").is_some());
}
#[test]
fn test_operation_metrics() {
let metrics = OperationMetrics::new(OperationType::Query);
metrics.record(Duration::from_millis(10));
metrics.record(Duration::from_millis(20));
metrics.record_error();
assert_eq!(metrics.call_count.load(Ordering::Relaxed), 2);
assert_eq!(metrics.error_count.load(Ordering::Relaxed), 1);
assert_eq!(metrics.average_duration(), Some(Duration::from_millis(15)));
assert_eq!(metrics.error_rate(), 0.5);
}
#[test]
fn test_error_recording() {
let metrics = GraphQLMetrics::new();
let error1 = super::super::GraphQLError::parse_error("error1");
let error2 = super::super::GraphQLError::parse_error("error2");
let error3 = super::super::GraphQLError::validation_error("error3");
metrics.record_error(&error1);
metrics.record_error(&error2);
metrics.record_error(&error3);
let counts = metrics.error_counts();
assert_eq!(counts.get(&ErrorCode::ParseError), Some(&2));
assert_eq!(counts.get(&ErrorCode::ValidationError), Some(&1));
}
#[test]
fn test_prometheus_export() {
let metrics = GraphQLMetrics::new();
metrics.record_query(Duration::from_millis(10), OperationType::Query);
metrics.record_query(Duration::from_millis(5), OperationType::Mutation);
let output = metrics.to_prometheus();
assert!(output.contains("helios_graphql_queries_total"));
assert!(output.contains("helios_graphql_latency_ms"));
}
#[test]
fn test_metrics_reset() {
let metrics = GraphQLMetrics::new();
metrics.record_query(Duration::from_millis(10), OperationType::Query);
metrics.record_operation("GetUsers", Duration::from_millis(10), OperationType::Query);
assert_eq!(metrics.query_stats().total_count(), 1);
metrics.reset();
assert_eq!(metrics.query_stats().total_count(), 0);
assert!(metrics.all_operations().is_empty());
}
#[test]
fn test_latency_histogram_percentile() {
let mut histogram = LatencyHistogram::new();
for i in 1..=100 {
histogram.record(i * 1000); }
let p50 = histogram.percentile(50.0).unwrap();
let p99 = histogram.percentile(99.0).unwrap();
assert!(p50.as_millis() >= 45 && p50.as_millis() <= 55);
assert!(p99.as_millis() >= 95);
}
#[test]
fn test_query_stats_empty() {
let stats = QueryStats::new();
assert_eq!(stats.total_count(), 0);
assert!(stats.average_duration().is_none());
assert!(stats.min_duration().is_none());
assert!(stats.max_duration().is_none());
}
#[test]
fn test_metrics_uptime() {
let metrics = GraphQLMetrics::new();
std::thread::sleep(Duration::from_millis(10));
let uptime = metrics.uptime();
assert!(uptime >= Duration::from_millis(10));
}
#[test]
fn test_operation_metrics_error_rate_zero() {
let metrics = OperationMetrics::new(OperationType::Query);
assert_eq!(metrics.error_rate(), 0.0);
}
}