OXOエージェントの開発とデバッグに役立つヒントとコツ
OXOエージェントの開発とデバッグをもっと楽にするためのヒントとコツを紹介します。
本記事は、OXOエージェントの書き方を解説した前回の記事の続編です。以下のセクションでは、エージェントの作成とデバッグを簡単に進めるためのヒントとコツを紹介します。
OXOのエンジンとエージェントのログ
OXOのエンジンは--debugフラグを受け付けます。このフラグを指定すると、OXO自身のロガーとあらゆるサードパーティパッケージのロガーがすべてデバッグレベルに設定されます。これにより、内部で何が起きているかについて大量の情報が表示され、その中には非常に役立つものもあります。たとえば、ネットワークは正常に作成されたか、エージェントの設定は正しいか、システムエージェントは正常に動作しているか、といった問いに答えることができます。
特定のエージェント、または一部のエージェントだけを調べたい場合もあるでしょう。その場合は、--follow agent/<organisation>/<agent_name>フラグを使うと、特定のエージェントのログメッセージだけを表示できます。
フォローするエージェントの数に制限はなく、読みやすいようにそれぞれ異なる色で表示されます。
両方のフラグを使った例を示します。
oxo --debug scan run --agent agent/ostorlab/subfinder --follow agent/ostorlab/subfinder --agent agent/ostorlab/dnsx --follow agent/ostorlab/dnsx domain-name ostorlab.co

実際にはDockerコンテナをデバッグしている
エージェントは、連携してアセットをスキャンするDockerサービスです。そのため、調査にはDockerコマンドを活用できます。
docker service ls:実行中のサービスを一覧表示します。どのエージェントが起動していて、どのエージェントが停止しているかを確認するのに便利です。
docker service logs <service>:エージェントのDockerサービスのログメッセージを表示します。
docker service inspect <service>:サービスの状態や設定などのメタデータを表示します。
docker stats <container>:CPU%、メモリ使用量、I/O使用量など、コンテナの状態に関する情報を表示します。
調査の進め方をより細かく制御したい場合は、いつでも次のコマンドでエージェント内のシェルに入れることを覚えておいてください。
docker exec -it <container> bash/sh
明確なエラーメッセージが出ないものの、何かがおかしいと分かっているケースもあります。メッセージが送出されていない、あるいは処理されていないのかもしれません。送出されてはいるものの、誤ったキューに送られているのかもしれません。同じメッセージを二重に処理しないよう一意性チェックを追加したのに、何度も繰り返し処理されているのかもしれません。あるいは、まったく別の理由かもしれません。
RabbitMQ
エージェントは、メッセージの送受信にメッセージブローカーとしてRabbitMQを使用しています。 できることの一つは、RabbitMQコンテナ内のシェルに入り、RabbitMQのCLIコマンドを使って次の操作を行うことです。
列を指定してキューの一覧を取得します。列は必要に応じて追加・削除できます。
rabbitmqadmin list queues name node messages messages_ready messages_unacknowledged message_stats.publish_details.rate message_stats.deliver_get_details.rate message_stats.publish

バインディングの一覧を表示します。バインディングとは、メッセージをそれぞれのキューへルーティングするためのルールです。
rabbitmqadmin list bindings

キューがタイムアウト内に応答するかをテストし、応答しなかったキューを一覧表示します。
rabbitmqctl list_unresponsive_queues
RabbitMQサービスに関しては、別のアプローチもあります。--mq-exposed-portsフラグを使い、公開するRabbitMQ管理ユーザーインターフェースのポートを指定する方法です。たとえば次のとおりです。
oxo scan --mq-exposed-ports 15672:15672 run --agent ...
その後、127.0.0.1:15672でインターフェースにアクセスできます。デフォルトのユーザー名とパスワードはどちらも「guest」です。

前のセクションで見たコマンドラインと同じこと、たとえばキューやエクスチェンジの一覧表示、配信済み・処理済みメッセージの数とレートの確認ができるほか、サービスの状態を全体的に把握することもできます。

Redis
Redisには管理ユーザーインターフェースがありませんが、直感的に扱えるため、実際のところ必要ありません。基本的なコマンドで、次のような問いに答えられます。
どのキーが永続化されているか
KEYS <regex>
*を使うとすべてのキーを表示でき、任意の正規表現を使えば出力を絞り込めます。
キーは存在するか
文字列、バイト列、セットの場合は次を使用します。
EXISTS <key>
ハッシュマップの場合は次を使用します。
HEXISTS key fields
特定のキーの値の型は何か
TYPE <key>
特定のキーの値は何か 型に応じて使い分けます。 文字列/バイト列の値の場合
GET <key>
ハッシュマップの値の場合
HGET <key> <field>
または、すべての値を一覧表示する場合
HGETALL <key>
セットのすべての値の場合
SMEMBERS <key>
トレーシング
分散トレーシングは、リクエストがあるシステムから別のシステムへどのように流れていくかを俯瞰的に把握するための手法です。メッセージがエージェントからエージェントへ移動する際のトラブルシューティングに役立ちます。
OXOには、メッセージのprocessメソッドとemitメソッドの両方について、計装があらかじめ実装されています。言い換えれば、メッセージがいつ、どのセレクターで処理されたか、そのデータの内容、さらに出力や送出された結果の値を確認できます。
エージェント間のメッセージのトレーシングを有効にするには、おなじみのoxo scan runコマンドを--tracingフラグ付きで実行します。
これによりjaegerが公開されます。jaegerは、分散サービス間のトランザクションを監視・トレースするためのユーザーインターフェースです。簡単に言えば、マイクロサービス間、ここではエージェント間で起きるイベントの連鎖を可視化する役割を果たします。
Agent1で処理されるメッセージを追い、その処理結果がAgent2へ送出され、さらにその先へと続く様子をたどれます。時刻、所要時間、使用されたセレクター、メッセージの値など、さまざまな属性を確認できます。
結果を説明する前に、OpenTelemetryの主要な概念について簡単に触れておきます。
1. トレース:リクエスト(ここではメッセージ)がマルチサービスアーキテクチャ間(ここではエージェント間)を伝播する際にたどる経路。
2. スパン:リクエストによって、またはリクエストに対して実行される、追跡対象の操作。ここでは、たとえば処理中のメッセージが該当します。
3. コンテキスト:サービス間を移動するスパンの追跡に役立つメタデータ。基本的にはtraceIDとspanIDのことで、エージェント間のメッセージの流れを結び付けるのに役立ちます。
そこで、次のコマンドを実行すると
oxo scan --tracing run --agent agent/ostorlab/subfinder --agent agent/ostorlab/dnsx domain-name ostorlab.co
SubfinderエージェントとDNSxエージェントがostorlab.coを対象に起動し、ブラウザーでhttp://127.0.0.1:16686が開いてjaegerのユーザーインターフェースが表示されます。
ほかのエージェントはjaegerサービスの後に起動するため、最初は何も表示されなくても正常です。
サービスのドロップダウンには2つのエージェントが表示されます。Subfinderエージェントを例に取ると、
最初のドメインメッセージostorlab.coがルートスパンとして表示され、その下に子スパンが並び、何が起きたかの経路を示しています。
最初のメッセージが処理され、docs.ostorlab.co、blog.ostorlab.coなど、それぞれ特定のサブドメインを含む10件のメッセージが送出されました。

それらのメッセージはそれぞれDNSxエージェントに受け取られて処理され、追加情報を含むDNSレコードがそれぞれ送出されました。

ユニットテスト
エージェント開発サイクルの初期段階では、ユニットテストを活用して、コードが実際に動作し目的を果たしていることを確認するのが望ましいでしょう。外部要因や、エージェントとシステムのほかの部分とのやり取りではなく、エージェントのコードそのものに起因するバグや奇妙な挙動を取り除くための効率的な方法です。
OXOは、プロセス全体をユニットテストでカバーするのに役立つpytestフィクスチャを提供しています。たとえば次のとおりです。
agent_run_mock:このフィクスチャは、エージェントサービスのヘルスチェック、RabbitMQサーバーの初期化と接続、メッセージの消費と送出といった、エージェントの低レベルなメソッドをパッチします。また、次の属性を持つAgentRunInstanceインスタンスを返します。
agent_run_mock.raw_messages: List
agent_run_mock.emitted_messages: List
したがって、次のようなチェックによって
assert len(agent_run_mock.emitted_messages) > 0
何らかのメッセージが送出されたことが分かります。たとえば、最初の要素には次のようにアクセスできます。
agent_run_mock.emitted_messages[0]
これは、次の属性を持つMessageクラスです。
message.selector
message.data
これらの属性は、たとえば正しいセレクターを使用したか、メッセージのデータが想定どおりかを確認するのに役立ちます。
agent_persist_mock:このフィクスチャは、エージェントのRedis永続化コンポーネントをパッチし、Redisを模倣した辞書を返します。キーはRedisのキーで、値はRedisに保存される型に従います。これは、たとえばpersist_mixinを継承して受信メッセージの一意性チェックを実装し、まったく同じメッセージを複数回処理しないようにしているエージェントをテストする場合や、エージェントのすべてのレプリカで使えるカウンターが必要な場合に便利です。
assert agent_persist_mock.get('<key>') is not None
assert agent_persist_mock.get('<global_counter>') == 42
まとめ
エージェントのデバッグ方法について、さまざまな側面を見てきました。基本的なコマンドを使ったログの表示、RabbitMQのキューの確認、Redisのデータが存在するかの検証などです。また、メッセージのトレースをそのライフサイクル全体にわたって追う方法や、提供されているpytestフィクスチャを活用して、エージェントを起動する前の段階でコードをテストする方法も紹介しました。