Neutron, nuestro motor de IA, obtuvo un 96.75% en el benchmark CyberGym de UC Berkeley. Más información

Ingeniería

Ingeniería

Consejos y trucos para desarrollar y depurar agentes de OXO.

Consejos y trucos para que le resulte más fácil desarrollar y depurar agentes de OXO.

Este artículo es la continuación del artículo anterior sobre cómo escribir un agente de OXO. En las secciones siguientes compartiremos consejos y trucos para que escribir y depurar agentes sea una tarea sencilla.

Los registros del motor y de los agentes de OXO

El motor de OXO acepta un indicador --debug, que establece todos los loggers, el de OXO y los de cualquier paquete de terceros, en nivel de depuración. Esto mostrará mucha información sobre lo que está ocurriendo, parte de la cual es muy útil; por ejemplo, permite responder a preguntas como si la red se creó correctamente, si las configuraciones de los agentes son las correctas, si los agentes del sistema están en buen estado, etc.

Si solo le interesa investigar un agente concreto o un conjunto de agentes, puede utilizar el indicador --follow agent/<organisation>/<agent_name> para mostrar únicamente los mensajes de registro de un agente específico. Puede seguir tantos como desee; cada uno se mostrará con un color distinto para facilitar la lectura.

Estos son ambos indicadores en acción:

oxo --debug scan run --agent agent/ostorlab/subfinder --follow agent/ostorlab/subfinder --agent agent/ostorlab/dnsx --follow agent/ostorlab/dnsx domain-name ostorlab.co    

Indicador --follow
Indicador --follow

En el fondo, está depurando contenedores de Docker

Dado que los agentes son servicios de docker que trabajan de forma conjunta para escanear un activo, se pueden utilizar comandos de docker para ayudar en la investigación:

docker service ls: enumera los servicios en ejecución. Resulta útil para ver qué agentes están activos y cuáles no.
docker service logs <service>: muestra los mensajes de registro del servicio docker del agente.
docker service inspect <service>: muestra los metadatos del estado, las configuraciones, etc. del servicio.
docker stats <container>: muestra información sobre el estado del contenedor, como el porcentaje de CPU, la memoria y el uso de E/S.

Recuerde que siempre puede abrir un shell dentro del agente con:
docker exec -it <container> bash/sh, para tener más control sobre cómo quiere dirigir su investigación.

En algunos casos no tendrá un mensaje de error claro, pero sabrá que algo va mal: quizá el mensaje no se emite o no se procesa, quizá se emitió pero hacia la cola equivocada, quizá añadió una comprobación de unicidad para no procesar el mismo mensaje dos veces, pero aun así se sigue procesando una y otra vez, o puede deberse a un motivo completamente distinto.

RabbitMQ

Los agentes utilizan RabbitMQ como intermediario de mensajes (message broker) para enviar y recibir mensajes. Una cosa que podemos hacer es abrir un shell en el contenedor de RabbitMQ y utilizar los comandos de la CLI de RabbitMQ para:

Obtener la lista de colas con las columnas especificadas; puede añadirlas o quitarlas según sus necesidades:

rabbitmqadmin list queues name node messages messages_ready messages_unacknowledged message_stats.publish_details.rate message_stats.deliver_get_details.rate message_stats.publish

Estadísticas de las colas
Estadísticas de las colas

Mostrar la lista de bindings: los bindings son reglas para enrutar los mensajes a sus colas respectivas:

rabbitmqadmin list bindings

Bindings de RabbitMQ
Bindings de RabbitMQ

Comprobar que las colas responden dentro del tiempo de espera. Enumera las que no respondieron:

rabbitmqctl list_unresponsive_queues

Otro enfoque que puede adoptar con el servicio RabbitMQ es utilizar el indicador --mq-exposed-ports y especificar el puerto de la interfaz de usuario de administración de RabbitMQ que se desea exponer, por ejemplo :

oxo scan --mq-exposed-ports 15672:15672 run --agent ...

Después podrá acceder a la interfaz con 127.0.0.1:15672, con «guest» como nombre de usuario y contraseña predeterminados.

Interfaz de administración de RabbitMQ
Interfaz de administración de RabbitMQ

Puede lograr lo mismo que con la línea de comandos que vimos en la sección anterior, como enumerar colas e intercambios (exchanges), el número y la tasa de mensajes entregados o procesados, o simplemente hacerse una idea general del estado del servicio.

Interfaz de administración de colas de RabbitMQ
Interfaz de administración de colas de RabbitMQ

Redis

Redis no expone una interfaz de usuario de administración; sin embargo, como es intuitivo, en realidad no la necesitamos. Con comandos básicos podemos responder a preguntas como:

¿Qué claves están persistidas?

KEYS <regex>

puede usar * para mostrar todas las claves o cualquier expresión regular para limitar la salida.
¿Existe una clave?
Para cadenas, bytes y conjuntos use:

EXISTS <key>

Para hashmaps use:

HEXISTS key fields

¿Cuál es el tipo de valor de una clave concreta?

TYPE <key>

¿Cuál es el valor de una clave concreta? Depende del tipo: Para valores de tipo cadena o bytes

GET <key>

Para valores de tipo hashmap

HGET <key> <field>

O para listar todos los valores

HGETALL <key>

Para todos los valores de un conjunto

SMEMBERS <key>

Trazado

El trazado distribuido es una forma de obtener una vista panorámica de cómo fluye una solicitud de un sistema a otro. Puede ayudarnos a diagnosticar los mensajes a medida que pasan de un agente a otro.

OXO incorpora una instrumentación ya implementada tanto para el método de procesamiento como para el de emisión de mensajes. En otras palabras, puede ver cuándo se procesó un mensaje, con qué selector, el contenido de sus datos y también el valor de su salida o de los resultados emitidos

Para habilitar el trazado de los mensajes entre agentes, podemos ejecutar el clásico comando oxo scan run con el indicador --tracing. Esto expondrá jaeger, una interfaz de usuario para monitorizar y trazar transacciones entre servicios distribuidos. En términos sencillos, sirve para visualizar la cadena de eventos que ocurren entre microservicios, en nuestro caso entre agentes. Puede seguir un mensaje que procesa el Agente 1, y luego los resultados de ese procesamiento emitidos al Agente 2, y así sucesivamente. Puede ver distintos atributos, como la hora, la duración, qué selector se utilizó, el valor del mensaje, etc.

Antes de explicar los resultados, hablemos brevemente de los conceptos principales de OpenTelemetry.
1. Trace: la ruta que siguen las solicitudes, en nuestro caso los mensajes, a medida que se propagan por arquitecturas de varios servicios, en nuestro caso entre agentes.
2. Span: una operación rastreada realizada por la solicitud o sobre ella. En nuestro caso, por ejemplo, el mensaje que se está procesando.
3. Context: metadatos que ayudan a rastrear los spans cuando se desplazan entre los servicios. Básicamente son el traceID y el spanID, que nos ayudan a enlazar el flujo del mensaje entre los agentes.

Así, al ejecutar :

oxo scan --tracing run --agent agent/ostorlab/subfinder --agent agent/ostorlab/dnsx domain-name ostorlab.co

Se lanzarán los agentes Subfinder y DNSx sobre ostorlab.co y se abrirá un navegador en: http://127.0.0.1:16686 con la interfaz de usuario de jaeger.
Los demás agentes se crean después del servicio jaeger, por lo que es normal no ver nada al principio.

Podemos ver los 2 agentes en el menú desplegable de servicios; si tomamos el ejemplo del agente Subfinder:
Vemos nuestro primer mensaje de dominio ostorlab.co como span raíz, y sus hijos debajo, que describen la ruta de lo ocurrido. El primer mensaje se procesó y se emitieron otros 10 mensajes, cada uno con un subdominio específico: docs.ostorlab.co, blog.ostorlab.co.

Vista general del trazado de Subfinder
Vista general del trazado de Subfinder

Cada uno de ellos fue recogido por el agente DNSx, que lo procesó y emitió registros DNS con información adicional.

Detalles del trazado de Subfinder
Detalles del trazado de Subfinder

Pruebas unitarias

Al comienzo del ciclo de desarrollo de un agente, es preferible aprovechar la potencia de las pruebas unitarias para asegurarse de que el código realmente funciona y cumple su cometido. Es una forma eficaz de eliminar errores o comportamientos extraños que provienen directamente del código del agente y no de fuentes externas ni de la interacción de su agente con el resto del sistema.

OXO ofrece fixtures de pytest para ayudar a cubrir todo el proceso con pruebas unitarias, por ejemplo:

agent_run_mock: este fixture parchea los métodos de bajo nivel del agente, como la comprobación de estado del servicio del agente, la inicialización del servidor RabbitMQ, la conexión con él, y el consumo y la emisión de mensajes. También devuelve una instancia de AgentRunInstance con los siguientes atributos:

agent_run_mock.raw_messages: List
agent_run_mock.emitted_messages: List

Así, una comprobación como:

assert len(agent_run_mock.emitted_messages) > 0

nos indica que se emitió algún mensaje. Podemos acceder al primer elemento, por ejemplo, con:

agent_run_mock.emitted_messages[0]

que es una clase Message con los siguientes atributos:

message.selector
message.data

Estos atributos pueden ayudarnos a verificar, por ejemplo, que hemos utilizado el selector correcto o que los datos del mensaje son los esperados.

agent_persist_mock: este fixture parchea el componente de persistencia de Redis del agente y devuelve un diccionario que imita a Redis, con las claves como claves de Redis y los valores respetando los tipos almacenados en Redis. Resulta útil, por ejemplo, para probar agentes que heredan de persist_mixin para implementar una comprobación de unicidad de los mensajes recibidos, con el fin de no procesar el mismo mensaje varias veces, o cuando se necesita un contador que puedan utilizar todas las réplicas de un agente.

assert agent_persist_mock.get('<key>') is not None
assert agent_persist_mock.get('<global_counter>') == 42

Conclusión

Hemos visto distintos aspectos de cómo depurar agentes: desde el uso de comandos básicos para mostrar registros, comprobar las colas de RabbitMQ y verificar que existen datos en Redis. También hemos visto cómo seguir los trazados de los mensajes a lo largo de su ciclo de vida y cómo podemos aprovechar los fixtures de pytest que se ofrecen para probar nuestro código incluso antes de lanzar el agente.

Etiquetas:

open-source, oxo