log-full 0.0.1

A simple, asynchronous log library
Documentation
//! Quickwit 集成模块
//! 
//! 提供将日志发送到 Quickwit 的功能

use crate::error::{LogError, LogResult};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

/// Quickwit 配置
#[derive(Debug, Clone)]
pub struct QuickwitConfig {
    /// Quickwit 服务器 URL
    pub url: String,
    /// 索引 ID
    pub index_id: String,
    /// 连接超时时间(秒)
    pub timeout: u64,
    /// 批量发送大小
    pub batch_size: usize,
    /// 是否启用
    pub enabled: bool,
    /// 认证 token(可选)
    pub token: Option<String>,
}

impl Default for QuickwitConfig {
    fn default() -> Self {
        Self {
            url: String::new(),
            index_id: String::new(),
            timeout: 30,
            batch_size: 100,
            enabled: false,
            token: None,
        }
    }
}

impl QuickwitConfig {
    /// 创建新的 Quickwit 配置
    pub fn new(url: String, index_id: String) -> Self {
        Self {
            url,
            index_id,
            timeout: 30,
            batch_size: 100,
            enabled: true,
            token: None,
        }
    }

    /// 设置超时时间
    pub fn with_timeout(mut self, timeout: u64) -> Self {
        self.timeout = timeout;
        self
    }

    /// 设置批量大小
    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
        self.batch_size = batch_size;
        self
    }

    /// 设置认证 token
    pub fn with_token<T: Into<String>>(mut self, token: T) -> Self {
        self.token = Some(token.into());
        self
    }

    /// 验证配置
    pub fn validate(&self) -> LogResult<()> {
        if !self.enabled {
            return Ok(());
        }
        
        if self.url.is_empty() {
            return Err(LogError::config("Quickwit URL cannot be empty"));
        }
        
        if self.index_id.is_empty() {
            return Err(LogError::config("Quickwit index_id cannot be empty"));
        }
        
        if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
            return Err(LogError::config("Quickwit URL must start with http:// or https://"));
        }
        
        Ok(())
    }
}

/// Quickwit 日志条目
#[derive(Debug, Clone)]
pub struct QuickwitLogEntry {
    /// 时间戳
    pub timestamp: u64,
    /// 日志级别
    pub level: String,
    /// 日志消息
    pub message: String,
    /// 模块路径
    pub module: Option<String>,
    /// 文件名
    pub file: Option<String>,
    /// 行号
    pub line: Option<u32>,
    /// 进程 ID
    pub process_id: Option<u32>,
    /// 线程 ID
    pub thread_id: Option<String>,
    /// 自定义字段
    pub custom_fields: HashMap<String, String>,
}

impl QuickwitLogEntry {
    /// 创建新的日志条目
    pub fn new(level: log::Level, message: String) -> Self {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
            
        Self {
            timestamp,
            level: level.to_string(),
            message,
            module: None,
            file: None,
            line: None,
            process_id: None,
            thread_id: None,
            custom_fields: HashMap::new(),
        }
    }

    /// 从 log::Record 创建日志条目
    pub fn from_record(record: &log::Record) -> Self {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
            
        let mut entry = Self {
            timestamp,
            level: record.level().to_string(),
            message: format!("{}", record.args()),
            module: Some(record.target().to_string()),
            file: record.file().map(|f| f.to_string()),
            line: record.line(),
            process_id: Some(std::process::id()),
            thread_id: std::thread::current().name().map(|n| n.to_string()),
            custom_fields: HashMap::new(),
        };
        
        // 如果没有线程名,使用线程 ID
        if entry.thread_id.is_none() {
            entry.thread_id = Some(format!("{:?}", std::thread::current().id()));
        }
        
        entry
    }

    /// 添加自定义字段
    pub fn add_field<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
        self.custom_fields.insert(key.into(), value.into());
        self
    }

    /// 转换为 JSON 字符串
    pub fn to_json(&self) -> LogResult<String> {
        let escaped_message = self.message.replace('"', "\\\"");
        let mut json = format!(
            "{{\"timestamp\":{},\"level\":\"{}\",\"message\":\"{}\"",
            self.timestamp,
            self.level,
            escaped_message
        );
        
        if let Some(ref module) = self.module {
            json.push_str(&format!(",\"module\":\"{}\"", module));
        }
        
        if let Some(ref file) = self.file {
            json.push_str(&format!(",\"file\":\"{}\"", file));
        }
        
        if let Some(line) = self.line {
            json.push_str(&format!(",\"line\":{}", line));
        }
        
        if let Some(process_id) = self.process_id {
            json.push_str(&format!(",\"process_id\":{}", process_id));
        }
        
        if let Some(ref thread_id) = self.thread_id {
            json.push_str(&format!(",\"thread_id\":\"{}\"", thread_id));
        }
        
        // 添加自定义字段
        for (key, value) in &self.custom_fields {
            let escaped_value = value.replace('"', "\\\"");
            json.push_str(&format!(
                ",\"{}\":\"{}\"",
                key,
                escaped_value
            ));
        }
        
        json.push('}');
        Ok(json)
    }
}

/// Quickwit 客户端
pub struct QuickwitClient {
    config: QuickwitConfig,
    client: Option<Arc<dyn HttpClient + Send + Sync>>,
}

/// HTTP 客户端 trait
pub trait HttpClient {
    fn post(&self, url: &str, body: &str) -> LogResult<()>;
    fn post_with_token(&self, url: &str, body: &str, token: Option<&str>) -> LogResult<()>;
}

/// 简单的 HTTP 客户端实现(使用标准库)
pub struct SimpleHttpClient;

impl HttpClient for SimpleHttpClient {
    fn post(&self, url: &str, body: &str) -> LogResult<()> {
        self.post_with_token(url, body, None)
    }

    fn post_with_token(&self, url: &str, body: &str, token: Option<&str>) -> LogResult<()> {
        // 这里使用简单的 HTTP 实现
        // 在实际项目中,建议使用 reqwest 或其他 HTTP 客户端库
        use std::io::Write;
        use std::net::TcpStream;
        
        let url_parts: Vec<&str> = url.splitn(3, '/').collect();
        if url_parts.len() < 3 {
            return Err(LogError::custom("Invalid URL format"));
        }
        
        let host_port = url_parts[2].split('/').next().unwrap_or("");
        let path = &url[url.find(host_port).unwrap() + host_port.len()..];
        
        let (host, port) = if let Some(colon_pos) = host_port.find(':') {
            (&host_port[..colon_pos], host_port[colon_pos + 1..].parse().unwrap_or(80))
        } else {
            (host_port, if url.starts_with("https") { 443 } else { 80 })
        };
        
        let mut stream = TcpStream::connect((host, port))
            .map_err(|e| LogError::custom(format!("Failed to connect: {}", e)))?;
        
        let mut headers = format!(
            "POST {} HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\nContent-Length: {}",
            path, host, body.len()
        );
        
        // 添加 Authorization header(如果提供了 token)
        if let Some(token) = token {
            headers.push_str(&format!("\r\nAuthorization: Bearer {}", token));
        }
        
        headers.push_str("\r\n\r\n");
        let request = format!("{}{}", headers, body);
        
        stream.write_all(request.as_bytes())
            .map_err(|e| LogError::custom(format!("Failed to send request: {}", e)))?;
        
        Ok(())
    }
}

impl QuickwitClient {
    /// 创建新的 Quickwit 客户端
    pub fn new(config: QuickwitConfig) -> LogResult<Self> {
        config.validate()?;
        
        Ok(Self {
            config,
            client: Some(Arc::new(SimpleHttpClient)),
        })
    }

    /// 设置自定义 HTTP 客户端
    pub fn with_client(mut self, client: Arc<dyn HttpClient + Send + Sync>) -> Self {
        self.client = Some(client);
        self
    }

    /// 发送单个日志条目
    pub fn send_log(&self, entry: &QuickwitLogEntry) -> LogResult<()> {
        if !self.config.enabled {
            return Ok(());
        }
        
        let json = entry.to_json()?;
        let url = format!("{}/api/v1/{}/ingest", self.config.url, self.config.index_id);
        
        if let Some(ref client) = self.client {
            client.post_with_token(&url, &json, self.config.token.as_deref())?;
        }
        
        Ok(())
    }

    /// 批量发送日志条目
    pub fn send_logs(&self, entries: &[QuickwitLogEntry]) -> LogResult<()> {
        if !self.config.enabled || entries.is_empty() {
            return Ok(());
        }
        
        let mut json_array = String::from("[");
        for (i, entry) in entries.iter().enumerate() {
            if i > 0 {
                json_array.push(',');
            }
            json_array.push_str(&entry.to_json()?);
        }
        json_array.push(']');
        
        self.send_json(&json_array)
    }

    /// 发送 JSON 数据到 Quickwit
    fn send_json(&self, json: &str) -> LogResult<()> {
        let url = format!("{}/api/v1/{}/ingest", self.config.url, self.config.index_id);
        
        if let Some(ref client) = self.client {
            client.post_with_token(&url, json, self.config.token.as_deref())?;
        }
        
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_quickwit_config() {
        let config = QuickwitConfig::new(
            "http://localhost:7280".to_string(),
            "my-index".to_string(),
        );
        
        assert!(config.validate().is_ok());
        assert!(config.enabled);
    }

    #[test]
    fn test_log_entry_json() {
        let entry = QuickwitLogEntry::new(log::Level::Info, "Test message".to_string())
            .add_field("custom", "value");
        
        let json = entry.to_json().unwrap();
        assert!(json.contains("Test message"));
        assert!(json.contains("custom"));
    }

    #[test]
    fn test_invalid_config() {
        let config = QuickwitConfig::new(
            "invalid-url".to_string(),
            "my-index".to_string(),
        );
        
        assert!(config.validate().is_err());
    }
}