401 lines
17 KiB
Markdown
401 lines
17 KiB
Markdown
# Config Manager
|
||
## Описание
|
||
Пакет предназначен как базовое приложение для проектов, в которых нужно периодически запускать одну и ту же функцию в одном потоке, с возможностью перезагрузки конфига и сервисным контуром вокруг прикладной логики.
|
||
|
||
Под сервисным контуром здесь понимаются:
|
||
- логирование;
|
||
- трассировка бизнес-процессов и связанных сущностей;
|
||
- управление приложением через каналы управления (например, HTTP API).
|
||
|
||
**Контракт:** приложение наследует **ConfigManagerV2**, переопределяет **execute()** (периодическая работа). Управление (старт/стоп, health) — через каналы, которые создаются снаружи и передаются в конструктор в **control_channels** (в т.ч. HttpControlChannel для API).
|
||
|
||
## ConfigManager: устройство и взаимосвязи
|
||
|
||
**ConfigManager** (класс ConfigManagerV2) — точка входа приложения. Он наследует внутреннюю логику от **\_RuntimeController** (циклы воркера и обновления конфига, запуск/остановка каналов управления).
|
||
|
||
**Ядро (core):**
|
||
- **ConfigLoader** — читает конфиг из файла (YAML/JSON), считает хеш и отдаёт конфиг только при изменении; при ошибке парсинга возвращает последний валидный конфиг.
|
||
- **WorkerLoop** — в отдельном потоке циклически вызывает ваш метод `execute()` с паузой между вызовами; реагирует на событие остановки и колбэки успеха/ошибки.
|
||
- **LogManager** — применяет секцию `log` из конфига к логированию (dictConfig).
|
||
- **TraceManager** — управляет структурированной трассировкой процессов, контекстов и сообщений.
|
||
- **HealthAggregator** — собирает состояние: жизненный цикл (idle/starting/running/…), время последнего успешного `execute()` и таймаут здоровья; формирует единый ответ для health (ok/unhealthy).
|
||
- **ControlChannelBridge** — один мост для всех каналов: обработчики on_start/on_stop/on_status (сброс/установка halt, текст статуса).
|
||
|
||
**Каналы управления (control):**
|
||
- **ControlChannel** — абстрактный контракт: `start(on_start, on_stop, on_status)`, `stop()`.
|
||
- **HttpControlChannel** — HTTP API (`/health`, `/actions/start`, `/actions/stop`, `/actions/status`); использует **UvicornServerRunner**; для `/health` вызывает **HealthAggregator.collect()**, для действий — переданные обработчики из **ControlChannelBridge**.
|
||
- **TelegramControlChannel** — реализация через long polling Telegram; команды `/start`, `/stop`, `/status` вызывают переданные обработчики.
|
||
|
||
**Поток работы:** при `start()` менеджер поднимает каналы из **control_channels** (заданные снаружи), затем запускает два цикла: **WorkerLoop** и периодическое обновление конфига через **ConfigLoader**. Управление по API: `/health`, `/actions/start`, `/actions/stop` — если в control_channels передан **HttpControlChannel**. Остановка по halt завершает оба цикла; в конце останавливаются все каналы.
|
||
|
||
## Запуск приложения с ConfigManagerV2 и HttpControlChannel
|
||
|
||
1. **Наследуйте ConfigManagerV2** и реализуйте метод `execute()` (в нём — ваша периодическая работа). При необходимости переопределите `get_health_status()` для кастомного ответа `/health`.
|
||
|
||
2. **Создайте каналы снаружи и передайте в конструктор.** Для HTTP API создайте **HttpControlChannel**; для health нужен колбэк менеджера — передайте **control_channels** как фабрику (lambda, получающую менеджер):
|
||
```python
|
||
from config_manager.v2.control import HttpControlChannel
|
||
|
||
app = MyApp(
|
||
str(path_to_config),
|
||
control_channels=lambda m: [
|
||
HttpControlChannel(
|
||
host="0.0.0.0",
|
||
port=8000,
|
||
timeout=3,
|
||
health_provider=m.get_health_provider(),
|
||
)
|
||
],
|
||
)
|
||
```
|
||
Либо передайте готовый список каналов: `control_channels=[channel1, channel2]`.
|
||
|
||
3. **Запустите из async-контекста:** `await app.start()` или `asyncio.create_task(app.start())` для фона. Остановка: `await app.stop()` или запрос `/actions/stop` по HTTP.
|
||
|
||
**Минимальный пример с HTTP API:**
|
||
|
||
```python
|
||
import asyncio
|
||
import logging
|
||
from pathlib import Path
|
||
|
||
from config_manager import ConfigManager
|
||
from config_manager.v2.control import HttpControlChannel
|
||
|
||
class MyApp(ConfigManager):
|
||
def execute(self) -> None:
|
||
pass # ваша периодическая работа
|
||
|
||
async def main() -> None:
|
||
app = MyApp(
|
||
str(Path(__file__).parent / "config.yaml"),
|
||
control_channels=lambda m: [
|
||
HttpControlChannel(
|
||
host="0.0.0.0", port=8000, timeout=3,
|
||
health_provider=m.get_health_provider(),
|
||
)
|
||
],
|
||
)
|
||
asyncio.create_task(app.start())
|
||
await asyncio.sleep(3600)
|
||
|
||
if __name__ == "__main__":
|
||
logging.basicConfig(level=logging.INFO)
|
||
asyncio.run(main())
|
||
```
|
||
|
||
Готовый пример: `tests/test_app.py`.
|
||
|
||
## Диаграмма классов
|
||
|
||
```mermaid
|
||
classDiagram
|
||
direction TB
|
||
|
||
class ConfigManagerV2 {
|
||
+str path
|
||
+Any config
|
||
+float update_interval
|
||
+float work_interval
|
||
-ConfigLoader _loader
|
||
-LifecycleState _state
|
||
+start() async
|
||
+stop() async
|
||
+execute()*
|
||
+get_health_status() HealthPayload
|
||
-_run() async
|
||
-_worker_loop() async
|
||
-_periodic_update_loop() async
|
||
}
|
||
|
||
class _RuntimeController {
|
||
<<внутренний>>
|
||
-_on_execute_success()
|
||
-_on_execute_error(exc)
|
||
-_worker_loop() async
|
||
-_periodic_update_loop() async
|
||
-_start_control_channels() async
|
||
-_stop_control_channels() async
|
||
-_run() async
|
||
}
|
||
|
||
class ConfigLoader {
|
||
+str path
|
||
+Any config
|
||
+Any last_valid_config
|
||
+load_if_changed() async
|
||
+parse_config(data) Any
|
||
-_read_file_sync() str
|
||
-read_file_async() async
|
||
}
|
||
|
||
class WorkerLoop {
|
||
-Callable execute
|
||
-Callable get_interval
|
||
-Event halt_event
|
||
+run() async
|
||
}
|
||
|
||
class LogManager {
|
||
+apply_config(config) None
|
||
}
|
||
|
||
class TraceManager {
|
||
+bind_context(alias, parent_id, type, attrs) str
|
||
+open_context(alias, parent_id, type, attrs) contextmanager
|
||
+current_trace_id() str
|
||
+step(name) None
|
||
+info(message, status, attrs) None
|
||
+warning(message, status, attrs) None
|
||
+error(message, status, attrs) None
|
||
}
|
||
|
||
class TraceContextStore {
|
||
+current() ActiveTraceContext
|
||
+current_trace_id() str
|
||
+push(record) ActiveTraceContext
|
||
+pop() ActiveTraceContext
|
||
+set_step(step) ActiveTraceContext
|
||
}
|
||
|
||
class TraceContextRecord {
|
||
+str trace_id
|
||
+str parent_id
|
||
+str alias
|
||
+str type
|
||
+datetime event_time
|
||
+dict attrs
|
||
}
|
||
|
||
class TraceLogMessage {
|
||
+str trace_id
|
||
+str step
|
||
+str status
|
||
+str level
|
||
+str message
|
||
+datetime event_time
|
||
+dict attrs
|
||
}
|
||
|
||
class TraceTransport {
|
||
<<protocol>>
|
||
+write_context(record) None
|
||
+write_message(record) None
|
||
}
|
||
|
||
class MySqlTraceTransport {
|
||
+write_context(record) None
|
||
+write_message(record) None
|
||
}
|
||
|
||
class ControlChannel {
|
||
<<абстрактный>>
|
||
+start(on_start, on_stop, on_status) async*
|
||
+stop() async*
|
||
}
|
||
|
||
class TelegramControlChannel {
|
||
-str _token
|
||
-int _chat_id
|
||
+start(on_start, on_stop, on_status) async
|
||
+stop() async
|
||
-_poll_loop() async
|
||
}
|
||
|
||
class HttpControlChannel {
|
||
-UvicornServerRunner _runner
|
||
-Callable _health_provider
|
||
+start(on_start, on_stop, on_status) async
|
||
+stop() async
|
||
+int port
|
||
}
|
||
|
||
class HealthAggregator {
|
||
-Callable get_state
|
||
-Callable get_app_health
|
||
+collect() async HealthPayload
|
||
}
|
||
|
||
class ControlChannelBridge {
|
||
-Event _halt
|
||
-Callable _get_state
|
||
-Callable _get_status
|
||
+on_start() async str
|
||
+on_stop() async str
|
||
+on_status() async str
|
||
}
|
||
|
||
class UvicornServerRunner {
|
||
-Server _server
|
||
-Task _serve_task
|
||
+start(app) async
|
||
+stop() async
|
||
+int port
|
||
}
|
||
|
||
ConfigManagerV2 --|> _RuntimeController : наследует
|
||
ConfigManagerV2 --> ConfigLoader : использует
|
||
ConfigManagerV2 --> LogManager : использует
|
||
ConfigManagerV2 --> TraceManager : использует
|
||
ConfigManagerV2 --> HealthAggregator : использует
|
||
ConfigManagerV2 --> ControlChannelBridge : использует
|
||
ConfigManagerV2 ..> ControlChannel : список каналов
|
||
_RuntimeController ..> WorkerLoop : создаёт в _worker_loop
|
||
TraceManager --> TraceContextStore : использует
|
||
TraceManager --> TraceTransport : использует
|
||
TraceManager ..> TraceContextRecord : создаёт
|
||
TraceManager ..> TraceLogMessage : создаёт
|
||
MySqlTraceTransport --|> TraceTransport : реализует
|
||
TelegramControlChannel --|> ControlChannel : реализует
|
||
HttpControlChannel --|> ControlChannel : реализует
|
||
HttpControlChannel --> UvicornServerRunner : использует
|
||
HttpControlChannel ..> HealthAggregator : health_provider
|
||
ControlChannelBridge ..> ControlChannel : on_start, on_stop, on_status
|
||
```
|
||
|
||
## Диаграмма последовательности (запуск и работа)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
participant User
|
||
participant ConfigManagerV2
|
||
participant ControlChannel as ControlChannel(s)
|
||
participant WorkerLoop
|
||
participant ConfigLoader
|
||
participant Client as HTTP Client
|
||
|
||
User->>ConfigManagerV2: start()
|
||
ConfigManagerV2->>ConfigManagerV2: _start_control_channels()
|
||
ConfigManagerV2->>ControlChannel: start(on_start, on_stop, on_status)
|
||
ControlChannel-->>ConfigManagerV2: started
|
||
|
||
par Циклы работы
|
||
ConfigManagerV2->>WorkerLoop: run()
|
||
loop Периодически
|
||
WorkerLoop->>ConfigManagerV2: execute()
|
||
ConfigManagerV2-->>WorkerLoop: success/error
|
||
end
|
||
and
|
||
loop Периодически
|
||
ConfigManagerV2->>ConfigLoader: load_if_changed()
|
||
ConfigLoader-->>ConfigManagerV2: config / unchanged
|
||
end
|
||
end
|
||
|
||
Note over Client,ControlChannel: Запросы по HTTP API (если HttpControlChannel)
|
||
|
||
Client->>ControlChannel: GET /health
|
||
ControlChannel->>ConfigManagerV2: health_provider.collect()
|
||
ConfigManagerV2-->>ControlChannel: HealthPayload
|
||
ControlChannel-->>Client: 200 OK
|
||
|
||
Client->>ControlChannel: POST /actions/stop
|
||
ControlChannel->>ConfigManagerV2: on_stop() (bridge)
|
||
ConfigManagerV2->>ConfigManagerV2: set halt
|
||
ConfigManagerV2->>WorkerLoop: halt_event.set()
|
||
ConfigManagerV2->>ControlChannel: stop()
|
||
ControlChannel-->>Client: 200 OK
|
||
```
|
||
|
||
## Логирование
|
||
Логирование настраивается из конфигурационного файла только если в нём есть секция **`log`** в формате [dictConfig](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig). Если секции `log` нет, менеджер пишет предупреждение в лог, а уровень Python по умолчанию (WARNING) сохраняется — сообщения INFO/DEBUG могут не отображаться.
|
||
|
||
**Как проверить, что конфигурация логирования применилась:**
|
||
- Убедитесь, что путь к файлу конфига верный и файл загружается при старте (в логах нет ошибки чтения конфига).
|
||
- Убедитесь, что в конфиге есть ключ `log` с `version: 1`, `handlers` и `loggers` (пример — `tests/config.yaml`).
|
||
- После старта в логе должно появиться сообщение уровня INFO: `"Logging configuration applied"` (из `config_manager.v2.core.log_manager`). Если его нет, либо секция `log` отсутствует (будет предупреждение), либо уровень root/пакета выше INFO.
|
||
|
||
## Trace
|
||
Модуль `trace` предназначен для структурированной трассировки прикладных процессов и иерархически связанных сущностей.
|
||
|
||
Базовая идея:
|
||
- есть `TraceContextRecord` — логический контекст, который группирует сообщения;
|
||
- есть `TraceLogMessage` — отдельное событие внутри текущего контекста;
|
||
- контексты могут быть вложенными: один родительский контекст и много дочерних;
|
||
- активный контекст хранится в `TraceContextStore`, а при выходе из дочернего `with` автоматически восстанавливается родитель.
|
||
|
||
### Архитектура
|
||
Основные части модуля:
|
||
- `TraceManager` — публичный API для приложений;
|
||
- `TraceContextStore` — хранение активного контекста и стека вложенности;
|
||
- `TraceContextRecord` — описание контекста;
|
||
- `TraceLogMessage` — описание сообщения;
|
||
- `TraceTransport` — интерфейс транспорта;
|
||
- `MySqlTraceTransport` — запись контекстов и сообщений в MySQL.
|
||
|
||
Сущности:
|
||
|
||
`TraceContextRecord`
|
||
- `trace_id`
|
||
- `parent_id`
|
||
- `alias`
|
||
- `type`
|
||
- `event_time`
|
||
- `attrs`
|
||
|
||
`TraceLogMessage`
|
||
- `trace_id`
|
||
- `event_time`
|
||
- `step`
|
||
- `status`
|
||
- `level`
|
||
- `message`
|
||
- `attrs`
|
||
|
||
### Принцип использования
|
||
1. На старте процесса создаётся контекст через `bind_context()` или `open_context()`.
|
||
2. Для серии сообщений выставляется текущий `step()`.
|
||
3. Сообщения пишутся через `info()/warning()/error()/exception()`.
|
||
4. При использовании `open_context()` дочерний контекст автоматически закрывается по выходу из `with`, а родительский становится текущим снова.
|
||
|
||
### Пример: корневой контекст
|
||
```python
|
||
from config_manager.v2.trace import TraceManager
|
||
|
||
trace = TraceManager()
|
||
|
||
trace_id = trace.bind_context(
|
||
alias="job-123",
|
||
type="task",
|
||
attrs={"source": "scheduler"},
|
||
)
|
||
|
||
trace.step("prepare")
|
||
trace.info("Подготовка завершена", status="completed", attrs={"items_count": 2})
|
||
```
|
||
|
||
### Пример: дочерний контекст
|
||
```python
|
||
with trace.open_context(
|
||
alias="subtask-1",
|
||
type="subtask",
|
||
parent_id=trace_id,
|
||
attrs={"segment": "phase-a"},
|
||
) as child_trace_id:
|
||
trace.step("execute")
|
||
trace.info("Подзадача запущена", status="started")
|
||
trace.info("Подзадача завершена", status="completed", attrs={"duration_ms": 120})
|
||
|
||
# Здесь снова активен родительский контекст
|
||
trace.step("finish")
|
||
trace.info("Обработка завершена", status="completed")
|
||
```
|
||
|
||
### Хранение в MySQL
|
||
Для MySQL предусмотрен `MySqlTraceTransport`. Он пишет две сущности в отдельные таблицы:
|
||
- `trace_contexts`
|
||
- `trace_messages`
|
||
|
||
Это позволяет:
|
||
- отдельно хранить структуру процесса;
|
||
- отдельно хранить историю шагов и сообщений;
|
||
- строить отчёты и трассировку без завязки на `logging`.
|
||
|
||
## Установка
|
||
``pip install git+https://git.lesha.spb.ru/alex/config_manager.git``
|
||
|
||
## Контакты
|
||
- **e-mail**: lesha.spb@gmail.com
|
||
- **telegram**: https://t.me/lesha_spb
|