use super::super::Metadata;
use super::cache::CacheMetrics;
use super::cost::CostInfo;
use super::error::ErrorInfo;
use super::token::TokenUsage;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestMetrics {
#[serde(flatten)]
pub metadata: Metadata,
pub request_id: String,
pub user_id: Option<Uuid>,
pub team_id: Option<Uuid>,
pub api_key_id: Option<Uuid>,
pub model: String,
pub provider: String,
pub request_type: String,
pub status: RequestStatus,
pub status_code: u16,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub response_time_ms: u64,
pub queue_time_ms: u64,
pub provider_time_ms: u64,
pub token_usage: TokenUsage,
pub cost: CostInfo,
pub error: Option<ErrorInfo>,
pub cache: CacheMetrics,
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RequestStatus {
Success,
Error,
Timeout,
RateLimit,
QuotaExceeded,
Cancelled,
}
impl RequestMetrics {
pub fn new(request_id: String, model: String, provider: String, request_type: String) -> Self {
Self {
metadata: Metadata::new(),
request_id,
user_id: None,
team_id: None,
api_key_id: None,
model,
provider,
request_type,
status: RequestStatus::Success,
status_code: 200,
timestamp: chrono::Utc::now(),
response_time_ms: 0,
queue_time_ms: 0,
provider_time_ms: 0,
token_usage: TokenUsage::default(),
cost: CostInfo::default(),
error: None,
cache: CacheMetrics::default(),
extra: HashMap::new(),
}
}
pub fn with_user(mut self, user_id: Uuid, team_id: Option<Uuid>) -> Self {
self.user_id = Some(user_id);
self.team_id = team_id;
self
}
pub fn with_api_key(mut self, api_key_id: Uuid) -> Self {
self.api_key_id = Some(api_key_id);
self
}
pub fn with_timing(
mut self,
response_time_ms: u64,
queue_time_ms: u64,
provider_time_ms: u64,
) -> Self {
self.response_time_ms = response_time_ms;
self.queue_time_ms = queue_time_ms;
self.provider_time_ms = provider_time_ms;
self
}
pub fn with_tokens(mut self, input_tokens: u32, output_tokens: u32) -> Self {
self.token_usage.input_tokens = input_tokens;
self.token_usage.output_tokens = output_tokens;
self.token_usage.total_tokens = input_tokens + output_tokens;
self
}
pub fn with_cost(mut self, input_cost: f64, output_cost: f64, currency: String) -> Self {
self.cost.input_cost = input_cost;
self.cost.output_cost = output_cost;
self.cost.total_cost = input_cost + output_cost;
self.cost.currency = currency;
self
}
pub fn with_error(mut self, error: ErrorInfo) -> Self {
self.status = RequestStatus::Error;
self.error = Some(error);
self
}
pub fn with_cache(mut self, cache: CacheMetrics) -> Self {
self.cache = cache;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_metrics() -> RequestMetrics {
RequestMetrics::new(
"req-123".to_string(),
"gpt-4".to_string(),
"openai".to_string(),
"chat_completion".to_string(),
)
}
#[test]
fn test_request_metrics_creation() {
let metrics = create_test_metrics();
assert_eq!(metrics.request_id, "req-123");
assert_eq!(metrics.model, "gpt-4");
assert_eq!(metrics.provider, "openai");
assert!(matches!(metrics.status, RequestStatus::Success));
}
#[test]
fn test_request_metrics_default_values() {
let metrics = create_test_metrics();
assert!(metrics.user_id.is_none());
assert!(metrics.team_id.is_none());
assert!(metrics.api_key_id.is_none());
assert_eq!(metrics.status_code, 200);
assert_eq!(metrics.response_time_ms, 0);
assert_eq!(metrics.queue_time_ms, 0);
assert_eq!(metrics.provider_time_ms, 0);
assert!(metrics.error.is_none());
assert!(metrics.extra.is_empty());
}
#[test]
fn test_with_user() {
let user_id = Uuid::new_v4();
let team_id = Uuid::new_v4();
let metrics = create_test_metrics().with_user(user_id, Some(team_id));
assert_eq!(metrics.user_id, Some(user_id));
assert_eq!(metrics.team_id, Some(team_id));
}
#[test]
fn test_with_user_no_team() {
let user_id = Uuid::new_v4();
let metrics = create_test_metrics().with_user(user_id, None);
assert_eq!(metrics.user_id, Some(user_id));
assert!(metrics.team_id.is_none());
}
#[test]
fn test_with_api_key() {
let api_key_id = Uuid::new_v4();
let metrics = create_test_metrics().with_api_key(api_key_id);
assert_eq!(metrics.api_key_id, Some(api_key_id));
}
#[test]
fn test_with_timing() {
let metrics = create_test_metrics().with_timing(100, 10, 80);
assert_eq!(metrics.response_time_ms, 100);
assert_eq!(metrics.queue_time_ms, 10);
assert_eq!(metrics.provider_time_ms, 80);
}
#[test]
fn test_with_timing_zero_values() {
let metrics = create_test_metrics().with_timing(0, 0, 0);
assert_eq!(metrics.response_time_ms, 0);
assert_eq!(metrics.queue_time_ms, 0);
assert_eq!(metrics.provider_time_ms, 0);
}
#[test]
fn test_with_tokens() {
let metrics = create_test_metrics().with_tokens(1000, 500);
assert_eq!(metrics.token_usage.input_tokens, 1000);
assert_eq!(metrics.token_usage.output_tokens, 500);
assert_eq!(metrics.token_usage.total_tokens, 1500);
}
#[test]
fn test_with_cost() {
let metrics = create_test_metrics().with_cost(0.01, 0.02, "USD".to_string());
assert_eq!(metrics.cost.input_cost, 0.01);
assert_eq!(metrics.cost.output_cost, 0.02);
assert_eq!(metrics.cost.total_cost, 0.03);
assert_eq!(metrics.cost.currency, "USD");
}
#[test]
fn test_with_error() {
let error = ErrorInfo {
code: "rate_limit".to_string(),
message: "Too many requests".to_string(),
error_type: "rate_limit_error".to_string(),
provider_code: None,
stack_trace: None,
};
let metrics = create_test_metrics().with_error(error);
assert!(matches!(metrics.status, RequestStatus::Error));
assert!(metrics.error.is_some());
assert_eq!(metrics.error.as_ref().unwrap().code, "rate_limit");
}
#[test]
fn test_with_cache() {
let cache = CacheMetrics {
hit: true,
..Default::default()
};
let metrics = create_test_metrics().with_cache(cache);
assert!(metrics.cache.hit);
}
#[test]
fn test_builder_chain() {
let user_id = Uuid::new_v4();
let api_key_id = Uuid::new_v4();
let metrics = create_test_metrics()
.with_user(user_id, None)
.with_api_key(api_key_id)
.with_timing(150, 20, 120)
.with_tokens(2000, 1000)
.with_cost(0.05, 0.10, "EUR".to_string());
assert_eq!(metrics.user_id, Some(user_id));
assert_eq!(metrics.api_key_id, Some(api_key_id));
assert_eq!(metrics.response_time_ms, 150);
assert_eq!(metrics.token_usage.total_tokens, 3000);
assert!((metrics.cost.total_cost - 0.15).abs() < 1e-10);
}
#[test]
fn test_request_status_success() {
let metrics = create_test_metrics();
assert!(matches!(metrics.status, RequestStatus::Success));
}
#[test]
fn test_request_status_error() {
let error = ErrorInfo {
code: "error".to_string(),
message: "Error".to_string(),
error_type: "generic_error".to_string(),
provider_code: None,
stack_trace: None,
};
let metrics = create_test_metrics().with_error(error);
assert!(matches!(metrics.status, RequestStatus::Error));
}
#[test]
fn test_request_status_serialization() {
let status = RequestStatus::RateLimit;
let json = serde_json::to_value(&status).unwrap();
assert_eq!(json, "rate_limit");
}
#[test]
fn test_request_status_deserialization() {
let json = "\"quota_exceeded\"";
let status: RequestStatus = serde_json::from_str(json).unwrap();
assert!(matches!(status, RequestStatus::QuotaExceeded));
}
#[test]
fn test_request_metrics_serialization() {
let metrics = create_test_metrics();
let json = serde_json::to_value(&metrics).unwrap();
assert_eq!(json["request_id"], "req-123");
assert_eq!(json["model"], "gpt-4");
assert_eq!(json["provider"], "openai");
}
#[test]
fn test_request_metrics_clone() {
let original = create_test_metrics().with_tokens(100, 50);
let cloned = original.clone();
assert_eq!(original.request_id, cloned.request_id);
assert_eq!(
original.token_usage.input_tokens,
cloned.token_usage.input_tokens
);
}
#[test]
fn test_extra_metadata() {
let mut metrics = create_test_metrics();
metrics
.extra
.insert("custom_key".to_string(), serde_json::json!("custom_value"));
metrics
.extra
.insert("numeric".to_string(), serde_json::json!(42));
assert_eq!(metrics.extra.len(), 2);
assert_eq!(metrics.extra.get("custom_key").unwrap(), "custom_value");
}
}