use chrono::Utc;
use serde::Serialize;
use serde_json::Value;
use std::io::Write;
use std::sync::{Arc, Mutex};
use std::time::Instant;
#[derive(Debug, Clone, Serialize)]
pub struct LogEntry {
pub ts: String,
pub model: String,
pub provider: String,
pub latency_ms: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub reasoning_tokens: u64,
pub total_tokens: u64,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
impl LogEntry {
pub fn from_response(
provider: &str,
model: &str,
response: &Value,
latency: std::time::Duration,
) -> Self {
let usage = response
.get("usage")
.cloned()
.unwrap_or(serde_json::json!({}));
Self {
ts: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
model: model.to_string(),
provider: provider.to_string(),
latency_ms: latency.as_millis() as u64,
input_tokens: usage
.get("prompt_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0),
output_tokens: usage
.get("completion_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0),
reasoning_tokens: usage
.get("reasoning_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0),
total_tokens: usage
.get("total_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0),
status: "ok".to_string(),
error: None,
request_id: response
.get("id")
.and_then(|v| v.as_str())
.map(String::from),
}
}
pub fn from_error(
provider: &str,
model: &str,
error: &str,
latency: std::time::Duration,
) -> Self {
Self {
ts: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
model: model.to_string(),
provider: provider.to_string(),
latency_ms: latency.as_millis() as u64,
input_tokens: 0,
output_tokens: 0,
reasoning_tokens: 0,
total_tokens: 0,
status: "error".to_string(),
error: Some(error.to_string()),
request_id: None,
}
}
}
pub struct Logger {
writer: Arc<Mutex<Box<dyn Write + Send>>>,
}
impl Logger {
pub fn to_file(path: &str) -> std::io::Result<Self> {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
Ok(Self {
writer: Arc::new(Mutex::new(Box::new(file))),
})
}
pub fn to_stderr() -> Self {
Self {
writer: Arc::new(Mutex::new(Box::new(std::io::stderr()))),
}
}
pub fn log(&self, entry: &LogEntry) {
if let Ok(json) = serde_json::to_string(entry) {
if let Ok(mut writer) = self.writer.lock() {
let _ = writeln!(writer, "{}", json);
}
}
}
}
pub struct RequestTimer {
start: Instant,
}
impl RequestTimer {
pub fn start() -> Self {
Self {
start: Instant::now(),
}
}
pub fn elapsed(&self) -> std::time::Duration {
self.start.elapsed()
}
}