开发与调试 OXO Agent 的技巧与窍门
让开发与调试 OXO Agent 更加轻松的实用技巧与窍门。
本文是上一篇关于如何编写 OXO Agent 的文章的续篇。在以下各节中,我们将分享一些技巧与窍门,让编写和调试 Agent 变得简单。
OXO 引擎与 Agent 日志
OXO 引擎接受 --debug 标志,它会将所有日志记录器(包括 OXO 自身的以及任何第三方包的)设置为 debug 级别。这样会显示大量关于运行情况的信息,其中有些非常有用。例如,您可以借此回答以下问题:网络是否已成功创建、Agent 的配置是否正确、系统 Agent 是否健康等。
假设您只想排查某个特定 Agent 或一组 Agent,这时可以使用 --follow agent/<organisation>/<agent_name> 标志,只显示特定 Agent 的日志消息。
您可以根据需要跟踪任意多个 Agent;每个 Agent 都会以不同颜色标记,便于阅读。
以下是这两个标志的实际效果:
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 容器
由于 Agent 是协同扫描资产的 Docker 服务,因此可以借助 Docker 命令来辅助排查:
docker service ls:列出正在运行的服务。可用于查看哪些 Agent 已启动、哪些已停止。
docker service logs <service>:显示 Agent Docker 服务的日志消息。
docker service inspect <service>:显示服务状态、配置等元数据。
docker stats <container>:显示容器的状态信息,例如 CPU%、内存和 IO 使用情况。
请记住,您始终可以通过以下命令进入 Agent 内部的 shell:
docker exec -it <container> bash/sh,从而更灵活地掌控排查过程。
在某些情况下,您不会得到明确的错误消息,但知道某处出了问题:也许消息没有被发出或处理;也许消息已经发出,但发到了错误的队列;也许您添加了唯一性检查以避免重复处理同一条消息,但它仍然被一遍又一遍地处理;也可能是完全不同的原因。
RabbitMQ
Agent 使用 RabbitMQ 作为消息代理来发送和接收消息。 我们可以做的一件事是进入 RabbitMQ 容器的 shell,并使用 RabbitMQ 命令行命令来:
获取队列列表并指定显示的列,您可以根据需要增删这些列:
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>
追踪
分布式追踪是一种从全局视角观察请求如何在系统之间流转的方式。它可以帮助我们在消息从一个 Agent 流向另一个 Agent 时排查问题。
OXO 已经为处理消息和发出消息这两个方法实现了插桩。换言之,您可以看到消息何时被处理、使用了哪个选择器、其数据内容,以及其输出或发出结果的值
要启用 Agent 之间消息的追踪,我们可以在经典的 oxo scan run 命令中加上 --tracing 标志。
这将暴露 jaeger;它是一个用于监控和追踪分布式服务之间事务的用户界面。简单来说,它的作用是可视化微服务之间(在我们的场景中即 Agent 之间)发生的事件链。
您可以跟踪一条消息先由 Agent1 处理,然后处理结果被发送给 Agent2,依此类推。您可以看到各种属性,例如时间、持续时间、使用了哪个选择器、消息的值等。
在解释结果之前,我们先简要介绍一下 OpenTelemetry 的主要概念。
1. Trace(追踪):请求(在我们的场景中即消息)在多服务架构(在我们的场景中即 Agent 之间)中传播时所经过的路径。
2. Span(跨度):由请求执行或针对请求执行的一个被跟踪的操作。在我们的场景中,例如正在被处理的消息。
3. Context(上下文):在 Span 于服务之间移动时帮助跟踪它们的元数据。它本质上就是 traceID 和 spanID,帮助我们把消息在 Agent 之间的流转串联起来。
因此,运行:
oxo scan --tracing run --agent agent/ostorlab/subfinder --agent agent/ostorlab/dnsx domain-name ostorlab.co
Subfinder 和 DNSx Agent 将针对 ostorlab.co 启动,并且浏览器会打开 http://127.0.0.1:16686,显示 jaeger 用户界面。
其他 Agent 在 jaeger 服务之后才会启动,因此一开始看不到任何内容是正常的。
我们可以在服务下拉菜单中看到这 2 个 Agent。以 Subfinder Agent 为例:
我们看到第一条域名消息 ostorlab.co 作为根 Span,其下方是子 Span,描述了所发生事件的路径。
第一条消息被处理后,又发出了 10 条消息,每条消息都包含一个特定的子域名:docs.ostorlab.co、blog.ostorlab.co。

其中每条消息都被 DNSx Agent 接收并处理,各自发出带有额外信息的 DNS 记录。

单元测试
在 Agent 开发周期的初期,最好充分利用单元测试的能力,确保您的代码确实能够正常工作并实现其目的。这是一种高效的方法,可以排除直接来自 Agent 代码(而非来自外部来源,或来自您的 Agent 与系统其余部分的交互)的缺陷或异常行为。
OXO 提供了 pytest fixture,帮助在单元测试中覆盖整个流程,例如:
agent_run_mock:该 fixture 会 patch Agent 的底层方法,例如 Agent 服务健康检查、初始化 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:该 fixture 会 patch Agent 的 Redis 持久化组件,并返回一个模拟 Redis 的字典,其键即 Redis 键,其值遵循 Redis 中存储的类型。这很有用,例如,可用于测试继承 persist_mixin 的 Agent——这类 Agent 会对接收到的消息实现唯一性检查,以避免多次处理完全相同的消息;或者当您需要一个可供某个 Agent 所有副本共用的计数器时,也可以用它来测试。
assert agent_persist_mock.get('<key>') is not None
assert agent_persist_mock.get('<global_counter>') == 42
结论
我们了解了调试 Agent 的不同方面:从使用基本命令显示日志、检查 RabbitMQ 队列,到验证 Redis 数据是否存在。我们还了解了如何在消息的整个生命周期中跟踪其追踪信息,以及如何利用所提供的 pytest fixture,甚至在启动 Agent 之前就测试我们的代码。