use chrono::{DateTime, Utc};
use reqwest::{Request, Response};
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct DebugConfig {
pub enabled: bool,
pub level: DebugLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DebugLevel {
Basic = 1, Headers = 2, Full = 3, }
impl From<u8> for DebugLevel {
fn from(level: u8) -> Self {
match level {
1 => DebugLevel::Basic,
2 => DebugLevel::Headers,
3 => DebugLevel::Full,
_ => DebugLevel::Basic,
}
}
}
#[derive(Debug)]
pub struct RequestDebugInfo {
pub timestamp: DateTime<Utc>,
pub method: String,
pub url: String,
pub headers: Option<HashMap<String, String>>,
pub body: Option<String>,
pub user_agent: Option<String>,
}
#[derive(Debug)]
pub struct ResponseDebugInfo {
pub timestamp: DateTime<Utc>,
pub status: u16,
pub headers: Option<HashMap<String, String>>,
pub body: Option<String>,
pub content_length: Option<u64>,
pub duration: Duration,
}
#[derive(Debug)]
pub struct DebugSession {
pub request: RequestDebugInfo,
pub response: Option<ResponseDebugInfo>,
pub error: Option<String>,
pub start_time: Instant,
}
impl DebugConfig {
pub fn new(enabled: bool, level: u8) -> Self {
Self {
enabled,
level: DebugLevel::from(level),
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
level: DebugLevel::Basic,
}
}
pub fn log_request(&self, request_info: &RequestDebugInfo, session_id: &str) {
if !self.enabled {
return;
}
println!(
"🔍 [DEBUG] Request {} [{}]",
session_id,
request_info.timestamp.format("%H:%M:%S%.3f")
);
println!(" {} {}", request_info.method, request_info.url);
if let Some(ua) = &request_info.user_agent {
println!(" User-Agent: {}", ua);
}
if self.level >= DebugLevel::Headers {
if let Some(headers) = &request_info.headers {
println!(" Headers:");
for (key, value) in headers {
println!(" {}: {}", key, value);
}
}
}
if self.level >= DebugLevel::Full {
if let Some(body) = &request_info.body {
println!(" Body: {}", body);
}
}
}
pub fn log_response(&self, response_info: &ResponseDebugInfo, session_id: &str) {
if !self.enabled {
return;
}
println!(
"📨 [DEBUG] Response {} [{}]",
session_id,
response_info.timestamp.format("%H:%M:%S%.3f")
);
println!(
" Status: {} ({:?})",
response_info.status, response_info.duration
);
if let Some(content_length) = response_info.content_length {
println!(" Content-Length: {} bytes", content_length);
}
if self.level >= DebugLevel::Headers {
if let Some(headers) = &response_info.headers {
println!(" Headers:");
for (key, value) in headers {
println!(" {}: {}", key, value);
}
}
}
if self.level >= DebugLevel::Full {
if let Some(body) = &response_info.body {
let preview = if body.len() > 500 {
format!("{}... ({} chars total)", &body[..500], body.len())
} else {
body.clone()
};
println!(" Body: {}", preview);
}
}
}
pub fn log_error(&self, error: &str, session_id: &str, duration: Duration) {
if !self.enabled {
return;
}
println!("❌ [DEBUG] Error {} ({:?})", session_id, duration);
println!(" {}", error);
}
pub fn start_session(&self, method: &str, url: &str) -> Option<DebugSession> {
if !self.enabled {
return None;
}
let request_info = RequestDebugInfo {
timestamp: Utc::now(),
method: method.to_string(),
url: url.to_string(),
headers: None,
body: None,
user_agent: None,
};
Some(DebugSession {
request: request_info,
response: None,
error: None,
start_time: Instant::now(),
})
}
pub fn generate_session_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
format!("{:06}", COUNTER.fetch_add(1, Ordering::SeqCst))
}
}
impl RequestDebugInfo {
pub fn from_reqwest_request(
request: &Request,
include_headers: bool,
include_body: bool,
) -> Self {
let mut headers = None;
let mut user_agent = None;
if include_headers {
let mut header_map = HashMap::new();
for (name, value) in request.headers() {
if let Ok(value_str) = value.to_str() {
let name_str = name.as_str();
if name_str.to_lowercase() == "user-agent" {
user_agent = Some(value_str.to_string());
}
header_map.insert(name_str.to_string(), value_str.to_string());
}
}
if !header_map.is_empty() {
headers = Some(header_map);
}
} else {
if let Some(ua_value) = request.headers().get("user-agent") {
if let Ok(ua_str) = ua_value.to_str() {
user_agent = Some(ua_str.to_string());
}
}
}
let body = if include_body {
None
} else {
None
};
Self {
timestamp: Utc::now(),
method: request.method().to_string(),
url: request.url().to_string(),
headers,
body,
user_agent,
}
}
}
impl ResponseDebugInfo {
pub async fn from_reqwest_response(
response: Response,
include_headers: bool,
include_body: bool,
duration: Duration,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let status = response.status().as_u16();
let content_length = response.content_length();
let mut headers = None;
if include_headers {
let mut header_map = HashMap::new();
for (name, value) in response.headers() {
if let Ok(value_str) = value.to_str() {
header_map.insert(name.as_str().to_string(), value_str.to_string());
}
}
if !header_map.is_empty() {
headers = Some(header_map);
}
}
let body = if include_body {
let bytes = response.bytes().await?;
let body_text = String::from_utf8_lossy(&bytes).to_string();
Some(body_text)
} else {
None
};
let debug_info = Self {
timestamp: Utc::now(),
status,
headers,
body,
content_length,
duration,
};
Ok(debug_info)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug_level_conversion() {
assert_eq!(DebugLevel::from(1), DebugLevel::Basic);
assert_eq!(DebugLevel::from(2), DebugLevel::Headers);
assert_eq!(DebugLevel::from(3), DebugLevel::Full);
assert_eq!(DebugLevel::from(99), DebugLevel::Basic); }
#[test]
fn test_debug_config() {
let config = DebugConfig::new(true, 2);
assert!(config.enabled);
assert_eq!(config.level, DebugLevel::Headers);
let disabled = DebugConfig::disabled();
assert!(!disabled.enabled);
assert_eq!(disabled.level, DebugLevel::Basic);
}
#[test]
fn test_session_id_generation() {
let id1 = DebugConfig::generate_session_id();
let id2 = DebugConfig::generate_session_id();
assert_ne!(id1, id2);
assert_eq!(id1.len(), 6);
assert_eq!(id2.len(), 6);
}
}