use crate::utils::types::{ApiKey, ModelName, RequestId, UserId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogContext {
pub request_id: Option<RequestId>,
pub user_id: Option<UserId>,
pub organization_id: Option<Uuid>,
pub api_key: Option<String>,
pub model: Option<ModelName>,
pub provider: Option<String>,
pub extra: HashMap<String, serde_json::Value>,
}
impl LogContext {
pub fn new() -> Self {
Self {
request_id: None,
user_id: None,
organization_id: None,
api_key: None,
model: None,
provider: None,
extra: HashMap::new(),
}
}
#[allow(dead_code)] pub fn with_request_id(mut self, request_id: RequestId) -> Self {
self.request_id = Some(request_id);
self
}
#[allow(dead_code)] pub fn with_user_id(mut self, user_id: UserId) -> Self {
self.user_id = Some(user_id);
self
}
#[allow(dead_code)] pub fn with_organization_id(mut self, org_id: Uuid) -> Self {
self.organization_id = Some(org_id);
self
}
#[allow(dead_code)] pub fn with_api_key(mut self, api_key: &ApiKey) -> Self {
self.api_key = Some(api_key.as_display_str());
self
}
#[allow(dead_code)] pub fn with_model(mut self, model: ModelName) -> Self {
self.model = Some(model);
self
}
#[allow(dead_code)] pub fn with_provider(mut self, provider: String) -> Self {
self.provider = Some(provider);
self
}
#[allow(dead_code)] pub fn with_field<T: Serialize>(mut self, key: &str, value: T) -> Self {
if let Ok(json_value) = serde_json::to_value(value) {
self.extra.insert(key.to_string(), json_value);
}
self
}
#[allow(dead_code)] pub fn context_fields(&self) -> String {
let mut fields = Vec::new();
if let Some(request_id) = &self.request_id {
fields.push(format!("request_id={}", request_id.as_str()));
}
if let Some(user_id) = &self.user_id {
fields.push(format!("user_id={}", user_id));
}
if let Some(model) = &self.model {
fields.push(format!("model={}", model.as_str()));
}
if let Some(provider) = &self.provider {
fields.push(format!("provider={}", provider));
}
fields.join(", ")
}
}
impl Default for LogContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PerformanceMetrics {
pub duration_ms: u64,
pub memory_bytes: Option<u64>,
pub db_queries: Option<u32>,
pub cache_hits: Option<u32>,
pub cache_misses: Option<u32>,
pub tokens_used: Option<u32>,
pub cost_usd: Option<f64>,
}
impl PerformanceMetrics {
#[allow(dead_code)] pub fn new(duration: Duration) -> Self {
Self {
duration_ms: duration.as_millis() as u64,
memory_bytes: None,
db_queries: None,
cache_hits: None,
cache_misses: None,
tokens_used: None,
cost_usd: None,
}
}
#[allow(dead_code)] pub fn with_memory(mut self, bytes: u64) -> Self {
self.memory_bytes = Some(bytes);
self
}
#[allow(dead_code)] pub fn with_db_queries(mut self, count: u32) -> Self {
self.db_queries = Some(count);
self
}
#[allow(dead_code)] pub fn with_cache_stats(mut self, hits: u32, misses: u32) -> Self {
self.cache_hits = Some(hits);
self.cache_misses = Some(misses);
self
}
#[allow(dead_code)] pub fn with_tokens(mut self, tokens: u32) -> Self {
self.tokens_used = Some(tokens);
self
}
#[allow(dead_code)] pub fn with_cost(mut self, cost: f64) -> Self {
self.cost_usd = Some(cost);
self
}
}
pub struct StructuredLogger {
context: LogContext,
}
impl StructuredLogger {
#[allow(dead_code)] pub fn new(context: LogContext) -> Self {
Self { context }
}
#[allow(dead_code)] pub fn info(&self, message: &str) {
let context_str = self.context.context_fields();
info!("{} | {}", message, context_str);
}
#[allow(dead_code)] pub fn warn(&self, message: &str) {
let context_str = self.context.context_fields();
warn!("{} | {}", message, context_str);
}
#[allow(dead_code)] pub fn error(&self, message: &str, error: Option<&dyn std::error::Error>) {
let context_str = self.context.context_fields();
if let Some(err) = error {
error!("{} | {} | error={}", message, context_str, err);
} else {
error!("{} | {}", message, context_str);
}
}
#[allow(dead_code)] pub fn debug(&self, message: &str) {
let context_str = self.context.context_fields();
debug!("{} | {}", message, context_str);
}
#[allow(dead_code)] pub fn performance(&self, operation: &str, metrics: PerformanceMetrics) {
let context_str = self.context.context_fields();
info!(
"Performance metrics: operation={}, metrics={:?} | {}",
operation, metrics, context_str
);
}
#[allow(dead_code)] pub fn api_request(&self, method: &str, path: &str, status_code: u16, duration: Duration) {
let context_str = self.context.context_fields();
info!(
"API request completed: method={}, path={}, status_code={}, duration_ms={} | {}",
method,
path,
status_code,
duration.as_millis(),
context_str
);
}
#[allow(dead_code)] pub fn database_operation(
&self,
operation: &str,
table: &str,
duration: Duration,
rows_affected: Option<u64>,
) {
let context_str = self.context.context_fields();
debug!(
"Database operation completed: operation={}, table={}, duration_ms={}, rows_affected={:?} | {}",
operation,
table,
duration.as_millis(),
rows_affected,
context_str
);
}
#[allow(dead_code)] pub fn cache_operation(&self, operation: &str, key: &str, hit: bool, duration: Duration) {
let context_str = self.context.context_fields();
debug!(
"Cache operation completed: operation={}, key={}, hit={}, duration_ms={} | {}",
operation,
key,
hit,
duration.as_millis(),
context_str
);
}
#[allow(dead_code)] pub fn provider_interaction(
&self,
provider: &str,
model: &str,
tokens: Option<u32>,
cost: Option<f64>,
duration: Duration,
) {
let context_str = self.context.context_fields();
info!(
"Provider interaction completed: provider={}, model={}, tokens={:?}, cost_usd={:?}, duration_ms={} | {}",
provider,
model,
tokens,
cost,
duration.as_millis(),
context_str
);
}
}
pub struct Timer {
start: Instant,
operation: String,
logger: StructuredLogger,
}
impl Timer {
#[allow(dead_code)] pub fn start(operation: String, logger: StructuredLogger) -> Self {
Self {
start: Instant::now(),
operation,
logger,
}
}
#[allow(dead_code)] pub fn stop(self) {
let duration = self.start.elapsed();
self.logger
.performance(&self.operation, PerformanceMetrics::new(duration));
}
#[allow(dead_code)] pub fn stop_with_metrics(self, metrics: PerformanceMetrics) {
self.logger.performance(&self.operation, metrics);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_context() {
let context = LogContext::new()
.with_request_id(RequestId::new())
.with_user_id(UserId::new())
.with_field("test_field", "test_value");
assert!(context.request_id.is_some());
assert!(context.user_id.is_some());
assert!(context.extra.contains_key("test_field"));
}
#[test]
fn test_performance_metrics() {
let metrics = PerformanceMetrics::new(Duration::from_millis(100))
.with_memory(1024)
.with_db_queries(5)
.with_cache_stats(10, 2)
.with_tokens(150)
.with_cost(0.001);
assert_eq!(metrics.duration_ms, 100);
assert_eq!(metrics.memory_bytes, Some(1024));
assert_eq!(metrics.db_queries, Some(5));
assert_eq!(metrics.cache_hits, Some(10));
assert_eq!(metrics.cache_misses, Some(2));
assert_eq!(metrics.tokens_used, Some(150));
assert_eq!(metrics.cost_usd, Some(0.001));
}
#[test]
fn test_structured_logger() {
let context = LogContext::new().with_request_id(RequestId::new());
let logger = StructuredLogger::new(context);
logger.info("Test info message");
logger.debug("Test debug message");
}
#[test]
fn test_timer() {
let context = LogContext::new();
let logger = StructuredLogger::new(context);
let timer = Timer::start("test_operation".to_string(), logger);
std::thread::sleep(Duration::from_millis(1));
timer.stop();
}
}