В production отладчик не поставишь
Отладчик показывает операцию изнутри — но в production его не поставить, а живой запуск не за что ухватить.
Во время разработки отладчик замораживает исполнение на любом шаге: видно аргументы, промежуточные значения и то, что уйдёт дальше. В production так нельзя — процесс не остановить ради одного запроса, а редкий вход трудно воспроизвести. Да и на лету запуск не наблюдать: цепочка вызовов живёт в call stack, её промежуточные значения — в локальных переменных, и как только вызов вернул управление, всё это исчезает. Логи сохраняют лишь заранее выбранные значения; технический trace видит вызовы и их длительность, но не знает, какие из них — бизнес-шаги и какие промежуточные значения между ними важны. Причину приходится угадывать по косвенным признакам.
Данные во время исполнения есть — отладчик это доказывает. Не хватает другого: заранее известной границы у запуска и у каждого его шага, к которой можно прицепить online-наблюдение — не останавливая процесс.
{ validated_items: 3 } · видны все значенияпауза на любом шаге — виден весь state
остановить нельзя — середина не видна
Границы запуска уже заданы архитектурой
Чтобы наблюдать запуск как целое, у него должна быть точная граница — и у каждого его шага тоже.
В AOA операция — это Action с внешней границей Params → Result. Прогоняет её машина — движок AOA, который исполняет Action и заранее знает, где запуск начинается и заканчивается. Внутри путь разбит на Aspects, и у каждого — своё промежуточное состояние, state. Аспект получает snapshot предыдущего state и возвращает свой новый — тот полностью заменяет предыдущий, а не дополняет его. Поэтому всё нужное следующим шагам аспект переносит явно, и между шагами ничего не протекает само; @result_* проверяет выход сразу на границе. Эти границы — не инструмент наблюдения, а само устройство операции; но именно они дают заранее известные точки, где у каждого шага виден вход, выход и state.
Машина в любой момент знает рамку запуска и весь его контрактный путь: что вошло в каждый Aspect и какой state вышел. Наблюдаемый артефакт «один запуск» уже собран самой архитектурой — осталось выпустить его наружу.
{ }{ validated_items: 3 }{ validated_items: 3 }{ …, reservation_id: 'res_42' }{ …, reservation_id: 'res_42' }{ …, payment_id: 'pay_91' }Online debug: живой запуск виден снаружи
Раз границы известны, машина на каждой из них выпускает событие — а внешний наблюдатель собирает из них живую картину запуска.
На внешней границе видны фактические Params и Result, на каждой внутренней — входящий и исходящий state, длительность и ошибка. Машина знает все эти точки и автоматически выпускает lifecycle events; плагин получает их снаружи и строит online-проекцию конкретного запуска — дерево шагов с реальными входами, выходами, ошибками и временем. Одинаково в development, в тестах и в production — без вызовов логирования, ручных таймеров и tracing-кода внутри бизнес-методов.
Это не подключённый отладчик и не остановка процесса, а его безопасная online-проекция. Наблюдатель видит только разрешённое контрактом: чувствительные поля остаются opaque, а сбой самого плагина изолирован и не меняет результат операции. Ровно это и есть Action X-Ray.
- start · Params
- validate · state → state′ · 0.4 мс
- reserve · state′ → state″ · 12 мсopaque
- charge · error · 3 мс
- finish · Result · 15 мс
Плагин отдают машине при создании — бизнес-методы остаются нетронутыми:
plugin = OpenTelemetryPlugin( tracer_provider=tracer_provider, logger_provider=logger_provider, service_name="checkout-service",)# The plugin lives outside the Action; business methods stay untouched.machine = ActionProductMachine(plugins=[plugin])result = await machine.run(Context(), CreateOrderAction(), params)В тесте — тот же рентген
Тот же online-снимок доступен и в тесте: TestBench запускает ту же операцию через ту же машину.
TestBench не создаёт особую тестовую видимость — он прогоняет тот же Action через ту же машину и те же границы. Меняется только внешний мир: вместо production-контекста, ресурсов и gateway берутся их тестовые реализации, а роли, pipeline, checkers и сборка Result остаются настоящими. Операцию можно прогнать целиком или остановиться на одной границе — Aspect, summary или compensator — без специальных крючков в бизнес-коде.
Тест проверяет ту же проекцию, что доступна в production: не только финальный Result, но и любой промежуточный state. Меняется реальность вокруг операции, а не сама операция и не способ её наблюдать.
роли · pipeline · checkers · сборка Resultодна и та же online-проекцияТот же Action через TestBench — целиком или на одной границе:
inventory = AsyncMock(spec=InventoryGateway)inventory.reserve.return_value = "res_42"bench = TestBench().with_user( user_id="customer", roles=(CustomerRole,)).with_mocks({InventoryGateway: inventory})# The whole operation — same machine, same boundariesresult = await bench.run(CreateOrderAction(), params, rollup=False)# Or stop at a single boundary and inspect its statestate_after = await bench.run_aspect( CreateOrderAction(), "reserve_aspect", params=params, state={"validated_items": 3},)assert state_after["reservation_id"] == "res_42"Online debug становится свойством архитектуры
AOA задаёт внешнюю границу Action, внутренние границы его Aspects и превращает промежуточный state в контракт. Во время запуска эти же границы становятся системой координат для lifecycle events — и видимость, которая раньше возникала только под отладчиком или после ручной инструментализации, теперь доступна online для каждого исполнения, включая production, без инфраструктурного кода внутри операции.
