Journaux et observabilité
Chaque événement émis par ZeroClaw passe par une seule crate : zeroclaw-log. La crate est responsable du schéma JSONL sur disque, du flux de diffusion au sein du processus que lit le tableau de bord, du pont facultatif vers l’Observer typé (Prometheus / OTel), ainsi que des macros (record!, scope!, spawn!) appelées par les sous-systèmes.
Cette page couvre ce dont un opérateur a besoin : la configuration, l’emplacement du journal, la structure des événements et la manière de les interroger.
Configuration ([observability])
Valeurs par défaut : log_persistence = "rolling", log_persistence_max_entries = 200, log_tool_io = "redacted", log_tool_io_truncate_bytes = 40960, log_llm_request_payload = "off". Une nouvelle installation génère un fichier JSONL rotatif de 200 événements dans ~/.zeroclaw/data/state/runtime-trace.jsonl, et la page Logs du tableau de bord fonctionne sans configuration supplémentaire.
log_persistence = "none" désactive entièrement la persistance, mais ne contrôle pas le flux de diffusion utilisé par les SSE du tableau de bord. Le pont Observer typé facultatif est également indépendant de la persistance, mais il ne reçoit les événements de journal canoniques que lorsqu’il est explicitement lié ; l’initialisation actuelle de la production n’installe pas cette liaison.
La persistance est assurée au mieux, plutôt que comme une garantie d’audit transactionnelle. Le pont Observer, lorsqu’il est lié, ainsi que la diffusion ont lieu avant que l’événement ne soit soumis à une file d’attente bornée destinée à l’écriture en arrière-plan. Une file pleine ou une défaillance d’écriture du worker peut entraîner l’absence d’un événement dans JSONL. La synchronisation périodique porte sur le fichier actif courant ; la rotation quotidienne, avant le premier ajout d’une nouvelle journée UTC, et la rotation par taille, après un ajout qui franchit le seuil, peuvent renommer le fichier actif sans le synchroniser au préalable ; la cadence ne fournit donc aucune borne de durabilité pour une archive venant d’être soumise à une rotation. Voir Architecture de la journalisation pour les contrats de livraison distincts.
Rotation des archives (log_persistence = "rotating")
rotating n’applique aucune limitation du nombre d’entrées aux événements acceptés par le processus d’écriture en arrière-plan, comme full, mais ZeroClaw gère le fichier actif : celui-ci est déplacé vers une archive horodatée lorsqu’un seuil de taille et/ou une limite quotidienne est atteint, et les anciennes archives sont supprimées selon leur nombre et leur ancienneté. Cela diffère de rolling, qui supprime les anciennes entrées du fichier actif ; les événements ayant fait l’objet d’une rotation sont conservés dans des fichiers d’archive pour faciliter les diagnostics ultérieurs.
| Clé | Par défaut | Effet |
|---|---|---|
log_persistence_max_bytes | 0 | Effectue une rotation dès qu’un append laisse le fichier actif à ou au-dessus de ce nombre d’octets. 0 désactive la rotation par taille. |
log_persistence_rotate_daily | true | Avant le premier événement d’un nouveau jour UTC, archiver un fichier dont la dernière écriture date d’un jour précédent. |
log_persistence_retention_max_files | 7 | Conservez au plus ce nombre d’archives ; après une rotation, les plus anciennes dépassant cette limite sont supprimées. 0 conserve toutes les archives. |
log_persistence_retention_max_age_days | 0 | Supprime les archives plus anciennes que ce nombre de jours après une rotation. 0 désactive le nettoyage basé sur l’âge. |
Les archives sont placées à côté du fichier actif et en conservent l’extension, avec un horodatage UTC triable inséré avant cette extension. Par exemple, runtime-trace.jsonl passe à runtime-trace.20260624-031500.jsonl. Le tableau de bord et le point de terminaison /api/logs ne lisent que le fichier actif, les archives constituant donc un enregistrement sur disque destiné à l’inspection hors ligne plutôt qu’une surface d’interrogation en direct.
La rotation journalière s’appuie sur le calendrier UTC, de sorte que sa limite peut ne pas coïncider avec minuit local dans d’autres fuseaux horaires. Ces clés sont ignorées sauf si log_persistence = "rotating", et les modes none, rolling et full restent inchangés.
Attributs de span GenAI (observability-otel)
Les spans llm.response portent les attributs de contenu de message OTel GenAI gen_ai.input.messages, gen_ai.output.messages et gen_ai.system_instructions (encodés en chaîne JSON), qui peuplent les panneaux Input/Output/System dans Langfuse/Tempo.
Confidentialité et coût. Le contenu capturé est nettoyé selon une approche best-effort : les données d’image intégrées sont omises et les formats connus d’identifiants (key=value, bearer, ainsi que les préfixes de style
sk-/ghp_/xoxb--) sont masqués. Cela ne garantit PAS la suppression de tous les secrets ou des données personnelles (PII). Privilégiez un backend de traçage avec contrôle d’accès si les conversations peuvent être sensibles. Le coût de capture est O(taille du prompt) par itération de la boucle agent (l’historique en croissance est rescané à chaque tour), et la charge utile par span croît proportionnellement au texte complet. Sur les backends à facturation par octet, appliquez la troncature côté exporteur plutôt que de supprimer les attributs.
Capture de contenu OTel
La capture de contenu OTel est indépendante de la capture basée sur les logs (log_tool_io, log_llm_request_payload). Elle contrôle le contenu émis en tant qu’attributs de span OpenTelemetry.
Contenu IA générative
Contrôle gen_ai.system_instructions, gen_ai.input.messages et gen_ai.output.messages sur les spans OTel.
[observability]
otel_genai_content = "off" # off | redacted | full
otel_genai_content_max_chars = 1000 # limite de troncature par champ
off(par défaut) : Aucun attribut de contenu, uniquement des métadonnées.redacted: Le contenu est scanné pour les fuites et tronqué àmax_charspar champ.full: Le contenu est scanné pour les fuites mais n’est pas tronqué.
E/S d’outil
Contrôle les gen_ai.tool.arguments, input.value, gen_ai.tool.result et output.value sur les spans OTel.
[observability]
otel_tool_io = "off" # off | redacted | full
otel_tool_io_max_chars = 1000 # limite de troncature par champ
off(par défaut) : Aucun attribut de contenu, uniquement le nom de l’outil + le résultat.redacted: Le contenu est scanné pour les fuites et tronqué àmax_charspar champ.full: Le contenu est scanné pour les fuites mais n’est pas tronqué.
Notes de comportement
- Définir
*_max_chars = 0est équivalent àoffpour cette politique. - Le contenu est toujours nettoyé (motifs d’identifiants + motifs de secrets) avant la troncature.
- La troncature préserve la structure JSON pour les arguments d’outils (chaînes feuilles tronquées).
- Les champs tronqués reçoivent un marqueur
…[truncated {n} of {total} chars]. Le marqueur est une métadonnée et ne compte pas dansmax_chars: le contenu conservé fait exactementmax_charscaractères, le marqueur étant ajouté par-dessus. - La valeur par défaut
offest un changement axé sur la confidentialité par rapport au comportement précédent (activé par un flag de fonctionnalité, mais toujours actif une fois activé). - La politique de contenu est liée à l’instance observer/config, et non au processus. Il n’existe pas de politique de contenu OTel globale au processus : chaque
OtelObserverdérive une configuration de contenu immuable deObservabilityConfigà la construction et la consulte à la frontière d’export OTel. Plusieurs observers dans le même processus conservent des politiques indépendantes : un observer ultérieur ne peut ni remplacer ni faire taire le paramètre de confidentialité d’un observer antérieur (pas de last-writer-wins, pas de dérive inter-observers).
Mémoires imbriquées par tour et spans RAG (observability-otel)
Les spans memory.recall, memory.store et rag.retrieve s’imbriquent sous le span de tour gen_ai.agent.invoke chaque fois que l’opération s’exécute au sein d’un tour d’agent attribué, de sorte qu’un tour complet (rappel de mémoire, stockage par sauvegarde automatique, appels LLM, appels d’outils) apparaît comme une seule trace dans Langfuse/Tempo. Les trois événements portent le même triplet channel / agent_alias / turn_id que les événements LLM et outils, exposés sous forme d’attributs de span zeroclaw.channel, gen_ai.agent.name et zeroclaw.turn_id.
Les opérations mémoire en dehors d’un tour corrélé continuent de produire des spans racines : le stockage mémoire REST de la passerelle, et la récupération hardware-RAG de process_message, qui s’exécute avant l’ouverture de la parenthèse de tour et reste donc un span racine portant l’attribut zeroclaw.turn_id correspondant (l’imbrication complète de ce span est suivie dans #8844). Un turn_id qui ne correspond plus à un tour actif se dégrade également en span racine plutôt que de deviner un parent.
Capture de la charge utile de la requête LLM (log_llm_request_payload)
log_llm_request_payload contrôle si l’événement llm_request enregistre le prompt sortant et la conversation en plus de son messages_count. Il est désactivé par défaut et constitue une surface sensible en matière de confidentialité : lorsqu’il est activé, ZeroClaw persiste le prompt système complet ainsi que l’intégralité de l’historique de conversation à chaque tour.
| Valeur | Ce qui est capturé |
|---|---|
off (par défaut) | Uniquement messages_count. Aucun contenu de message n’est enregistré ; comportement existant. |
redacted | Historique complet des messages (rôle + contenu), scanné pour les informations d’identification avec la même passe scrub_credentials utilisée pour raw_response et l’I/O des outils, puis tronqué à log_tool_io_truncate_bytes. La troncature est signalée par request_messages_truncated et request_messages_original_bytes. |
full | Même nettoyage des identifiants que redacted, mais non tronqué (fidélité de rejeu, miroir de raw_response). |
redacted et full appliquent toujours le nettoyage des identifiants ; la seule différence entre les deux réside dans la troncature. La capture réutilise la limite existante log_tool_io_truncate_bytes plutôt que d’en introduire une seconde. Définissez ou laissez log_llm_request_payload = "off" pour désactiver la capture instantanément, sans redéploiement.
Format sur disque
JSONL : un événement par ligne, UTF-8, permissions 0o600 sous Unix. Le chemin critique est non bloquant : record_event transmet l’événement sérialisé à un thread d’arrière-plan dédié (zeroclaw-log-writer) via un canal borné et retourne immédiatement. Le worker appelle sync_all à une cadence périodique : tous les 100 écritures ou toutes les 1 seconde de temps horloge, selon ce qui survient en premier, plus un dernier sync_all lorsque le canal se ferme lors d’un arrêt normal. Ce compromis troque la durabilité par événement (le comportement synchrone antérieur) contre une latence d’écriture bornée : un crash du processus peut entraîner la perte d’un intervalle de synchronisation d’écritures en attente. Si le worker prend du retard, record_event rejette l’événement avec un tracing::warn! plutôt que de bloquer le runtime asynchrone. Les workers sont des singletons par processus ; la désactivation et la réactivation de la persistance via init_from_config supprime l’ancien worker (la fermeture du canal déclenche sa synchronisation finale et la sortie du thread) et en instancie un nouveau.
La forme de la ligne reflète zeroclaw_log::event::LogEvent. Clés de premier niveau :
| Clé | Type | Notes |
|---|---|---|
id | Chaîne UUID v4 | ID d’événement persistant. |
@timestamp | RFC 3339 + ms, UTC | Triable lexicographiquement ; le lecteur effectue le tri sur ce champ. |
severity_number | u8 | OTel : 1 TRACE, 5 DEBUG, 9 INFO, 13 WARN, 17 ERROR. |
severity_text | chaîne | Libellé du bucket pour severity_number. |
event.category | chaîne | agent, channel, cron, memory, tool, provider, session, system, ou internal. |
event.action | chaîne | Identifiant stable (llm_request, channel_message_inbound, …). |
event.outcome | string | omitted | success, failure, unknown (omis lorsque unknown). |
service.name | chaîne | Constant "zeroclaw". |
service.version | chaîne | Version du crate du démon en cours d’exécution. |
trace_id | chaîne hexadécimale | omise | Corrélation par tour. Un tour d’agent = un trace_id. |
span_id | chaîne hexadécimale | omise | Sous-segment au sein d’un tour. |
zeroclaw.* | carte de chaînes plate | Attribution liée à l’alias (voir ci-dessous). |
message | string | omitted | Corps de ligne lisible par l’utilisateur. |
attributes | objet | omis | Charge utile par action en format libre. |
schema_version | u8 | Actuellement 2. Les lignes v1 migrent sur place au démarrage. |
Attribution zeroclaw.*
La source de vérité Rust est ATTRIBUTION_FIELDS + COMPOSITE_PREFIXES dans crates/zeroclaw-log/src/event.rs. La réponse /api/logs transporte la liste canonique en tant que attribution_keys ; récupérez-la au lieu de la coder en dur.
Les champs simples (ATTRIBUTION_FIELDS) contiennent chacun une seule chaîne. Les préfixes composites obtiennent trois clés : <prefix>, <prefix>_type, <prefix>_alias (par ex. channel = "discord.glados", channel_type = "discord", channel_alias = "glados"). Les filtres peuvent correspondre de manière approximative ou précise.
Lorsqu’un appel de traçage définit un champ à préfixe composite avec un type seul (sans .), seul l’emplacement _type est renseigné, de sorte qu’un appel tracing::*!(model_provider = name, …) à l’intérieur d’un span qui porte déjà le composite complet <type>.<alias> ne l’écrase pas lors de la fusion feuille→racine.
Interrogation
La page Logs du tableau de bord est la surface principale. En dessous :
GET /api/logs
Filtres de premier niveau (paramètres de requête) : since_ts, until_ts, until_line_offset, action, category, outcome, severity_min, trace_id, q (sous-chaîne sur message + attributes), hide_internal (supprime event.category = "internal"), limit. Le champ historique until_id reste disponible pour la compatibilité avec les curseurs horodatage/ID.
Chaque autre ?<key>=<value> est traité comme un filtre d’égalité par attribution, la passerelle valide la clé avec is_attribution_field et rejette les clés inconnues avec une erreur 400. La réponse inclut attribution_keys: string[], afin que les appelants n’aient pas à deviner.
Exemples :
sh
# Tous les événements WARN+ depuis le démarrage du démon.
curl $ZEROCLAW_GATEWAY/api/logs?severity_min=13
# Événements d'un agent spécifique :
curl "$ZEROCLAW_GATEWAY/api/logs?agent_alias=glados"
# Trafic Discord pour un bot :
curl "$ZEROCLAW_GATEWAY/api/logs?channel=discord.glados"
# Un tour d'agent unique :
curl "$ZEROCLAW_GATEWAY/api/logs?trace_id=<value-from-a-prior-event>"
La pagination des journaux remonte en arrière à l’aide d’un curseur d’offset en octets. Tant que at_end est false, transmettez une valeur non nulle de next_cursor_line_offset en tant que until_line_offset avec les mêmes filtres hors curseur pour charger des événements plus anciens sans relire les octets plus récents. Recommencez depuis la page la plus récente après avoir modifié les filtres. Traitez at_end: true comme le signal d’arrêt des requêtes de pages plus anciennes pour ce parcours de pagination. La réponse héritée next_cursor: [timestamp, id] | null est conservée pour la compatibilité ; l’utilisation de sa paire horodatage/ID en tant que until_ts et until_id pour la pagination est dépréciée, car le départage lexicographique par ID peut ignorer silencieusement des événements ayant le même horodatage.
until_line_offset est une position dans le fichier actif actuel, et non un point de contrôle d’événement persistant. Les ajouts seuls la préservent, mais la purge par rotation, la rotation des archives, la migration au démarrage et une modification du chemin configuré remplacent les octets ou le fichier actif auxquels elle fait référence. Reprenez à partir de la page la plus récente après ces changements plutôt que de réutiliser un ancien décalage. /api/logs ne lit que le fichier actif ; consultez directement les archives horodatées lorsqu’un historique plus ancien ayant fait l’objet d’une rotation est nécessaire.
La réponse /api/status inclut daemon_started_at: string (RFC 3339), ce qui permet à un tableau de bord d’utiliser par défaut « depuis le démarrage du démon » sans aller-retour supplémentaire.
Visionneuses de journaux externes
Le schéma JSONL est un hybride OTel-logs + ECS : @timestamp, severity_number + severity_text, event.{category,action,outcome}, service.{name,version}, attributes, ainsi que l’espace de noms fournisseur zeroclaw.*. La plupart des visionneuses de journaux l’ingèrent avec peu ou pas de transformation. Remplacez <install> par le chemin absolu de votre répertoire d’installation dans les exemples ci-dessous (généralement ~/.zeroclaw développé).
Grafana Loki
Les libellés Promtail extraient agent_alias, channel et severity_text afin qu’ils soient filtrables dans Grafana :
scrape_configs:
- nom_tâche: zeroclaw
static_configs:
- cibles: [localhost]
étiquettes:
job: zeroclaw
__path__: /data/state/runtime-trace.jsonl
pipeline_stages:
- json:
expressions:
agent: zeroclaw.agent_alias
canal: zeroclaw.channel
niveau: severity_text
- étiquettes:
agent:
canal:
niveau:
- horodatage:
source: '@timestamp'
format: RFC3339
OpenTelemetry Collector
Le récepteur filelog mappe le schéma directement. Exportez ensuite vers n’importe quel collecteur OTel (Tempo, Honeycomb, Datadog, etc.) :
destinataires:
filelog/zeroclaw:
include: [/data/state/runtime-trace.jsonl]
opérateurs:
- type: json_parser
horodatage:
parse_from: attributes["@timestamp"]
disposition: '%Y-%m-%dT%H:%M:%S.%LZ'
sévérité:
parse_from: attributes.severity_number
Kibana / Elastic
L’ingestion fonctionne telle quelle. Les pipelines ECS stricts attendent log.level à la place de severity_text. Un pipeline d’ingestion Filebeat qui renomme severity_text en log.level (et severity_number en log.syslog.severity.code) comble cette lacune. @timestamp et event.{category,action,outcome} sont déjà aux positions canoniques.
Vector / Fluent Bit
Les deux suivent en continu (tail) le JSONL avec une étape d’analyse JSON ; aucune transformation de schéma n’est nécessaire avant l’envoi vers n’importe quel backend.
Format du terminal
Le formateur de stderr du daemon préfixe chaque ligne avec l’identité liée à l’alias englobante la plus proche :
- agent context →
[<agent_alias>] - channel-only context (écouteur de canal, pas encore d’agent) →
[<channel_composite>](par ex.[discord.glados]) - sinon →
[system]
La chaîne de spans suit : channel_listener{channel=discord.glados}: …. Les champs de span sont visibles en ligne.
Migration de schéma
Au démarrage, si log_persistence est activé et que le fichier existe, le writer fait transiter les lignes au schéma 1 par une migration sur place vers le schéma 2 avant le premier ajout. Streaming pur, avec une allocation limitée à une seule ligne quelle que soit la taille du fichier. Le fichier migré est renommé de manière atomique à son emplacement final. Les fichiers déjà en v2 restent intacts.
Si la migration échoue, le démon journalise un warn et continue d’écrire les ajouts v2 ; les anciennes lignes v1 restent lisibles par les outils qui comprennent encore v1 mais ne passeront pas le désérialiseur du lecteur v2.
Qu’est-ce que internal ?
event.category = "internal" est la catégorie destinée au bruit opérationnel dont un opérateur n’a pas besoin sur le tableau de bord par défaut : signaux de pulsation (heartbeat), diffusions en attente, nouvelles tentatives de synchronisation avec perte, et autres. Le commutateur « Hide internal » du tableau de bord (activé par défaut) filtre ces éléments.
Utilisez-le lorsque vous avez un événement à haute fréquence dont la présence importe pour l’analyse forensique mais dont l’absence est l’état normal. Ne l’utilisez pas comme régulateur de volume pour de véritables erreurs.
Fichiers d’intérêt
crates/zeroclaw-log/src/event.rs: la structure canonique deLogEvent.crates/zeroclaw-log/src/layer.rs: le Layertracing-subscriberqui capture chaque appeltracing::*et alimente le pipeline.crates/zeroclaw-log/src/macro.rs:record!,scope!,spawn!.crates/zeroclaw-log/src/writer.rs: ajout, troncature glissante et rotation d’archives.crates/zeroclaw-log/src/reader.rs: lecteur de/api/logs.crates/zeroclaw-log/src/config.rs:StoragePolicy,ToolIoPolicy,ResolvedPolicy.crates/zeroclaw-log/src/migrate.rs: migration en streaming de schema-1 → schema-2.crates/zeroclaw-log/src/observer_bridge.rs: projectionObservertypée pour les consommateurs Prometheus / OTel.crates/zeroclaw-gateway/src/api_logs.rs: l’adaptateur HTTP.
Vérifiez la source avant de vous fier au texte de cette page.