inklog 0.2.0

Enterprise-grade Rust logging infrastructure
Documentation
// Copyright (c) 2026 Kirky.X
// SPDX-License-Identifier: MIT
// 多 Sink 降级功能测试
// 测试 File Sink 故障时降级到 Database,Database 故障时降级到 Console
// 验证降级后消息不丢失,以及恢复后自动切回

#[cfg(test)]
mod multi_sink_fallback_test {
    use inklog::config::{ConsoleSinkConfig, DatabaseDriver, DatabaseSinkConfig, FileSinkConfig};
    use inklog::metrics::{FallbackAction, FallbackConfig, FallbackState};
    use inklog::{InklogConfig, LoggerManager};
    use std::fs;
    use std::path::PathBuf;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    use tempfile::TempDir;
    use inklog::tokio::sync::Mutex;
    use inklog::tokio::time::Duration;

    // === 降级状态测试 ===

    #[test]
    fn test_fallback_state_creation() {
        let state = FallbackState::new();
        
        assert!(!state.is_fallback_active());
        assert_eq!(state.get_current_action(), FallbackAction::None);
        assert_eq!(state.get_retry_count(), 0);
    }

    #[test]
    fn test_fallback_state_transitions() {
        let state = FallbackState::new();
        
        // 初始状态
        assert!(!state.is_fallback_active());
        
        // 触发降级
        state.enter_fallback(FallbackAction::Database);
        assert!(state.is_fallback_active());
        assert_eq!(state.get_current_action(), FallbackAction::Database);
        
        // 恢复
        state.exit_fallback();
        assert!(!state.is_fallback_active());
        assert_eq!(state.get_current_action(), FallbackAction::None);
    }

    #[test]
    fn test_fallback_state_retry_counting() {
        let state = FallbackState::new();
        
        // 初始重试计数
        assert_eq!(state.get_retry_count(), 0);
        
        // 触发降级
        state.enter_fallback(FallbackAction::Console);
        assert_eq!(state.get_retry_count(), 1);
        
        // 再次触发
        state.enter_fallback(FallbackAction::Console);
        assert_eq!(state.get_retry_count(), 2);
        
        // 恢复后重置
        state.exit_fallback();
        assert_eq!(state.get_retry_count(), 0);
    }

    #[test]
    fn test_fallback_config_defaults() {
        let config = FallbackConfig::default();
        
        assert!(config.auto_fallback);
        assert_eq!(config.initial_delay_ms, 1000);
        assert_eq!(config.max_delay_ms, 60000);
        assert_eq!(config.max_retries, 10);
    }

    // === 文件到数据库降级测试 ===

    #[tokio::test]
    async fn test_file_sink_failure_triggers_fallback() {
        let temp_dir = TempDir::new().unwrap();
        let db_path = temp_dir.path().join("test.db");
        let db_url = format!("sqlite://{}", db_path.display());
        
        // 创建无效的日志路径以触发文件写入失败
        let invalid_path = PathBuf::from("/nonexistent/path/that/does/not/exist/log.log");
        
        let file_config = FileSinkConfig {
            enabled: true,
            path: invalid_path,
            max_size: "1MB".into(),
            ..Default::default()
        };
        
        let db_config = DatabaseSinkConfig {
            enabled: true,
            driver: DatabaseDriver::SQLite,
            url: db_url.clone(),
            pool_size: 2,
            batch_size: 10,
            flush_interval_ms: 100,
            table_name: "logs".to_string(),
            ..Default::default()
        };
        
        let config = InklogConfig {
            global: inklog::config::GlobalConfig {
                auto_fallback: true,
                fallback_initial_delay_ms: 100,
                fallback_max_retries: 3,
                ..Default::default()
            },
            file_sink: Some(file_config),
            database_sink: Some(db_config),
            console_sink: Some(ConsoleSinkConfig {
                enabled: true,
                ..Default::default()
            }),
            ..Default::default()
        };
        
        let logger = LoggerManager::with_config(config).await.unwrap();
        
        // 写入日志(应该触发降级)
        log::info!("Testing fallback from file to database");
        log::warn!("This should be logged via fallback mechanism");
        
        // 等待处理
        tokio::time::sleep(Duration::from_millis(500)).await;
        
        // 验证日志仍然被记录(通过降级路径)
        // 如果降级成功,消息应该出现在控制台或数据库中
        log::info!("Fallback test completed");
        
        logger.shutdown().await.ok();
    }

    // === 数据库到文件降级测试 ===

    #[tokio::test]
    async fn test_database_sink_failure_triggers_fallback() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("fallback_test.log");
        
        // 使用无效的数据库 URL
        let invalid_db_url = "postgres://invalid:invalid@nonexistent:5432/nonexistent";
        
        let file_config = FileSinkConfig {
            enabled: true,
            path: file_path,
            max_size: "1MB".into(),
            ..Default::default()
        };
        
        let db_config = DatabaseSinkConfig {
            enabled: true,
            driver: DatabaseDriver::PostgreSQL,
            url: invalid_db_url.to_string(),
            pool_size: 1,
            batch_size: 10,
            flush_interval_ms: 100,
            table_name: "logs".to_string(),
            ..Default::default()
        };
        
        let config = InklogConfig {
            global: inklog::config::GlobalConfig {
                auto_fallback: true,
                fallback_initial_delay_ms: 100,
                fallback_max_retries: 3,
                ..Default::default()
            },
            file_sink: Some(file_config),
            database_sink: Some(db_config),
            console_sink: Some(ConsoleSinkConfig {
                enabled: true,
                ..Default::default()
            }),
            ..Default::default()
        };
        
        let logger = LoggerManager::with_config(config).await.unwrap();
        
        // 写入日志(数据库连接失败应该触发降级到文件)
        log::info!("Testing fallback from database to file");
        log::error!("Database connection failed, falling back to file");
        
        // 等待处理
        tokio::time::sleep(Duration::from_millis(500)).await;
        
        // 验证文件存在(降级成功)
        if file_path.exists() {
            let content = fs::read_to_string(&file_path).unwrap();
            assert!(content.contains("fallback") || content.contains("database"));
        }
        
        log::info!("Database fallback test completed");
        
        logger.shutdown().await.ok();
    }

    // === 所有 Sink 不可用时的处理 ===

    #[tokio::test]
    async fn test_all_sinks_unavailable() {
        let temp_dir = TempDir::new().unwrap();
        
        // 无效的文件路径
        let invalid_file = PathBuf::from("/invalid/path/fallback.log");
        
        // 无效的数据库
        let invalid_db = "mysql://invalid:invalid@localhost:3306/invalid";
        
        let config = InklogConfig {
            global: inklog::config::GlobalConfig {
                auto_fallback: true,
                fallback_initial_delay_ms: 50,
                fallback_max_retries: 2,
                ..Default::default()
            },
            file_sink: Some(FileSinkConfig {
                enabled: true,
                path: invalid_file,
                ..Default::default()
            }),
            database_sink: Some(DatabaseSinkConfig {
                enabled: true,
                driver: DatabaseDriver::MySQL,
                url: invalid_db.to_string(),
                pool_size: 1,
                table_name: "logs".to_string(),
                ..Default::default()
            }),
            console_sink: Some(ConsoleSinkConfig {
                enabled: true,
                ..Default::default()
            }),
            ..Default::default()
        };
        
        // 即使文件和数据库都不可用,控制台 Sink 应该始终可用
        let logger = LoggerManager::with_config(config).await.unwrap();
        
        // 消息应该至少记录到控制台
        log::info!("All sinks unavailable test - message should appear in console");
        log::warn!("Fallback to console successful");
        
        tokio::time::sleep(Duration::from_millis(200)).await;
        
        log::info!("Console fallback test completed");
        
        logger.shutdown().await.ok();
    }

    // === 降级恢复测试 ===

    #[tokio::test]
    async fn test_fallback_recovery() {
        use std::sync::atomic::AtomicUsize;
        use std::sync::Arc;
        
        let state = Arc::new(FallbackState::new());
        
        // 初始状态
        assert!(!state.is_fallback_active());
        
        // 模拟降级
        state.enter_fallback(FallbackAction::Database);
        assert!(state.is_fallback_active());
        
        // 模拟恢复
        state.exit_fallback();
        assert!(!state.is_fallback_active());
    }

    // === 并发降级场景测试 ===

    #[tokio::test]
    async fn test_concurrent_fallback_scenarios() {
        use std::sync::Arc;
        use inklog::tokio::sync::Barrier;
        use std::sync::atomic::{AtomicUsize, Ordering};
        
        let temp_dir = TempDir::new().unwrap();
        let db_path = temp_dir.path().join("concurrent.db");
        let db_url = format!("sqlite://{}", db_path.display());
        
        let file_config = FileSinkConfig {
            enabled: true,
            path: PathBuf::from("/invalid/concurrent.log"),
            ..Default::default()
        };
        
        let db_config = DatabaseSinkConfig {
            enabled: true,
            driver: DatabaseDriver::SQLite,
            url: db_url.clone(),
            pool_size: 5,
            batch_size: 100,
            flush_interval_ms: 50,
            table_name: "logs".to_string(),
            ..Default::default()
        };
        
        let config = InklogConfig {
            global: inklog::config::GlobalConfig {
                auto_fallback: true,
                ..Default::default()
            },
            file_sink: Some(file_config),
            database_sink: Some(db_config),
            console_sink: Some(ConsoleSinkConfig::default()),
            ..Default::default()
        };
        
        let logger = LoggerManager::with_config(config).await.unwrap();
        
        // 并发写入测试
        let barrier = Arc::new(Barrier::new(10));
        let counter = Arc::new(AtomicUsize::new(0));
        
        let handles: Vec<_> = (0..10)
            .map(|_| {
                let barrier = barrier.clone();
                let counter = counter.clone();
                tokio::spawn(async move {
                    barrier.wait().await;
                    for _ in 0..10 {
                        log::info!("Concurrent fallback test message");
                        counter.fetch_add(1, Ordering::SeqCst);
                    }
                })
            })
            .collect();
        
        for handle in handles {
            handle.await.unwrap();
        }
        
        // 验证所有消息被处理
        assert_eq!(counter.load(Ordering::SeqCst), 100);
        
        log::info!("Concurrent fallback test completed");
        
        logger.shutdown().await.ok();
    }

    // === 降级配置测试 ===

    #[test]
    fn test_fallback_config_options() {
        let config = FallbackConfig {
            auto_fallback: true,
            initial_delay_ms: 500,
            max_delay_ms: 30000,
            max_retries: 5,
        };
        
        assert!(config.auto_fallback);
        assert_eq!(config.initial_delay_ms, 500);
        assert_eq!(config.max_delay_ms, 30000);
        assert_eq!(config.max_retries, 5);
    }

    #[test]
    fn test_fallback_action_variants() {
        // 验证所有降级动作类型
        let actions = [
            FallbackAction::None,
            FallbackAction::Console,
            FallbackAction::File,
            FallbackAction::Database,
        ];
        
        for action in actions {
            let state = FallbackState::new();
            state.enter_fallback(action);
            assert_eq!(state.get_current_action(), action);
        }
    }
}