在生产环境中无法挂接调试器
调试器从内部展示一次操作——但在生产环境中你无法挂接它,而一次实时运行又没有可以抓住的着力点。
在开发过程中,调试器可以在任意步骤冻结执行:你能看到参数、中间值,以及将要传递下去的东西。在生产环境中这做不到——你无法为了单个请求而暂停进程,罕见的输入也难以复现。你也无法即时观察一次运行:调用链存在于调用栈中,它的中间值存在于局部变量里,而一旦某个调用返回,这一切就都消失了。日志只保留预先选定的值;技术性的追踪能看到调用及其耗时,却不知道其中哪些是业务步骤,也不知道它们之间哪些中间值重要。原因只能从间接线索去猜测。
数据在运行时是存在的——调试器已经证明了这一点。缺的是另一样东西:一个预先已知的边界,围绕整个运行、也围绕它的每一步,让你可以在其上挂接在线观察——而无需停止进程。
{ validated_items: 3 } · 所有值都可见可在任意步骤暂停——整个 state 都可见
无法停下——中间过程不可见
运行的边界已经由架构设定
要把一次运行作为整体来观察,它就需要一个精确的边界——它的每一步也同样需要。
在 AOA 中,一次操作就是一个带有外部边界 Params → Result 的 Action。它由机器运行——机器就是执行 Action、并预先知道运行在哪里开始、又在哪里结束的 AOA 引擎。在内部,路径被拆分为一个个 Aspect,每个 Aspect 都有自己的中间状态,即 state。一个 Aspect 接收上一个 state 的 snapshot,并返回它自己新的 state——这个新 state 会完全替换掉上一个 state,而不是在其之上追加。因此,后续步骤需要的一切,都由该 Aspect 显式地向前携带,步骤之间不会有任何东西自行泄漏;@result_* 就在边界处立即检查输出。这些边界并不是一种观察工具,而是操作本身的构造;但正是它们提供了那些预先已知的点,在这些点上,每一步的输入、输出和 state 都清晰可见。
机器在任何时刻都知道运行的边框及其完整的契约路径:什么进入了每个 Aspect,又输出了什么样的 state。可观察的“单次运行”产物已经由架构本身组装完成——剩下的只是把它释放出来。
{ }{ validated_items: 3 }{ validated_items: 3 }{ …, reservation_id: 'res_42' }{ …, reservation_id: 'res_42' }{ …, payment_id: 'pay_91' }在线调试:从外部看到的一次实时运行
既然边界都是已知的,机器就在每一个边界上发出一个事件——而外部观察者把这些事件组装成运行的实时画面。
在外部边界上,你能看到实际的 Params 和 Result;在每一个内部边界上,则能看到进入和离开的 state、耗时以及任何错误。机器知道所有这些点,并自动发出 lifecycle events;plugin 从外部接收它们,为某一次具体运行构建一个在线投影——一棵由各个步骤组成的树,带有真实的输入、输出、错误和耗时。在开发、测试和生产环境中都一样——无需在业务方法内部放置任何日志调用、手动计时器或追踪代码。
这不是一个挂接的调试器,也不是被停止的进程,而是它的一份安全的在线投影。观察者只能看到契约所允许的内容:敏感字段保持不透明,而 plugin 自身的失败会被隔离,不会改变操作的结果。这正是 Action X-Ray。
- start · Params
- validate · state → state′ · 0.4 ms
- reserve · state′ → state″ · 12 ms不透明
- charge · error · 3 ms
- finish · Result · 15 ms
plugin 在创建时被交给机器——业务方法保持不变:
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)在测试中——同一张 X 光片
同样的在线 snapshot 在测试中也能获得:TestBench 通过同一个机器运行同一个操作。
TestBench 并不创建一种仅用于测试的特殊可见性——它通过同一个机器和同一组边界运行同一个 Action。改变的只有外部世界:不再使用生产环境的 Context、Resources 和 gateway,而是使用它们的测试实现,而角色、流水线、checkers 以及 Result 的组装都保持真实。操作可以完整运行,也可以停在单个边界——某个 Aspect、summary 或 compensator——而无需在业务代码中埋设特殊钩子。
测试验证的正是生产环境中可用的那同一个投影:不仅是最终的 Result,还有任何中间状态。改变的是操作周围的现实,而不是操作本身,也不是观察它的方式。
角色 · 流水线 · checkers · Result 组装同一个在线投影同一个 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"在线调试成为架构的一种属性
AOA 设定 Action 的外部边界、其 Aspects 的内部边界,并把中间状态变成一份契约。在一次运行期间,正是这些边界成为 lifecycle events 的坐标系——而过去只有在调试器下、或经过手动埋点之后才会出现的可见性,如今对每一次执行都可在线获得,包括生产环境,且操作内部没有任何基础设施代码。
