本番にデバッガは差し込めない
デバッガは操作を内側から見せてくれる — けれど本番には差し込めないし、動いている実行にはつかまるところがない。
開発中なら、デバッガはどのステップでも実行を凍らせる。引数も、途中の値も、この先へ渡っていくものも見える。本番ではそうはいかない。一つのリクエストのためにプロセスを止めるわけにはいかないし、まれな入力は再現しにくい。走っている実行をそのまま眺めることもできない。呼び出しの連なりはコールスタックの中に、その途中の値はローカル変数の中に生きていて、呼び出しが返った瞬間にすべて消える。ログに残るのはあらかじめ選んでおいた値だけ。技術的なトレースは呼び出しとその所要時間を見るが、そのどれが業務上のステップで、あいだのどの値が大事なのかは知らない。原因は間接的な手がかりから推し量るしかなくなる。
実行中のデータは存在している — デバッガがそれを証明している。足りないのは別のものだ。実行そのものと各ステップに、あらかじめ分かっている境界があること。そこへ online の観測を掛けられること — プロセスを止めずに。
{ validated_items: 3 } · すべての値が見えるどのステップでも一時停止 — state がすべて見える
止められない — 途中が見えない
実行の境界はアーキテクチャがすでに決めている
実行を一つのまとまりとして観測するには、それに正確な境界が要る — そして各ステップにも。
AOA では操作とは、Params → Result という外側の境界を持つ Action だ。それを走らせるのは機械 — Action を実行する AOA のエンジンであり、実行がどこで始まりどこで終わるかをあらかじめ知っている。内側では道筋が Aspect に分かれ、それぞれが自分の途中状態 state を持つ。アスペクトは直前の state のスナップショットを受け取り、自分の新しい 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 と出ていく state、所要時間、そしてエラーが見える。機械はこれらの地点をすべて知っていて、lifecycle events を自動で発する。プラグインはそれを外から受け取り、その一回の実行の online な投影を組み立てる — 実際の入口、出口、エラー、時間を伴ったステップの木だ。開発でも、テストでも、本番でも同じように。業務メソッドの中にログ呼び出しも、手書きのタイマーも、トレース用のコードも要らない。
これは差し込まれたデバッガでもなければ、プロセスの停止でもない。安全な online の投影だ。観測者に見えるのは契約が許したものだけ。機微なフィールドは opaque のままで、プラグイン自身の障害は切り離され、操作の結果を変えない。それがまさに Action X-Ray である。
- start · Params
- validate · state → state′ · 0.4 ms
- reserve · state′ → state″ · 12 msopaque
- charge · error · 3 ms
- finish · Result · 15 ms
プラグインは機械の生成時に渡す — 業務メソッドはそのまま:
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 を、同じ機械と同じ境界に通すだけだ。変わるのは外の世界だけ — 本番の Context、Resource、Gateway の代わりにテスト用の実装を取る。ロール、パイプライン、checker、そして Result の組み立ては本物のまま。操作は丸ごと走らせることも、境界の一つ — Aspect、summary、compensator — で止めることもできる。業務コードに専用のフックを仕込むことなく。
テストが確かめるのは、本番で手に入るのと同じ投影だ。最後の 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 の外側の境界と、その Aspect の内側の境界を定め、途中の state を契約に変える。実行のあいだ、その同じ境界が lifecycle events の座標系になる — かつてはデバッガの下でしか、あるいは手で計器を仕込んだあとでしか得られなかった可視性が、いまはどの実行についても online で手に入る。本番も含めて、操作の中に基盤のコードを置くことなく。
