From cd1bd21f63973c752d0cb0dbb7fcb9652676cbee Mon Sep 17 00:00:00 2001 From: Komisar Date: Thu, 16 Apr 2026 14:59:08 +0300 Subject: [PATCH] Add log_manager concept docs (README.md, AGENTS.md) and update checklist --- CHECKLIST.md | 3 +- src/utils/log_manager/AGENTS.md | 299 ++++++++++++++++++++++++++++++++ src/utils/log_manager/README.md | 249 ++++++++++++++++++++++++++ 3 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 src/utils/log_manager/AGENTS.md create mode 100644 src/utils/log_manager/README.md diff --git a/CHECKLIST.md b/CHECKLIST.md index a45df3b..ee75958 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -18,8 +18,9 @@ ## Pending -- [ ] Создать приложение `app/komAI.py` как точку входа - [ ] Реализовать модуль логирования + - [ ] stdout/stderr перехват при аварийном завершении (on crash) +- [ ] Создать приложение `app/komAI.py` как точку входа - [ ] Реализовать систему модулей (`modules/`) - [ ] Настроить CI/CD - [ ] Написать интеграционные тесты \ No newline at end of file diff --git a/src/utils/log_manager/AGENTS.md b/src/utils/log_manager/AGENTS.md new file mode 100644 index 0000000..68607a3 --- /dev/null +++ b/src/utils/log_manager/AGENTS.md @@ -0,0 +1,299 @@ +# LogManager Implementation Guide + +## Overview + +LogManager wraps Python's `logging` to provide unified logging across all modules. All console output goes through `logger.print()` which handles console output and optional file duplication. + +## Usage Pattern + +```python +import src.utils.log_manager as log + +# Register module +log.register(module="my_module", log_console=True, log_file="my_module.log") + +# Get logger and use +logger = log.get_logger("my_module") +logger.print("message") # to console + file +logger.print("msg", level="warning") # with level for file +log.print("global message") # from root logger +``` + +## Config Constants + +```python +# Category +LOG_CATEGORY = "logger" + +# Global config keys +LOG_CONSOLE = "log_console" +LOG_STDERR = "log_stderr" +LOG_FILE = "log_file" +LOG_PATH = "log_path" +LOG_LEVEL = "log_level" +LOG_FILE_LEVEL = "log_file_level" +LOG_ROTATION = "log_rotation" +LOG_ROTATION_SIZE = "log_rotation_size" +LOG_ROTATION_COUNT = "log_rotation_count" +LOG_TIMESTAMP = "log_timestamp" +LOG_BUFFERED = "log_buffered" + +# Module config keys +MODULE_LOG_CONSOLE = "log_console" +MODULE_LOG_STDERR = "log_stderr" +MODULE_LOG_FILE = "log_file" +MODULE_LOG_LEVEL = "log_level" + +# Defaults +DEFAULT_TIMESTAMP = "%Y-%m-%d %H:%M:%S" +DEFAULT_LOG_LEVEL = "INFO" +DEFAULT_FILE_LEVEL = "DEBUG" +``` + +## Module Registration + +### Global Registration (happens once) + +Before using logging, global parameters must be registered in config: + +```python +def register_global_params(config): + config.register(name=LOG_CONSOLE, val=True, cat=LOG_CATEGORY, desc="Enable console output") + config.register(name=LOG_STDERR, val=True, cat=LOG_CATEGORY, desc="Enable stderr for errors") + config.register(name=LOG_FILE, val="app.log", cat=LOG_CATEGORY, desc="Log file name") + config.register(name=LOG_PATH, val="./log", cat=LOG_CATEGORY, desc="Log directory path") + config.register(name=LOG_LEVEL, val="INFO", cat=LOG_CATEGORY, desc="Console log level") + config.register(name=LOG_FILE_LEVEL, val="DEBUG", cat=LOG_CATEGORY, desc="File log level") + config.register(name=LOG_ROTATION, val="size", cat=LOG_CATEGORY, desc="Rotation type: size|external") + config.register(name=LOG_ROTATION_SIZE, val="10MB", cat=LOG_CATEGORY, desc="Max file size for rotation") + config.register(name=LOG_ROTATION_COUNT, val=5, cat=LOG_CATEGORY, desc="Number of rotated files to keep") + config.register(name=LOG_TIMESTAMP, val=DEFAULT_TIMESTAMP, cat=LOG_CATEGORY, desc="Log timestamp format") + config.register(name=LOG_BUFFERED, val=False, cat=LOG_CATEGORY, desc="Buffer file writes") +``` + +### Module Registration + +Each module registers itself: + +```python +def register(module, log_console=True, log_stderr=False, log_file=None, log_level=None): + """ + Register a module for logging. + + Args: + module: Module name (used as logger name suffix) + log_console: Enable console output (inherits from global if None) + log_stderr: Enable stderr for warnings/errors (inherits if None) + log_file: Log file name (inherits from global if None, None = no file logging) + log_level: Console/file level (inherits from global if None) + """ +``` + +## Config Hierarchy + +``` +# Global settings (category: "logger") +logger.log_console: true +logger.log_stderr: true +logger.log_file: "app.log" +logger.log_path: "./log" +logger.log_level: "INFO" +logger.log_file_level: "DEBUG" +logger.log_rotation: "size" +logger.log_rotation_size: "10MB" +logger.log_rotation_count: 5 +logger.log_timestamp: "%Y-%m-%d %H:%M:%S" +logger.log_buffered: false + +# Module-specific (category: "module_name") +module_name.log_console: false # override global +module_name.log_file: "custom.log" # override global +module_name.log_level: "DEBUG" # override global +``` + +**Inheritance rule:** If module-specific param is not set or None, inherits from global. + +## Class Design + +### LoggerPrint class + +Wrapper around `logging.Logger` that provides `print()` method. + +```python +class LoggerPrint(logging.Logger): + def __init__(self, name, module_name): + super().__init__(name) + self.module_name = module_name + self.console_enabled = True + self.stderr_enabled = True + self.file_enabled = False + self.file_level = "DEBUG" + + def print(self, msg, level="info"): + """Print to console and optionally to file.""" + # Determine if should output to console based on level + # Determine if should output to file based on level + # Handle stdout vs stderr based on level + pass +``` + +### LogManager class + +Factory and configuration holder. + +```python +class LogManager: + def __init__(self, config): + self.config = config + self._loggers = {} # module_name -> LoggerPrint + + def register(self, module, log_console=None, log_stderr=None, + log_file=None, log_level=None): + """Register a module with its logging settings.""" + # Read global config + # Apply module overrides + # Create logger with appropriate handlers + pass + + def get_logger(self, module): + """Get existing logger or create new one.""" + return self._loggers.get(module) + + def setup(self): + """Initialize logging system.""" + # Setup root logger + # Register all modules + pass + + def print(self, msg, level="info"): + """Print from root logger.""" + pass +``` + +### FileHandlerFactory + +Creates appropriate file handler based on rotation settings. + +```python +class FileHandlerFactory: + @staticmethod + def create(path, filename, rotation="size", + max_bytes="10MB", backup_count=5): + """Create file handler with rotation.""" + if rotation == "size": + return RotatingFileHandler( + path / filename, + maxBytes=parse_size(max_bytes), + backupCount=backup_count + ) + else: # external + return FileHandler(path / filename) +``` + +## Key Implementation Details + +### Console Output + +- stdout for levels: debug, info +- stderr for levels: warning, error, critical + +### Level Mapping + +``` +level="debug" → console + file (DEBUG level) +level="info" → console + file (INFO level) +level="warning" → stderr + file (WARNING level) +level="error" → stderr + file (ERROR level) +``` + +### File Path Resolution + +```python +def get_log_path(): + log_path = config.get(LOG_PATH, cat=LOG_CATEGORY) or "./log" + base_path = Path(log_path) + base_path.mkdir(parents=True, exist_ok=True) + return base_path +``` + +### Size Parsing + +```python +def parse_size(size_str): + """Parse size string like '10MB' to bytes.""" + units = {"B": 1, "KB": 1024, "MB": 1024*1024, "GB": 1024*1024*1024} + size_str = size_str.upper().strip() + for unit, multiplier in units.items(): + if size_str.endswith(unit): + return int(size_str[:-len(unit)]) * multiplier + return int(size_str) +``` + +### Module Config Reading + +```python +def get_module_config(module): + """Get effective config for module, merging global with module-specific.""" + global_log_console = config.get(LOG_CONSOLE, cat=LOG_CATEGORY) + global_log_stderr = config.get(LOG_STDERR, cat=LOG_CATEGORY) + global_log_file = config.get(LOG_FILE, cat=LOG_CATEGORY) + global_log_level = config.get(LOG_LEVEL, cat=LOG_CATEGORY) + + module_log_console = config.get(MODULE_LOG_CONSOLE, cat=module) + module_log_stderr = config.get(MODULE_LOG_STDERR, cat=module) + module_log_file = config.get(MODULE_LOG_FILE, cat=module) + module_log_level = config.get(MODULE_LOG_LEVEL, cat=module) + + return { + "log_console": module_log_console if module_log_console is not None else global_log_console, + "log_stderr": module_log_stderr if module_log_stderr is not None else global_log_stderr, + "log_file": module_log_file if module_log_file is not None else global_log_file, + "log_level": module_log_level if module_log_level is not None else global_log_level, + } +``` + +## Module Requirements + +1. **Never use `print()`** - always use `log.print()` +2. **Never use `logging` directly** - use LogManager wrapper +3. **Register module before use** - call `log.register(module="name", ...)` +4. **Use `log.setup()` at app startup** - after all module registrations + +## Usage Flow + +```python +# 1. app/komAI.py +import src.utils.config_manager as config +import src.utils.log_manager as log + +# Load config +config.load() + +# Register global logging params (if not already registered) +log.register_global_params(config) + +# Register modules +log.register(module="model", log_file="model.log") +log.register(module="ui", log_console=True) + +# Setup logging +log.setup() + +# Start application +log.print("Application started") +``` + +## Files Structure + +``` +src/utils/log_manager/ +├── __init__.py # exports log, LogManager class +├── constants.py # LOG_CATEGORY, LOG_CONSOLE, etc. +├── logger.py # LoggerPrint class +├── factory.py # FileHandlerFactory, parse_size +└── manager.py # LogManager class implementation +``` + +## Open Questions + +None - all details specified in this document. diff --git a/src/utils/log_manager/README.md b/src/utils/log_manager/README.md new file mode 100644 index 0000000..97f5297 --- /dev/null +++ b/src/utils/log_manager/README.md @@ -0,0 +1,249 @@ +# LogManager + +## Концепция + +Логирование через `print()` вместо стандартного `logging`. Все сообщения через `logger.print()` выводятся в консоль, дублируются в файл если настроено. + +## Использование в модулях + +### Шаблон модуля с логированием + +```python +# my_module/__init__.py +import src.utils.log_manager as log + +def init(): + log.register( + module="my_module", # имя модуля + log_console=True, # вывод в консоль + log_stderr=False, # вывод в stderr (для ошибок) + log_file="my_module.log" # имя файла (None = не писать в файл) + ) + log.setup() + +def do_something(): + log.print("Starting process...") # вместо print() + log.print("Processing...", level="info") # уровень для лога (default: info) +``` + +### Конфигурация в global.yaml + +```yaml +# Глобальные настройки +logger.log_console: true +logger.log_stderr: true +logger.log_file: "app.log" +logger.log_path: "./log" +logger.log_level: "INFO" +logger.log_file_level: "DEBUG" +logger.log_rotation: "size" +logger.log_rotation_size: "10MB" +logger.log_rotation_count: 5 +logger.log_timestamp: "%Y-%m-%d %H:%M:%S" +logger.log_buffered: false + +# Настройки для конкретного модуля (переопределяют глобальные) +my_module.log_console: false +my_module.log_file: "custom_module.log" +my_module.log_level: "DEBUG" +``` + +### Правила наследования + +1. Параметры модуля `module_name.*` переопределяют глобальные `logger.*` +2. Если параметр модуля не задан - берётся глобальный +3. `log_file: null` означает не писать в файл для этого модуля + +## API + +### Регистрация модуля + +```python +log.register( + module="my_module", + log_console=True, + log_stderr=False, + log_file="my_module.log", + log_level="INFO", + log_file_level="DEBUG" +) +``` + +### Получение логера + +```python +logger = log.get_logger("my_module") +logger.print("message") # вывод в консоль +logger.print("message", level="debug") # с уровнем для лога (default: info) +logger.info("message") # стандартный logging (только в файл) +``` + +### Глобальные функции + +```python +log.print("message") # от root logger +log.setup() # инициализация (вызывается при старте) +``` + +## Константы для конфига + +```python +# Категория логера +LOG_CATEGORY = "logger" + +# Параметры глобальные +LOG_CONSOLE = "log_console" +LOG_STDERR = "log_stderr" +LOG_FILE = "log_file" +LOG_PATH = "log_path" +LOG_LEVEL = "log_level" +LOG_FILE_LEVEL = "log_file_level" +LOG_ROTATION = "log_rotation" +LOG_ROTATION_SIZE = "log_rotation_size" +LOG_ROTATION_COUNT = "log_rotation_count" +LOG_TIMESTAMP = "log_timestamp" +LOG_BUFFERED = "log_buffered" + +# Параметры для модулей (внутри секции module_name) +MODULE_LOG_CONSOLE = "log_console" +MODULE_LOG_STDERR = "log_stderr" +MODULE_LOG_FILE = "log_file" +MODULE_LOG_LEVEL = "log_level" +``` + +## Правила написания модулей + +### Обязательно использовать `log.print()` вместо `print()` + +```python +# НЕПРАВИЛЬНО +print("Message") +print(f"Value: {x}") + +# ПРАВИЛЬНО +log.print("Message") +log.print(f"Value: {x}") +``` + +### Уровни логирования + +```python +log.print("debug message", level="debug") +log.print("info message", level="info") # default +log.print("warning message", level="warning") +log.print("error message", level="error") +``` + +### Инициализация модуля + +```python +# В __init__.py модуля +import src.utils.log_manager as log + +def init(): + log.register( + module="my_module", + log_console=True, + log_file="my_module.log" + ) + +def main(): + log.print("Module started") + # ... work ... +``` + +### Структура модуля с точки зрения логирования + +``` +my_module/ +├── __init__.py # log.register(), init() +├── logger.py # опционально, если нужен свой logger +└── ... +``` + +### Регистрация параметров логирования в своём модуле + +```python +# Модуль может захотеть свои параметры логирования +import src.utils.config_manager as config +import src.utils.log_manager as log + +def init(): + # Регистрация параметров своего модуля + config.register(name="option1", val="value1", cat="my_module", ...) + config.register(name="option2", val="value2", cat="my_module", ...) + + # Регистрация в log_manager для логирования + log.register(module="my_module", ...) +``` + +### Вложенные модули + +```python +# parent_module/__init__.py +import src.utils.log_manager as log + +def init(): + log.register(module="parent") + +# parent_module/child/__init__.py +import src.utils.log_manager as log + +def init(): + log.register(module="parent.child") + log.print("Child module message") +``` + +## Архитектура + +### Обработчики + +- **ConsoleWriter** - вывод в stdout/stderr +- **FileHandler** - запись в файл (с ротацией или без) +- **BufferedWriter** - буферизованный вывод в файл + +### Поток сообщений + +``` +logger.print("msg") + │ + ├─→ ConsoleWriter (если log_console) + │ └─→ stdout (уровни info, debug) + │ └─→ stderr (уровни warning, error) + │ + └─→ FileHandler (если log_file настроен) + └─→ BufferedWriter или direct write +``` + +### Структура логгеров + +``` +komAI # root logger +└── komAI.my_module # logger для модуля + ├─ ConsoleWriter # управляется module.log_console + └─ FileHandler # управляется module.log_file, module.log_level +``` + +## Ротация логов + +### По размеру (`log_rotation=size`) +- `RotatingFileHandler` +- При достижении `log_rotation_size` происходит ротация +- Хранится `log_rotation_count` ротированных файлов + +### Внешняя (`log_rotation=external`) +- Базовый `FileHandler` без ротации +- Внешняя система (logrotate) управляет ротацией + +## Формат timestamp + +По умолчанию: `DEFAULT_TIMESTAMP = "%Y-%m-%d %H:%M:%S"` + +Настраивается через `logger.log_timestamp` в конфиге. + +## Ограничения + +1. **Никогда не использовать `print()`** - только `log.print()` +2. **Не использовать `logging` напрямую** - использовать обёртку +3. **Все модули должны регистрироваться** через `log.register()` +4. **Имя модуля уникально** в рамках проекта