Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Ingénierie

Ingénierie

Trucs et astuces pour développer et déboguer les agents OXO.

Des trucs et astuces pour vous faciliter la vie lors du développement et du débogage des agents OXO.

Cet article fait suite au précédent article sur la manière d'écrire un agent OXO. Dans les sections suivantes, nous partagerons des trucs et astuces pour que l'écriture et le débogage des agents deviennent une tâche simple.

Les logs du moteur et des agents OXO

Le moteur d'OXO accepte un drapeau --debug, qui fait passer tous les loggers (ceux d'OXO comme ceux de tout paquet tiers) au niveau debug. Cela affiche beaucoup d'informations sur ce qui se passe, dont certaines sont très utiles : vous pouvez par exemple répondre à des questions comme « le réseau a-t-il été créé avec succès ? », « les configurations des agents sont-elles les bonnes ? », « les agents système sont-ils en bonne santé ? », etc.

Si vous souhaitez seulement investiguer un agent précis ou un ensemble d'agents, vous pouvez utiliser le drapeau --follow agent/<organisation>/<agent_name> pour n'afficher que les messages de log d'un agent donné. Vous pouvez en suivre autant que vous le souhaitez ; chacun sera coloré différemment pour une meilleure lisibilité.

Voici les deux drapeaux en action :

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

Drapeau --follow
Drapeau --follow

Vous déboguez en réalité des conteneurs docker

Comme les agents sont des services docker qui travaillent collectivement pour scanner un actif, on peut s'appuyer sur des commandes docker pour faciliter l'investigation :

docker service ls : liste les services en cours d'exécution. Utile pour voir quels agents sont actifs et lesquels sont à l'arrêt.
docker service logs <service> : affiche les messages de log du service docker de l'agent.
docker service inspect <service> : affiche les métadonnées de l'état du service, ses configurations, etc.
docker stats <container> : affiche des informations sur l'état du conteneur, comme le CPU %, la mémoire et l'utilisation des E/S.

N'oubliez pas que vous pouvez toujours ouvrir un shell dans l'agent avec :
docker exec -it <container> bash/sh, pour mieux contrôler la manière dont vous menez votre investigation.

Dans certains cas, vous n'aurez pas de message d'erreur clair, mais vous saurez que quelque chose ne va pas : peut-être que le message n'est pas émis ou traité, peut-être qu'il a été émis mais dans la mauvaise file, peut-être que vous avez ajouté une vérification d'unicité pour ne pas traiter deux fois le même message, mais qu'il est quand même traité encore et encore, ou bien il peut s'agir d'une raison complètement différente.

RabbitMQ

Les agents utilisent RabbitMQ comme broker de messages pour envoyer et recevoir des messages. Nous pouvons ouvrir un shell dans le conteneur RabbitMQ et utiliser les commandes cli de RabbitMQ pour :

Obtenir la liste des files avec les colonnes spécifiées, que vous pouvez ajouter ou retirer selon vos besoins :

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

Statistiques des files
Statistiques des files

Afficher la liste des liaisons (bindings) : les bindings sont des règles pour acheminer les messages vers les files respectives :

rabbitmqadmin list bindings

Bindings RabbitMQ
Bindings RabbitMQ

Tester si les files répondent dans le délai imparti. Liste celles qui n'ont pas répondu :

rabbitmqctl list_unresponsive_queues

Une autre approche possible avec le service RabbitMQ consiste à utiliser le drapeau --mq-exposed-ports et à spécifier le port de l'interface utilisateur de gestion de RabbitMQ à exposer, par exemple :

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

Vous pouvez ensuite accéder à l'interface avec 127.0.0.1:15672, avec 'guest' comme nom d'utilisateur et mot de passe par défaut.

Interface de gestion de RabbitMQ
Interface de gestion de RabbitMQ

Vous pouvez y faire la même chose qu'avec la ligne de commande vue dans la section précédente, comme lister les files, les exchanges, le nombre et le débit de messages livrés ou traités, ou simplement vous faire une idée générale de l'état du service.

Interface de gestion des files RabbitMQ
Interface de gestion des files RabbitMQ

Redis

Redis n'expose pas d'interface utilisateur de gestion, mais comme il est intuitif, nous n'en avons pas vraiment besoin. Avec quelques commandes de base, nous pouvons répondre à des questions comme :

Quelles clés sont persistées ?

KEYS <regex>

vous pouvez utiliser * pour afficher toutes les clés, ou n'importe quelle expression régulière pour limiter la sortie.
Une clé existe-t-elle ?
Pour les chaînes, les octets et les ensembles, utilisez :

EXISTS <key>

Pour les hashmaps, utilisez :

HEXISTS key fields

Quel est le type de la valeur d'une clé donnée ?

TYPE <key>

Quelle est la valeur d'une clé donnée ? Selon le type : Pour les valeurs de type chaîne/octets

GET <key>

Pour les valeurs de type hash-map

HGET <key> <field>

Ou pour lister toutes les valeurs

HGETALL <key>

Pour toutes les valeurs d'un ensemble

SMEMBERS <key>

Traçage

Le traçage distribué est un moyen d'obtenir une vue d'ensemble de la manière dont une requête circule d'un système à un autre. Il peut nous aider à diagnostiquer les messages lorsqu'ils passent d'un agent à un autre.

OXO est fourni avec une instrumentation déjà implémentée pour les méthodes de traitement et d'émission de messages. En d'autres termes, vous pouvez voir quand un message a été traité, avec quel sélecteur, le contenu de ses données, ainsi que la valeur de sa sortie ou des résultats émis

Pour activer le traçage des messages entre les agents, nous pouvons exécuter la commande classique oxo scan run avec le drapeau --tracing. Cela expose jaeger, une interface utilisateur de supervision et de traçage des transactions entre des services distribués. En termes simples, elle sert à visualiser la chaîne d'événements qui se produisent entre des microservices, dans notre cas entre des agents. Vous pouvez suivre un message traité par l'Agent1, puis les résultats de ce traitement émis vers l'Agent2, et ainsi de suite. Vous pouvez voir différents attributs comme l'heure, la durée, le sélecteur utilisé, la valeur du message, etc.

Avant d'expliquer les résultats, parlons brièvement des principaux concepts d'OpenTelemetry.
1. Trace : le chemin emprunté par les requêtes, dans notre cas les messages, lorsqu'elles se propagent dans des architectures multi-services, dans notre cas entre des agents.
2. Span : une opération suivie, effectuée par la requête ou sur elle. Dans notre cas, par exemple, le message en cours de traitement.
3. Contexte : des métadonnées qui permettent de suivre les spans lorsqu'ils passent d'un service à l'autre. Il s'agit essentiellement du traceID et du spanID, qui nous aident à relier le flux du message entre les agents.

Ainsi, en exécutant :

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

Les agents Subfinder et DNSx seront lancés sur ostorlab.co et un navigateur s'ouvrira sur : http://127.0.0.1:16686 avec l'interface utilisateur de jaeger.
Les autres agents sont lancés après le service jaeger, il est donc normal de ne rien voir au début.

Nous voyons les 2 agents dans la liste déroulante des services ; si l'on prend l'exemple de l'agent Subfinder :
nous voyons notre premier message de domaine ostorlab.co comme span racine, et ses enfants en dessous, décrivant le chemin de ce qui s'est passé. Le premier message a été traité, et 10 autres messages ont été émis, chacun avec un sous-domaine précis : docs.ostorlab.co, blog.ostorlab.co.

Vue d'ensemble de la trace de Subfinder
Vue d'ensemble de la trace de Subfinder

Chacun d'eux a été récupéré par l'agent DNSx, traité, et chacun a émis des enregistrements DNS avec des informations supplémentaires.

Détails de la trace de Subfinder
Détails de la trace de Subfinder

Tests unitaires

Au début du cycle de développement d'un agent, il est préférable de tirer parti de la puissance des tests unitaires pour s'assurer que votre code fonctionne réellement et remplit son objectif. C'est un moyen efficace d'éliminer les bugs ou les comportements étranges provenant directement du code de l'agent, et non de sources externes ou de l'interaction de votre agent avec le reste du système.

OXO propose des fixtures pytest pour aider à couvrir l'ensemble du processus dans des tests unitaires, par exemple :

agent_run_mock : cette fixture remplace par des mocks les méthodes de bas niveau de l'agent, comme le contrôle de santé du service de l'agent, l'initialisation du serveur RabbitMQ, la connexion à celui-ci, la consommation et l'émission de messages. Elle renvoie aussi une instance AgentRunInstance avec les attributs suivants :

agent_run_mock.raw_messages: List
agent_run_mock.emitted_messages: List

Ainsi, une vérification comme :

assert len(agent_run_mock.emitted_messages) > 0

nous indique qu'un message a bien été émis. Nous pouvons accéder au premier élément, par exemple, avec :

agent_run_mock.emitted_messages[0]

qui est une classe Message avec les attributs suivants :

message.selector
message.data

Ces attributs peuvent nous aider à vérifier, par exemple, que nous avons utilisé le bon sélecteur ou que les données du message sont celles que nous attendions.

agent_persist_mock : cette fixture remplace par un mock le composant de persistance Redis de l'agent et renvoie un dictionnaire imitant Redis, avec les clés comme clés Redis et des valeurs respectant les types stockés dans Redis. C'est utile, par exemple, pour tester les agents qui héritent de persist_mixin afin d'implémenter une vérification d'unicité des messages reçus, pour ne pas traiter plusieurs fois exactement le même message, ou lorsque vous avez besoin d'un compteur utilisable par toutes les réplicas d'un agent.

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

Conclusion

Nous avons vu différents aspects du débogage des agents : de l'utilisation de commandes de base pour afficher les logs, vérifier les files RabbitMQ et s'assurer de l'existence des données Redis, jusqu'au suivi des traces de messages tout au long de leur cycle de vie. Nous avons aussi vu comment tirer parti des fixtures pytest proposées pour tester notre code avant même de lancer l'agent.

Tags :

open-source, oxo