Add log_manager concept docs (README.md, AGENTS.md) and update checklist
This commit is contained in:
@@ -18,8 +18,9 @@
|
||||
|
||||
## Pending
|
||||
|
||||
- [ ] Создать приложение `app/komAI.py` как точку входа
|
||||
- [ ] Реализовать модуль логирования
|
||||
- [ ] stdout/stderr перехват при аварийном завершении (on crash)
|
||||
- [ ] Создать приложение `app/komAI.py` как точку входа
|
||||
- [ ] Реализовать систему модулей (`modules/`)
|
||||
- [ ] Настроить CI/CD
|
||||
- [ ] Написать интеграционные тесты
|
||||
299
src/utils/log_manager/AGENTS.md
Normal file
299
src/utils/log_manager/AGENTS.md
Normal 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.
|
||||
249
src/utils/log_manager/README.md
Normal file
249
src/utils/log_manager/README.md
Normal 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. **Имя модуля уникально** в рамках проекта
|
||||
Reference in New Issue
Block a user