Правки

This commit is contained in:
2026-03-07 17:21:27 +03:00
parent e055a6a09e
commit 93e9f4c37e
7 changed files with 347 additions and 168 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
__pycache__/ __pycache__/
*.pyc

425
README.md
View File

@@ -1,214 +1,319 @@
# PLBA # PLBA
`PLBA` (`Platform Runtime for Business Applications`) - runtime для бизнес-приложений. ## 1. Назначение платформы
PLBA (`Platform Runtime for Business Applications`) - это runtime-слой для бизнес-приложений, который забирает на себя инфраструктурную часть исполнения. Платформа стандартизирует запуск и остановку рабочих процессов, контроль состояния приложения и эксплуатационные сервисы вокруг них. За счет этого прикладной код концентрируется на бизнес-логике, а не на lifecycle, диагностике и служебных механизмах. Базовая модель использования строится вокруг `ApplicationModule`, `Worker` и прикладной `Routine`, где каждый уровень отвечает за свою часть ответственности. В результате приложение получается предсказуемым в эксплуатации, проще в сопровождении и масштабировании.
Платформа берет на себя инфраструктурные обязанности: ## 2. Концепция использования
- lifecycle приложения `ApplicationModule` - точка сборки приложения: здесь создаются зависимости, собираются рутины, создаются воркеры и регистрируются дополнительные health-контрибьюторы.
- запуск и остановку воркеров
- health/status
- tracing
- logging
- control plane
- загрузку конфигурации
Бизнес-приложение на базе `plba` собирается вокруг трех уровней: `Worker` - основной runtime-контракт платформы: он управляет исполнением (потоки, цикл, стратегия остановки), возвращает `health()` и `status()`, а также определяет критичность компонента для общего health.
- `ApplicationModule` собирает приложение и регистрирует воркеры
- `Worker` управляет исполнением и lifecycle
- `Routine` реализует бизнес-функцию
`Routine` не является контрактом `plba`. Это рекомендуемый архитектурный паттерн для прикладного кода. `Routine` - прикладной паттерн (не обязательный контракт платформы): класс или набор классов, где сосредоточена бизнес-логика, которую воркер регулярно или однократно запускает.
Правила построения приложений на платформе собраны отдельно в [application_guidelines.md](/Users/alex/Dev_projects_v2/apps/plba/requirements/application_guidelines.md). Вспомогательные сервисы платформы дополняют core-модель:
- `tracing` (`TraceService`, транспорты) - операционная трассировка контекстов и сообщений.
- `logging` (`LogManager`) - применение конфигурации логирования из runtime-конфига.
- `health` (`HealthRegistry`) - агрегирование здоровья воркеров и дополнительных компонентов.
- `workflow` (`WorkflowEngine` и persistence-слой) - исполнение шагов бизнес-процесса с переходами и фиксацией состояния.
- `control plane` (`ControlPlaneService`, `HttpControlChannel`) - внешние health/action endpoints.
- `queue` (`InMemoryTaskQueue`) - локальный in-memory буфер как утилита прикладного уровня.
## Installation ## 3. Архитектура
```mermaid
classDiagram
class ApplicationModule {
<<abstract>>
+name: str
+register(registry)
}
class ModuleRegistry {
+add_worker(worker)
+add_health_contributor(contributor)
+register_module(name)
}
class RuntimeManager {
+register_module(module)
+add_config_file(path)
+start()
+stop(timeout, force)
+status()
+current_health()
}
class WorkerSupervisor {
+register(worker)
+start()
+stop(timeout, force)
+statuses()
+healths()
}
class Worker {
<<abstract>>
+name: str
+critical: bool
+start()
+stop(force)
+health()
+status()
}
class Routine {
<<pattern>>
+run()
}
Установка `plba` через `pip` из git-репозитория: class ConfigurationManager
class LogManager
class HealthRegistry
class TraceService
class ControlPlaneService
class WorkflowRuntimeFactory
class WorkflowEngine
class WorkflowPersistence
class WorkflowRepository
class CheckpointRepository
class InMemoryTaskQueue
class MySqlTraceTransport
```bash ApplicationModule --> ModuleRegistry : register(...)
pip install "plba @ git+https://git.lesha.spb.ru/alex/plba.git" RuntimeManager --> ApplicationModule : register_module(...)
RuntimeManager --> ModuleRegistry
RuntimeManager --> ConfigurationManager
RuntimeManager --> LogManager
RuntimeManager --> HealthRegistry
RuntimeManager --> TraceService
RuntimeManager --> WorkerSupervisor
RuntimeManager --> ControlPlaneService
WorkerSupervisor --> Worker
Worker --> Routine : invokes
WorkflowRuntimeFactory --> WorkflowEngine : create_engine(...)
WorkflowEngine --> WorkflowPersistence
WorkflowPersistence --> WorkflowRepository
WorkflowPersistence --> CheckpointRepository
TraceService --> MySqlTraceTransport
``` ```
## Runtime model ## 4. Описание компонентов
1. приложение объявляет `ApplicationModule` ### 4.1 Core модули
2. модуль регистрирует один или несколько `Worker`
3. `RuntimeManager` запускает все воркеры
4. каждый `Worker` запускает свою бизнес-активность
5. runtime агрегирует health и status
6. runtime останавливает воркеры graceful или forcefully
## Main contracts #### ApplicationModule
- Назначение: композиция прикладного приложения и регистрация runtime-компонентов.
- Реализация: контракт `app_runtime.contracts.application.ApplicationModule`, регистрация через `app_runtime.core.registration.ModuleRegistry`.
- Как работает / API / вызовы / таблицы:
- API: `name`, `register(registry)`.
- Типичные вызовы: `registry.add_worker(worker)`, `registry.add_health_contributor(contributor)`.
- `RuntimeManager.register_module()` вызывает `module.register(...)` и добавляет имя модуля в снимок runtime.
- В БД напрямую не пишет.
- Типовая схема использования:
- Создать инфраструктурные и доменные сервисы.
- Создать `Routine`.
- Создать `Worker` с зависимостью на рутину.
- Зарегистрировать воркер и опциональные health contributors.
### `ApplicationModule` #### Worker
- Назначение: runtime-исполнение бизнес-активности, управление lifecycle, интерпретация health/status.
- Реализация: контракт `app_runtime.contracts.worker.Worker`, оркестрация через `app_runtime.workers.supervisor.WorkerSupervisor`.
- Как работает / API / вызовы / таблицы:
- API: `start()`, `stop(force=False)`, `health() -> WorkerHealth`, `status() -> WorkerStatus`, свойства `name`, `critical`.
- `WorkerSupervisor.start()` запускает все воркеры, `stop()` останавливает и ждет state=`stopped`.
- `RuntimeManager` получает агрегированные `statuses()`/`healths()` у супервизора.
- В БД напрямую не пишет (если прикладной worker сам не реализует persistence).
- Типовая схема использования:
- Внутри `start()` поднимать поток/пул или запускать одноразовую задачу.
- В цикле вызывать `routine.run()`.
- Ошибки бизнес-логики транслировать в `health()`/`status()`.
Описывает, из чего состоит приложение. #### Routine
- Назначение: изоляция прикладной бизнес-логики от runtime-обвязки.
- Реализация: прикладной класс (паттерн), обычно с методом `run()`; платформой не навязывается отдельный интерфейс.
- Как работает / API / вызовы / таблицы:
- Типичный API: `run()` + дополнительные domain-методы.
- Вызывается воркером в выбранной стратегии выполнения (single-run/loop, 1..N потоков).
- Может вызывать сервисы интеграций, workflow, очереди, доменные репозитории.
- Запись в таблицы определяется прикладными сервисами, а не самой платформой.
- Типовая схема использования:
- Оставлять рутину тонким оркестратором бизнес-действий.
- Сложную логику выносить в отдельные доменные сервисы.
Ответственность: ### 4.2 Service модули
- дать имя модуля
- зарегистрировать воркеры
- зарегистрировать health contributors при необходимости
- собрать прикладные зависимости
### `Worker` #### Configuration
- Назначение: централизованная загрузка и слияние runtime-конфигурации.
- Реализация: `ConfigurationManager`, `FileConfigProvider`, `ConfigFileLoader`.
- Как работает / API / вызовы / таблицы:
- API: `add_provider()`, `load()`, `reload()`, `get()`, `section()`.
- `RuntimeManager.start()` вызывает `configuration.load()`.
- Поддерживается YAML/JSON через `ConfigFileLoader.parse()`.
- В БД не пишет.
- Типовая схема использования:
- В bootstrap добавить файл: `runtime.add_config_file("config.yml")`.
- Читать секции из `runtime.configuration.section("...")` в прикладных фабриках.
Главный runtime-контракт платформы. #### Logging
- Назначение: применение и восстановление валидной конфигурации логирования.
- Реализация: `LogManager`.
- Как работает / API / вызовы / таблицы:
- API: `apply_config(config)`.
- `RuntimeManager.start()` вызывает `logs.apply_config(config)`.
- Если секция `log` некорректна, сервис пытается восстановить предыдущую валидную конфигурацию.
- В БД не пишет.
- Типовая схема использования:
- Передать `log` секцию в конфиг и запускать runtime стандартно через `start()`.
Контракт: #### Health
- `name` - Назначение: сводный health приложения по воркерам и дополнительным контрибьюторам.
- `critical` - Реализация: `HealthRegistry` + контрибьюторы по контракту `HealthContributor`.
- `start()` - Как работает / API / вызовы / таблицы:
- `stop(force=False)` - API: `register()`, `snapshot(worker_healths)`, `payload(state, worker_healths)`.
- `health()` - Агрегация: `unhealthy` при критичном `unhealthy`, `degraded` при любой деградации, иначе `ok`.
- `status()` - `RuntimeManager.current_health()` использует `HealthRegistry.payload(...)`.
- В БД не пишет.
`Worker` отвечает только за runtime-поведение: - Типовая схема использования:
- как запускается бизнес-активность - Зарегистрировать contributor в `ApplicationModule.register(...)`.
- в одном потоке или нескольких - Отдавать health наружу через control plane `/health`.
- single-run или loop
- graceful shutdown
- интерпретацию ошибок в `health/status`
### `Routine`
Рекомендуемый application-level паттерн.
`Routine` описывает бизнес-функцию:
- что читать
- какие сервисы вызывать
- какие бизнес-решения принимать
- что сохранять или отправлять наружу
Обычно воркер получает одну routine через конструктор и вызывает ее в `start()` или во внутренних helper-методах.
## Minimal example
#### Tracing
- Назначение: фиксация контекстов выполнения и событий по шагам операций.
- Реализация: `TraceService`, `TraceContextStore`, `NoOpTraceTransport`, `MySqlTraceTransport`.
- Как работает / API / вызовы / таблицы:
- API: `create_context()`, `open_context()`, `step()`, `info()/warning()/error()/exception()`, `new_root()`, `child_of()`, `attach()`, `resume()`.
- `TraceService` пишет записи через transport; ошибки транспорта изолируются в логах.
- При `MySqlTraceTransport` записи идут в таблицы:
- `trace_contexts`
- `trace_messages`
- Эталонная схема и migration для MySQL: `requirements/sql/trace_mysql_schema.sql`.
- Trace Context:
- Что это: запись о начале логического контекста выполнения (операция, воркер, подоперация), к которой потом привязываются trace-сообщения.
- Для чего нужен: позволяет собрать дерево исполнения и связать все сообщения конкретной бизнес-операции.
- Атрибуты контекста (`TraceContextRecord`): `trace_id`, `alias`, `parent_id`, `type`, `event_time`, `attrs`.
- Иерархия: `parent_id` указывает на родительский контекст; так строится цепочка root -> child.
- Таблица: `trace_contexts`.
- Как объявляется в коде:
```python ```python
from threading import Event, Lock, Thread with traces.open_context(alias="orders-worker", kind="worker", attrs={"routine": "orders"}) as trace_id:
from time import sleep ...
```
```python
root = traces.new_root("orders.sync")
child = traces.child_of(root, "orders.process_batch")
```
- Trace Message:
- Что это: событие внутри активного context (статус шага, предупреждение, ошибка, служебная информация).
- Роль `step`: текущая стадия операции (`parse`, `validate`, `persist` и т.д.), которую выставляют через `traces.step("...")`.
- Уровни сообщений: `INFO`, `WARNING`, `ERROR`; `exception(...)` пишет сообщение уровня `ERROR`.
- Атрибуты сообщения (`TraceLogMessage`): `trace_id`, `step`, `status`, `message`, `level`, `event_time`, `attrs`.
- Связь с context: каждое сообщение обязательно связано с текущим активным `trace_id`; без активного контекста `TraceService` выбрасывает ошибку.
- Таблица: `trace_messages`.
- Как правильно применять в проекте:
- Открывать один root-context на единицу бизнес-работы (например, обработка одного сообщения/заказа).
- Перед важными этапами явно менять `step`.
- `info` использовать для нормального прогресса, `warning` для recoverable ситуаций (retry/fallback), `error`/`exception` для сбоев.
- В `attrs` класть диагностические ключи (`entity_id`, `attempt`, `integration`, `duration_ms`), чтобы ускорить расследование инцидентов.
- Типовая схема использования:
- В воркере открыть контекст (`open_context`) и на каждом шаге рутины писать `step()/info()`.
from plba import ( #### Workflow
ApplicationModule, - Назначение: исполнение step-based workflow с переходами и сохранением прогресса.
Worker, - Реализация: `WorkflowRuntimeFactory`, `WorkflowEngine`, `WorkflowPersistence`, `WorkflowRepository`, `CheckpointRepository`.
WorkerHealth, - Как работает / API / вызовы / таблицы:
WorkerStatus, - API верхнего уровня: `WorkflowRuntimeFactory.create_engine(workflow)`, `WorkflowEngine.run(context)`.
create_runtime, - `WorkflowEngine` по шагам вызывает `WorkflowStep.run(context)`, применяет `TransitionResolver`, вызывает hooks.
) - `WorkflowPersistence` пишет состояние run/step/checkpoint через репозитории.
- При настроенном подключении к БД записи идут в таблицы:
- `workflow_runs`
- `workflow_steps`
- `workflow_checkpoints`
- Без подключения работает in-memory fallback репозиториев.
- Типовая схема использования:
- Описать `WorkflowDefinition` (узлы и transitions).
- Создать engine через фабрику.
- Запускать из рутины или воркера: `engine.run(context)`.
#### Control Plane
- Назначение: внешний канал управления runtime и чтения health/status.
- Реализация: `ControlPlaneService`, `HttpControlChannel`, `HttpControlAppFactory`.
- Как работает / API / вызовы / таблицы:
- API: `register_channel()`, `start(runtime)`, `stop()`, `snapshot(runtime)`.
- HTTP-канал поднимает endpoint'ы:
- `GET /health`
- `GET|POST /actions/{action}` (`start`, `stop`, `status`)
- Колбэки action'ов вызывают async-методы `RuntimeManager`.
- В БД не пишет.
- Типовая схема использования:
- При создании runtime включить `enable_http_control=True`.
- Использовать `/health` для readiness/liveness и `/actions/*` для операционного контроля.
#### Queue
- Назначение: простой in-memory буфер задач/сообщений внутри приложения.
- Реализация: `InMemoryTaskQueue[T]`.
- Как работает / API / вызовы / таблицы:
- API: `put(item)`, `get(timeout)`, `task_done()`, `qsize()`, `stats()`.
- Может использоваться между воркерами/сервисами как локальная очередь.
- В БД не пишет.
- Типовая схема использования:
- Producer в рутине кладет элементы через `put`.
- Consumer-воркер извлекает через `get(timeout)` и обрабатывает.
## 5. MVP бизнес-приложения
Минимальная конфигурация запуска:
1. Создать один `ApplicationModule`.
2. В модуле собрать одну `Routine` и один `Worker` (1 worker -> 1 routine).
3. Зарегистрировать воркер через `registry.add_worker(...)`.
4. Создать runtime: `create_runtime(module, config_path="config.yml")`.
5. Вызвать `runtime.start()`.
Минимальный пример:
```python
from plba import ApplicationModule, Worker, WorkerHealth, WorkerStatus, create_runtime
from app_runtime.core.registration import ModuleRegistry
class OrdersRoutine: class DemoRoutine:
def __init__(self, service) -> None:
self._service = service
def run(self) -> None: def run(self) -> None:
self._service.process_new_orders() print("business action")
class OrdersWorker(Worker): class DemoWorker(Worker):
def __init__(self, routine: OrdersRoutine, interval: float = 1.0) -> None: def __init__(self, routine: DemoRoutine) -> None:
self._routine = routine self._routine = routine
self._interval = interval self._started = False
self._thread: Thread | None = None
self._stop_requested = Event()
self._lock = Lock()
self._in_flight = 0
self._failures = 0
@property @property
def name(self) -> str: def name(self) -> str:
return "orders-worker" return "demo-worker"
@property @property
def critical(self) -> bool: def critical(self) -> bool:
return True return True
def start(self) -> None: def start(self) -> None:
if self._thread and self._thread.is_alive(): self._started = True
return self._routine.run()
self._stop_requested.clear()
self._thread = Thread(target=self._run_loop, daemon=True)
self._thread.start()
def stop(self, force: bool = False) -> None: def stop(self, force: bool = False) -> None:
del force del force
self._stop_requested.set() self._started = False
def health(self) -> WorkerHealth: def health(self) -> WorkerHealth:
if self._failures > 0: return WorkerHealth(name=self.name, status="ok", critical=self.critical)
return WorkerHealth(self.name, "degraded", self.critical)
return WorkerHealth(self.name, "ok", self.critical)
def status(self) -> WorkerStatus: def status(self) -> WorkerStatus:
alive = self._thread is not None and self._thread.is_alive() return WorkerStatus(name=self.name, state="idle" if self._started else "stopped")
state = "busy" if self._in_flight else "idle"
if not alive:
state = "stopped"
elif self._stop_requested.is_set():
state = "stopping"
return WorkerStatus(name=self.name, state=state, in_flight=self._in_flight)
def _run_loop(self) -> None:
while not self._stop_requested.is_set():
with self._lock:
self._in_flight += 1
try:
self._routine.run()
except Exception:
self._failures += 1
finally:
with self._lock:
self._in_flight -= 1
sleep(self._interval)
class OrdersModule(ApplicationModule): class DemoModule(ApplicationModule):
@property @property
def name(self) -> str: def name(self) -> str:
return "orders" return "demo"
def register(self, registry) -> None: def register(self, registry: ModuleRegistry) -> None:
service = OrderService() registry.add_worker(DemoWorker(DemoRoutine()))
routine = OrdersRoutine(service)
registry.add_worker(OrdersWorker(routine))
runtime = create_runtime(OrdersModule(), config_path="config.yml") runtime = create_runtime(DemoModule(), config_path="config.yml")
runtime.start() runtime.start()
``` ```
## Health and status Для production-сценария после MVP обычно добавляют `tracing`, `health contributors`, `workflow` и HTTP control plane, но базовый запуск не требует этих расширений.
Практика такая:
- `Routine` выполняет бизнес-работу
- `Worker` ловит ее ошибки
- `Worker` интерпретирует outcome в `health()` и `status()`
То есть routine не выставляет health напрямую.
## In-memory queue
`InMemoryTaskQueue` остается в платформе как простой in-memory utility.
Это не базовый платформенный контракт и не обязательный паттерн архитектуры.
Ее можно использовать в прикладном коде как локальный буфер между компонентами, если это действительно помогает.
```python
from plba import InMemoryTaskQueue
queue = InMemoryTaskQueue[str]()
queue.put("payload")
item = queue.get(timeout=0.1)
```
## Public API
Основные публичные сущности:
- `ApplicationModule`
- `Worker`
- `WorkerHealth`
- `WorkerStatus`
- `RuntimeManager`
- `WorkerSupervisor`
- `TraceService`
- `HealthRegistry`
- `InMemoryTaskQueue`
- `create_runtime(...)`

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "plba" name = "plba"
version = "0.2.1" version = "0.2.2"
description = "Platform runtime for business applications" description = "Platform runtime for business applications"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"

View File

@@ -0,0 +1,37 @@
-- MySQL schema for PLBA tracing.
-- Guarantees compatibility with TraceService levels: INFO, WARNING, ERROR.
CREATE TABLE IF NOT EXISTS trace_contexts (
trace_id VARCHAR(64) PRIMARY KEY,
parent_id VARCHAR(64) NULL,
alias VARCHAR(255) NOT NULL,
type VARCHAR(64) NULL,
event_time DATETIME(6) NOT NULL,
attrs_json JSON NOT NULL,
KEY idx_trace_contexts_parent_id (parent_id),
CONSTRAINT fk_trace_contexts_parent
FOREIGN KEY (parent_id) REFERENCES trace_contexts(trace_id)
ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS trace_messages (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
trace_id VARCHAR(64) NOT NULL,
event_time DATETIME(6) NOT NULL,
step VARCHAR(128) NOT NULL,
status VARCHAR(64) NOT NULL,
level ENUM('INFO', 'WARNING', 'ERROR') NOT NULL,
message TEXT NOT NULL,
attrs_json JSON NOT NULL,
KEY idx_trace_messages_trace_id (trace_id),
KEY idx_trace_messages_event_time (event_time),
KEY idx_trace_messages_level (level),
CONSTRAINT fk_trace_messages_context
FOREIGN KEY (trace_id) REFERENCES trace_contexts(trace_id)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Migration for existing installations.
-- If your `trace_messages.level` does not include WARNING, run this ALTER.
ALTER TABLE trace_messages
MODIFY COLUMN level ENUM('INFO', 'WARNING', 'ERROR') NOT NULL;

View File

@@ -3,7 +3,10 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Protocol from typing import Any, Literal, Protocol
TraceLevel = Literal["INFO", "WARNING", "ERROR"]
def utc_now() -> datetime: def utc_now() -> datetime:
@@ -44,7 +47,7 @@ class TraceLogMessage:
step: str step: str
status: str status: str
message: str message: str
level: str level: TraceLevel
event_time: datetime = field(default_factory=utc_now) event_time: datetime = field(default_factory=utc_now)
attrs: dict[str, Any] = field(default_factory=dict) attrs: dict[str, Any] = field(default_factory=dict)

View File

@@ -9,6 +9,7 @@ from app_runtime.contracts.trace import (
TraceContext, TraceContext,
TraceContextFactory, TraceContextFactory,
TraceContextRecord, TraceContextRecord,
TraceLevel,
TraceLogMessage, TraceLogMessage,
TraceTransport, TraceTransport,
utc_now, utc_now,
@@ -86,13 +87,13 @@ class TraceService(TraceContextFactory):
def step(self, name: str) -> None: def step(self, name: str) -> None:
self.store.set_step(str(name or "")) self.store.set_step(str(name or ""))
def info(self, message: str, *, status: str, attrs: dict[str, Any] | None = None) -> None: def info(self, message: str, *, status: str | None = None, attrs: dict[str, Any] | None = None) -> None:
self._write_message("INFO", message, status, attrs) self._write_message("INFO", message, status, attrs)
def warning(self, message: str, *, status: str, attrs: dict[str, Any] | None = None) -> None: def warning(self, message: str, *, status: str | None = None, attrs: dict[str, Any] | None = None) -> None:
self._write_message("WARNING", message, status, attrs) self._write_message("WARNING", message, status, attrs)
def error(self, message: str, *, status: str, attrs: dict[str, Any] | None = None) -> None: def error(self, message: str, *, status: str | None = None, attrs: dict[str, Any] | None = None) -> None:
self._write_message("ERROR", message, status, attrs) self._write_message("ERROR", message, status, attrs)
def exception(self, message: str, *, status: str = "failed", attrs: dict[str, Any] | None = None) -> None: def exception(self, message: str, *, status: str = "failed", attrs: dict[str, Any] | None = None) -> None:
@@ -142,9 +143,9 @@ class TraceService(TraceContextFactory):
def _write_message( def _write_message(
self, self,
level: str, level: TraceLevel,
message: str, message: str,
status: str, status: str | None,
attrs: dict[str, Any] | None, attrs: dict[str, Any] | None,
) -> None: ) -> None:
active = self.store.current() active = self.store.current()

View File

@@ -259,6 +259,38 @@ def test_trace_service_writes_contexts_and_messages() -> None:
assert transport.messages[0].step == "parse" assert transport.messages[0].step == "parse"
def test_trace_service_supports_warning_and_error_levels() -> None:
from app_runtime.tracing.service import TraceService
transport = RecordingTransport()
manager = TraceService(transport=transport)
with manager.open_context(alias="worker", kind="worker", attrs={"routine": "incoming"}):
manager.step("validate")
manager.warning("validation warning", status="degraded", attrs={"attempt": 1})
manager.error("integration failed", status="failed", attrs={"integration": "crm"})
manager.exception("caught exception", attrs={"exception_type": "RuntimeError"})
levels = [message.level for message in transport.messages]
assert levels == ["WARNING", "ERROR", "ERROR"]
def test_trace_service_allows_messages_without_status() -> None:
from app_runtime.tracing.service import TraceService
transport = RecordingTransport()
manager = TraceService(transport=transport)
with manager.open_context(alias="worker", kind="worker"):
manager.step("optional-status")
manager.info("info without status")
manager.warning("warning without status")
manager.error("error without status")
assert [message.status for message in transport.messages] == ["", "", ""]
assert all(message.trace_id == transport.contexts[0].trace_id for message in transport.messages)
def test_http_control_channel_exposes_health_and_actions() -> None: def test_http_control_channel_exposes_health_and_actions() -> None:
from app_runtime.control.base import ControlActionSet from app_runtime.control.base import ControlActionSet
from app_runtime.control.http_channel import HttpControlChannel from app_runtime.control.http_channel import HttpControlChannel