Add log_manager concept docs (README.md, AGENTS.md) and update checklist

This commit is contained in:
2026-04-16 14:59:08 +03:00
parent 547362de12
commit cd1bd21f63
3 changed files with 550 additions and 1 deletions

View File

@@ -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.

View File

@@ -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. **Имя модуля уникально** в рамках проекта