Jelajahi Sumber

开始测试日志管理功能.

wangxq 1 Minggu lalu
induk
melakukan
98b52e1065

+ 88 - 0
config/logging_config.yaml

@@ -0,0 +1,88 @@
+version: 1
+
+# 全局配置
+global:
+  base_level: INFO
+  
+# 默认配置(用于app.log)
+default:
+  level: INFO
+  console:
+    enabled: true
+    level: INFO
+    format: "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
+  file:
+    enabled: true
+    level: DEBUG
+    filename: "app.log"
+    format: "%(asctime)s [%(levelname)s] [%(name)s] [user:%(user_id)s] [session:%(session_id)s] %(filename)s:%(lineno)d - %(message)s"
+    rotation:
+      enabled: true
+      max_size: "50MB"
+      backup_count: 10
+
+# 模块特定配置
+modules:
+  app:
+    level: INFO
+    console:
+      enabled: true
+      level: INFO
+      format: "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
+    file:
+      enabled: true
+      level: DEBUG
+      filename: "app.log"
+      format: "%(asctime)s [%(levelname)s] [%(name)s] [user:%(user_id)s] [session:%(session_id)s] %(filename)s:%(lineno)d - %(message)s"
+      rotation:
+        enabled: true
+        max_size: "50MB"
+        backup_count: 10
+  
+  data_pipeline:
+    level: DEBUG
+    console:
+      enabled: true
+      level: INFO
+      format: "%(asctime)s [%(levelname)s] Pipeline: %(message)s"
+    file:
+      enabled: true
+      level: DEBUG
+      filename: "data_pipeline.log"
+      format: "%(asctime)s [%(levelname)s] [%(name)s] %(filename)s:%(lineno)d - %(message)s"
+      rotation:
+        enabled: true
+        max_size: "30MB"
+        backup_count: 8
+  
+  agent:
+    level: DEBUG
+    console:
+      enabled: true
+      level: INFO
+      format: "%(asctime)s [%(levelname)s] Agent: %(message)s"
+    file:
+      enabled: true
+      level: DEBUG
+      filename: "agent.log"
+      format: "%(asctime)s [%(levelname)s] [%(name)s] [user:%(user_id)s] [session:%(session_id)s] %(filename)s:%(lineno)d - %(message)s"
+      rotation:
+        enabled: true
+        max_size: "30MB"
+        backup_count: 8
+  
+  vanna:
+    level: INFO
+    console:
+      enabled: true
+      level: INFO
+      format: "%(asctime)s [%(levelname)s] Vanna: %(message)s"
+    file:
+      enabled: true
+      level: DEBUG
+      filename: "vanna.log"
+      format: "%(asctime)s [%(levelname)s] [%(name)s] %(filename)s:%(lineno)d - %(message)s"
+      rotation:
+        enabled: true
+        max_size: "20MB"
+        backup_count: 5 

+ 41 - 0
core/logging/__init__.py

@@ -0,0 +1,41 @@
+from .log_manager import LogManager
+import logging
+
+# 全局日志管理器实例
+_log_manager = LogManager()
+
+def initialize_logging(config_path: str = "config/logging_config.yaml"):
+    """初始化项目日志系统"""
+    _log_manager.initialize(config_path)
+
+def get_logger(name: str, module: str = "default") -> logging.Logger:
+    """获取logger实例 - 主要API"""
+    return _log_manager.get_logger(name, module)
+
+# 便捷方法
+def get_data_pipeline_logger(name: str) -> logging.Logger:
+    """获取data_pipeline模块logger"""
+    return get_logger(name, "data_pipeline")
+
+def get_agent_logger(name: str) -> logging.Logger:
+    """获取agent模块logger"""
+    return get_logger(name, "agent")
+
+def get_vanna_logger(name: str) -> logging.Logger:
+    """获取vanna模块logger"""
+    return get_logger(name, "vanna")
+
+def get_app_logger(name: str) -> logging.Logger:
+    """获取app模块logger"""
+    return get_logger(name, "app")
+
+# 上下文管理便捷方法
+def set_log_context(**kwargs):
+    """设置日志上下文(可选)
+    示例: set_log_context(user_id='user123', session_id='sess456')
+    """
+    _log_manager.set_context(**kwargs)
+
+def clear_log_context():
+    """清除日志上下文"""
+    _log_manager.clear_context() 

+ 201 - 0
core/logging/log_manager.py

@@ -0,0 +1,201 @@
+import logging
+import logging.handlers
+import os
+from typing import Dict, Optional
+from pathlib import Path
+import yaml
+import contextvars
+
+# 上下文变量,存储可选的上下文信息
+log_context = contextvars.ContextVar('log_context', default={})
+
+class ContextFilter(logging.Filter):
+    """添加上下文信息到日志记录"""
+    def filter(self, record):
+        ctx = log_context.get()
+        # 设置默认值,避免格式化错误
+        record.session_id = ctx.get('session_id', 'N/A')
+        record.user_id = ctx.get('user_id', 'anonymous')
+        record.request_id = ctx.get('request_id', 'N/A')
+        return True
+
+class LogManager:
+    """统一日志管理器 - 类似Log4j的功能"""
+    
+    _instance = None
+    _loggers: Dict[str, logging.Logger] = {}
+    _initialized = False
+    _fallback_to_console = False  # 标记是否降级到控制台
+    
+    def __new__(cls):
+        if cls._instance is None:
+            cls._instance = super().__new__(cls)
+        return cls._instance
+    
+    def __init__(self):
+        if not self._initialized:
+            self.config = None
+            self.base_log_dir = Path("logs")
+            self._setup_base_directory()
+            LogManager._initialized = True
+    
+    def initialize(self, config_path: str = "config/logging_config.yaml"):
+        """初始化日志系统"""
+        self.config = self._load_config(config_path)
+        self._setup_base_directory()
+        self._configure_root_logger()
+    
+    def get_logger(self, name: str, module: str = "default") -> logging.Logger:
+        """获取指定模块的logger"""
+        logger_key = f"{module}.{name}"
+        
+        if logger_key not in self._loggers:
+            logger = logging.getLogger(logger_key)
+            self._configure_logger(logger, module)
+            self._loggers[logger_key] = logger
+        
+        return self._loggers[logger_key]
+    
+    def set_context(self, **kwargs):
+        """设置日志上下文(可选)"""
+        ctx = log_context.get()
+        ctx.update(kwargs)
+        log_context.set(ctx)
+    
+    def clear_context(self):
+        """清除日志上下文"""
+        log_context.set({})
+    
+    def _load_config(self, config_path: str) -> dict:
+        """加载配置文件"""
+        try:
+            with open(config_path, 'r', encoding='utf-8') as f:
+                return yaml.safe_load(f)
+        except FileNotFoundError:
+            import sys
+            sys.stderr.write(f"[WARNING] 配置文件 {config_path} 未找到,使用默认配置\n")
+            return self._get_default_config()
+        except Exception as e:
+            import sys
+            sys.stderr.write(f"[ERROR] 加载配置文件失败: {e},使用默认配置\n")
+            return self._get_default_config()
+    
+    def _get_default_config(self) -> dict:
+        """获取默认配置"""
+        return {
+            'global': {'base_level': 'INFO'},
+            'default': {
+                'level': 'INFO',
+                'console': {
+                    'enabled': True,
+                    'level': 'INFO',
+                    'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
+                },
+                'file': {
+                    'enabled': True,
+                    'level': 'DEBUG',
+                    'filename': 'app.log',
+                    'format': '%(asctime)s [%(levelname)s] [%(name)s] [user:%(user_id)s] [session:%(session_id)s] %(filename)s:%(lineno)d - %(message)s',
+                    'rotation': {
+                        'enabled': True,
+                        'max_size': '50MB',
+                        'backup_count': 10
+                    }
+                }
+            },
+            'modules': {}
+        }
+    
+    def _setup_base_directory(self):
+        """创建日志目录(带降级策略)"""
+        try:
+            os.makedirs(self.base_log_dir, exist_ok=True)
+            self._fallback_to_console = False
+        except Exception as e:
+            import sys
+            sys.stderr.write(f"[WARNING] 无法创建日志目录 {self.base_log_dir},将只使用控制台输出: {e}\n")
+            self._fallback_to_console = True
+    
+    def _configure_root_logger(self):
+        """配置根日志器"""
+        root_logger = logging.getLogger()
+        root_logger.setLevel(getattr(logging, self.config['global']['base_level'].upper()))
+    
+    def _configure_logger(self, logger: logging.Logger, module: str):
+        """配置具体的logger"""
+        module_config = self.config.get('modules', {}).get(module, self.config['default'])
+        
+        # 设置日志级别
+        level = getattr(logging, module_config['level'].upper())
+        logger.setLevel(level)
+        
+        # 清除已有处理器
+        logger.handlers.clear()
+        logger.propagate = False
+        
+        # 添加控制台处理器
+        if module_config.get('console', {}).get('enabled', True):
+            console_handler = self._create_console_handler(module_config['console'])
+            console_handler.addFilter(ContextFilter())
+            logger.addHandler(console_handler)
+        
+        # 添加文件处理器(如果没有降级到控制台)
+        if not self._fallback_to_console and module_config.get('file', {}).get('enabled', True):
+            try:
+                file_handler = self._create_file_handler(module_config['file'], module)
+                file_handler.addFilter(ContextFilter())
+                logger.addHandler(file_handler)
+            except Exception as e:
+                import sys
+                sys.stderr.write(f"[WARNING] 无法创建文件处理器: {e}\n")
+                # 如果文件处理器创建失败,标记降级
+                self._fallback_to_console = True
+    
+    def _create_console_handler(self, console_config: dict) -> logging.StreamHandler:
+        """创建控制台处理器"""
+        handler = logging.StreamHandler()
+        handler.setLevel(getattr(logging, console_config.get('level', 'INFO').upper()))
+        
+        formatter = logging.Formatter(
+            console_config.get('format', '%(asctime)s [%(levelname)s] %(name)s: %(message)s'),
+            datefmt='%Y-%m-%d %H:%M:%S'
+        )
+        handler.setFormatter(formatter)
+        return handler
+    
+    def _create_file_handler(self, file_config: dict, module: str) -> logging.Handler:
+        """创建文件处理器(支持自动轮转)"""
+        log_file = self.base_log_dir / file_config.get('filename', f'{module}.log')
+        
+        # 使用RotatingFileHandler实现自动轮转和清理
+        rotation_config = file_config.get('rotation', {})
+        if rotation_config.get('enabled', False):
+            handler = logging.handlers.RotatingFileHandler(
+                log_file,
+                maxBytes=self._parse_size(rotation_config.get('max_size', '50MB')),
+                backupCount=rotation_config.get('backup_count', 10),
+                encoding='utf-8'
+            )
+        else:
+            handler = logging.FileHandler(log_file, encoding='utf-8')
+        
+        handler.setLevel(getattr(logging, file_config.get('level', 'DEBUG').upper()))
+        
+        formatter = logging.Formatter(
+            file_config.get('format', '%(asctime)s [%(levelname)s] [%(name)s] %(filename)s:%(lineno)d - %(message)s'),
+            datefmt='%Y-%m-%d %H:%M:%S'
+        )
+        handler.setFormatter(formatter)
+        return handler
+    
+    def _parse_size(self, size_str: str) -> int:
+        """解析大小字符串,如 '50MB' -> 字节数"""
+        size_str = size_str.upper()
+        if size_str.endswith('KB'):
+            return int(size_str[:-2]) * 1024
+        elif size_str.endswith('MB'):
+            return int(size_str[:-2]) * 1024 * 1024
+        elif size_str.endswith('GB'):
+            return int(size_str[:-2]) * 1024 * 1024 * 1024
+        else:
+            return int(size_str) 

+ 114 - 0
docs/日志服务改造检查报告.md

@@ -0,0 +1,114 @@
+# 日志服务改造检查报告
+
+## 检查时间
+2024年1月(具体日期根据实际情况调整)
+
+## 检查总结
+经过全面检查,日志服务改造工作**基本完成**。项目已经成功从分散的print语句迁移到统一的日志系统,实现了4个模块独立日志文件的目标。
+
+## 改造完成情况
+
+| 模块 | 状态 | 文件数 | 说明 |
+|------|------|--------|------|
+| **基础架构** | ✅ 完成 | 4 | 日志系统核心已创建并完善 |
+| **App模块** | ✅ 完成 | 7 | 包括citu_app.py和common目录 |
+| **Agent模块** | ✅ 完成 | 8 | 所有print语句已替换 |
+| **Vanna模块** | ✅ 完成 | 6 | customllm/custompgvector/customembedding |
+| **Data Pipeline模块** | ✅ 完成 | 15+ | 已改造为新系统,logger.py已成为兼容层 |
+
+## 已修复的问题
+
+1. **核心日志系统的print语句**
+   - 文件:`core/logging/log_manager.py`
+   - 修复:将print替换为`sys.stderr.write`,避免循环依赖
+
+2. **Core模块的print语句**
+   - 文件:`core/embedding_function.py`
+   - 修复:将test_embedding_connection函数中的print替换为logger调用
+
+3. **Common模块的print语句**
+   - 文件:`common/utils.py`
+   - 修复:将print_current_config函数改为使用logger
+
+## 合理保留的print语句
+
+以下文件的print语句是合理的,不需要改造:
+
+### CLI工具
+- `data_pipeline/validators/sql_validate_cli.py` - 命令行界面输出
+- `data_pipeline/trainer/run_training.py` - 训练脚本进度显示
+
+### 示例程序
+- `data_pipeline/validators/sql_validation_example.py` - 演示代码
+
+这些文件本质上是独立的工具或脚本,使用print进行终端输出是合理的。
+
+## 改造成果
+
+1. **统一的日志管理**
+   - 通过`LogManager`单例模式管理所有日志
+   - 提供统一的API接口
+
+2. **4个独立日志文件**
+   - `logs/app.log` - 主应用日志
+   - `logs/agent.log` - Agent模块日志
+   - `logs/vanna.log` - Vanna相关日志
+   - `logs/data_pipeline.log` - 数据处理管道日志
+
+3. **灵活的配置系统**
+   - 通过`config/logging_config.yaml`配置
+   - 支持模块级别的独立配置
+
+4. **上下文支持**
+   - 支持user_id、session_id等上下文信息
+   - 使用contextvars实现线程安全
+
+5. **错误降级**
+   - 文件系统不可用时自动降级到控制台
+   - 保证日志系统的健壮性
+
+## 验证方法
+
+### 1. 基础功能验证
+```python
+# 测试日志系统初始化
+from core.logging import initialize_logging, get_app_logger
+
+initialize_logging()
+logger = get_app_logger("TestModule")
+logger.info("测试日志输出")
+```
+
+### 2. 模块隔离验证
+- 检查logs目录下是否生成4个独立的日志文件
+- 确认各模块的日志只出现在对应的文件中
+
+### 3. 上下文功能验证
+```python
+from core.logging import set_log_context, clear_log_context
+
+set_log_context(user_id="test_user", session_id="test_session")
+logger.info("带上下文的日志")
+clear_log_context()
+```
+
+## 后续建议
+
+### 短期建议(1-2周)
+1. **全面测试**:运行完整的应用流程,验证日志输出
+2. **日志级别调整**:根据实际需要调整各模块的日志级别
+3. **监控设置**:设置日志文件大小监控和告警
+
+### 中期建议(1-3个月)
+1. **性能优化**:评估日志系统对应用性能的影响
+2. **日志分析**:建立日志分析和统计机制
+3. **集中管理**:考虑集成ELK或其他日志管理系统
+
+### 长期建议(3-6个月)
+1. **异步日志**:如果性能有影响,考虑实现异步日志
+2. **结构化日志**:考虑使用JSON格式的结构化日志
+3. **日志归档**:实现自动化的日志归档和清理策略
+
+## 结论
+
+日志服务改造工作已经按照设计方案成功完成。所有核心模块都已迁移到新的统一日志系统,实现了模块独立、配置灵活、易于管理的目标。建议在生产环境部署前进行充分的测试验证。 

+ 120 - 0
test_logging.py

@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+日志系统测试脚本
+用于验证日志服务改造是否成功
+"""
+
+import os
+import sys
+import time
+from pathlib import Path
+
+# 添加项目根目录到Python路径
+project_root = Path(__file__).parent
+sys.path.insert(0, str(project_root))
+
+def test_logging_system():
+    """测试日志系统的各个功能"""
+    print("=" * 60)
+    print("开始测试日志系统...")
+    print("=" * 60)
+    
+    # 1. 测试日志系统初始化
+    try:
+        from core.logging import initialize_logging, get_app_logger, get_agent_logger, get_vanna_logger, get_data_pipeline_logger
+        from core.logging import set_log_context, clear_log_context
+        
+        # 初始化日志系统
+        initialize_logging()
+        print("✅ 日志系统初始化成功")
+    except Exception as e:
+        print(f"❌ 日志系统初始化失败: {e}")
+        return
+    
+    # 2. 测试各模块的logger
+    print("\n测试各模块的logger...")
+    
+    # App模块
+    try:
+        app_logger = get_app_logger("TestApp")
+        app_logger.info("这是App模块的测试日志")
+        app_logger.warning("这是App模块的警告日志")
+        app_logger.error("这是App模块的错误日志")
+        print("✅ App模块logger测试成功")
+    except Exception as e:
+        print(f"❌ App模块logger测试失败: {e}")
+    
+    # Agent模块
+    try:
+        agent_logger = get_agent_logger("TestAgent")
+        agent_logger.info("这是Agent模块的测试日志")
+        agent_logger.debug("这是Agent模块的调试日志")
+        print("✅ Agent模块logger测试成功")
+    except Exception as e:
+        print(f"❌ Agent模块logger测试失败: {e}")
+    
+    # Vanna模块
+    try:
+        vanna_logger = get_vanna_logger("TestVanna")
+        vanna_logger.info("这是Vanna模块的测试日志")
+        vanna_logger.warning("这是Vanna模块的警告日志")
+        print("✅ Vanna模块logger测试成功")
+    except Exception as e:
+        print(f"❌ Vanna模块logger测试失败: {e}")
+    
+    # Data Pipeline模块
+    try:
+        dp_logger = get_data_pipeline_logger("TestDataPipeline")
+        dp_logger.info("这是Data Pipeline模块的测试日志")
+        dp_logger.error("这是Data Pipeline模块的错误日志")
+        print("✅ Data Pipeline模块logger测试成功")
+    except Exception as e:
+        print(f"❌ Data Pipeline模块logger测试失败: {e}")
+    
+    # 3. 测试上下文功能
+    print("\n测试上下文功能...")
+    try:
+        # 设置上下文
+        set_log_context(user_id="test_user_123", session_id="session_456")
+        app_logger.info("这是带上下文的日志")
+        
+        # 清除上下文
+        clear_log_context()
+        app_logger.info("这是清除上下文后的日志")
+        print("✅ 上下文功能测试成功")
+    except Exception as e:
+        print(f"❌ 上下文功能测试失败: {e}")
+    
+    # 4. 检查日志文件
+    print("\n检查日志文件...")
+    log_dir = Path("logs")
+    expected_files = ["app.log", "agent.log", "vanna.log", "data_pipeline.log"]
+    
+    if log_dir.exists():
+        print(f"日志目录: {log_dir.absolute()}")
+        for log_file in expected_files:
+            file_path = log_dir / log_file
+            if file_path.exists():
+                size = file_path.stat().st_size
+                print(f"✅ {log_file} - 大小: {size} bytes")
+                
+                # 显示最后几行日志
+                try:
+                    with open(file_path, 'r', encoding='utf-8') as f:
+                        lines = f.readlines()
+                        if lines:
+                            print(f"   最新日志: {lines[-1].strip()}")
+                except Exception as e:
+                    print(f"   读取日志失败: {e}")
+            else:
+                print(f"❌ {log_file} - 文件不存在")
+    else:
+        print(f"❌ 日志目录不存在: {log_dir}")
+    
+    print("\n" + "=" * 60)
+    print("日志系统测试完成!")
+    print("=" * 60)
+
+if __name__ == "__main__":
+    test_logging_system()