Journaux et observabilité
Chaque événement émis par ZeroClaw transite par une seule crate : zeroclaw-log. Cette crate gère le schéma JSONL sur disque, le flux de diffusion en mémoire que lit le tableau de bord, le pont Observer typé optionnel, le pont natif d’export de logs OTLP, ainsi que les 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.
Capture par démon de bureau
Le superviseur Desktop capture la sortie standard et la sortie d’erreur du démon dans un seul fichier borné : <config-dir>/logs/zeroclaw-desktop-daemon.log. <config-dir> suit la priorité de résolution canonique de la configuration : ZEROCLAW_CONFIG_DIR, puis ZEROCLAW_DATA_DIR, puis ZEROCLAW_WORKSPACE (déprécié), puis la résolution Homebrew/par défaut. Le fichier est limité à 8 MiB ; lorsque la sortie dépasse cette limite, les octets les plus anciens sont compactés et la fin la plus récente est conservée.
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 le tableau de bord SSE ou l’export OTLP natif. Le pont Observer typé et optionnel 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’amorçage de production actuel n’installe pas cette liaison.
La persistance est faite au mieux (best-effort) et ne constitue pas une garantie d’audit transactionnelle. Le pont Observer, la file d’attente OTLP native et la diffusion par broadcast s’exécutent avant que l’événement ne soit proposé à une file d’attente bornée du worker d’écriture en arrière-plan. Une file pleine ou un échec d’écriture du worker peut laisser un événement absent du JSONL. La synchronisation périodique couvre le fichier actif courant ; la rotation quotidienne, effectuée avant le premier ajout d’un nouveau jour UTC, et la rotation par taille, effectuée après un ajout qui franchit le seuil, peuvent renommer le fichier actif sans l’avoir synchronisé au préalable. La cadence de synchronisation ne borne donc pas la durabilité d’une archive qui vient d’être tournée. Voir Architecture de journalisation pour les contrats de livraison distincts.
Rotation des archives (log_persistence = "rotating")
rotating conserve chaque événement en faisant pivoter le fichier actif plutôt que de le tronquer. Le fichier actif est renommé en une archive horodatée lors d’un déclenchement par taille, par limite de jour ou par nombre d’entrées. Les anciennes archives sont élaguées selon leur nombre et leur âge après chaque rotation.
| 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_max_entries_per_segment | 0 | Effectuer une rotation dès que le nombre de lignes non vides du segment atteint cette limite. En régime permanent, chaque archive contient exactement ce nombre d’entrées. Lorsqu’il est activé pour la première fois sur un journal existant, le fichier est archivé intégralement (une transition au-delà de la limite) ; le régime permanent reprend à la rotation suivante. 0 désactive la rotation basée sur le nombre d’entrées. |
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 se trouvent à côté du fichier actif et conservent son extension, avec un horodatage UTC triable inséré avant cette extension. Par exemple, runtime-trace.jsonl devient runtime-trace.0000000001-20260624-031500.jsonl lors de la rotation. Le préfixe de séquence (0000000001) est écrit au moment de la rotation et détermine l’ordre de lecture côté lecteur. Un fichier annexe écrit au mieux conserve le plus haut niveau atteint (high-water mark) d’un redémarrage à l’autre. Si ce fichier annexe ne peut pas être écrit, que la rétention supprime toutes les archives numérotées et que le démon redémarre ensuite, une séquence peut être réutilisée ; un curseur en attente peut alors se rattacher à un historique plus récent. Les archives écrites avant l’introduction de la numérotation des séquences conservent leur ancien format (runtime-trace.20260624-031500.jsonl) et sont triées avant toutes les archives numérotées.
Le tableau de bord et GET /api/logs lisent désormais le fichier actif et toutes les archives conservées comme un seul flux d’événements logique. Les archives sont fusionnées de la plus ancienne à la plus récente, et les événements sont renvoyés du plus récent au plus ancien. L’API expose un curseur tenant compte des segments (next_segment_cursor) en complément du curseur à décalage en octets existant (next_cursor_line_offset) ; passez ?until_segment_cursor= dans les requêtes suivantes pour paginer au-delà des frontières de segments. Les anciens curseurs à décalage en octets restent valides et sont interprétés comme un décalage dans le fichier actif.
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.
Journaux OTLP natifs (observability-otel)
La fonctionnalité optionnelle observability-otel exporte les traces, les métriques et les journaux via le SDK officiel OpenTelemetry. Les journaux utilisent OTLP/HTTP avec un encodage protobuf et sont envoyés par lots vers <otel_endpoint>/v1/logs ; les traces et les métriques conservent leurs points de terminaison standard /v1/traces et /v1/metrics. Cette fonctionnalité reste exclue des builds par défaut en raison de l’empreinte plus importante du SDK et du client HTTP.
[observability]
backend = "otel"
otel_endpoint = "http://localhost:4318"
otel_service_name = "zeroclaw"
# L'inspection locale/hors ligne reste disponible en même temps.
log_persistence = "rolling"
# En-têtes facultatifs du fournisseur ou du collecteur.
[observability.otel_headers]
Authorization = "Bearer <collector-token>"
Compilez avec --features observability-otel. Le point de terminaison peut être un OpenTelemetry Collector, Grafana Alloy ou tout point de terminaison d’un éditeur acceptant les journaux OTLP sur HTTP/protobuf. Les valeurs d’en-têtes sont des champs de configuration secrets et n’apparaissent jamais dans les enregistrements exportés.
Chaque LogEvent canonique est mappé vers le modèle de données stable OpenTelemetry Logs :
| ZeroClaw field | OpenTelemetry |
|---|---|
@timestamp | LogRecord Timestamp ; l’heure d’export devient ObservedTimestamp |
severity_number, severity_text | LogRecord : champs de sévérité normalisés |
message | LogRecord Body |
| Nom du service configuré + version du crate | Ressource service.name, service.version |
| identifiants de trace valides de 16 octets et de span de 8 octets | LogRecord TraceId, SpanId |
event.* | Attributs LogRecord compatibles avec ECS |
_file, _line | code.file.path, code.line.number |
| modèle/fournisseur/jeton | gen_ai.* attributs sémantiques |
| identité de l’outil | tool.name |
Attribution zeroclaw.* | espaces de noms pour les attributs de LogRecord, y compris zeroclaw.sop_run_id |
Les chaînes de corrélation d’application qui ne sont pas des identifiants de trace/span OTel valides restent consultables sous forme d’attributs zeroclaw.trace_id / zeroclaw.span_id, plutôt que d’être converties en un contexte de trace inventé. Les valeurs JSON imbriquées sont mappées vers des maps et des listes AnyValue d’OTel. Les attributs éphémères réservés à la diffusion, y compris les codes d’appairage, ne sont jamais présentés à l’exportateur.
L’exportateur est additif. JSONL, le SSE du tableau de bord, /api/logs, la CLI et ZeroCode conservent leur schéma existant et continuent de fonctionner si le collecteur est indisponible. Le remplacement ou l’arrêt de l’exportateur force le vidage de la file d’attente par lots du SDK ; l’émission à l’exécution se contente de mettre en file d’attente et n’effectue aucune E/S réseau côté appelant.
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) | Aucun contenu de message n’est enregistré. L’événement contient toujours messages_count ainsi que les empreintes de préfixe toujours actives décrites ci-dessous (system_chars, tools_count et, lorsqu’ils sont présents, system_sha256, tools_sha256) ; il s’agit de condensats et non de contenu, mais un lecteur de trace peut néanmoins les comparer pour en vérifier l’égalité. |
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). |
Chaque événement llm_request transporte aussi des empreintes de préfixe des entrées de requête du runtime, quelle que soit la politique de charge utile : system_chars et tools_count sont toujours présents ; system_sha256 (les 16 premiers caractères hexadécimaux d’un SHA-256 calculé sur les messages system initiaux contigus, sérialisés sous forme de tableau JSON de leurs contenus, présent uniquement lorsque le premier message a le rôle system) et tools_sha256 (le même calcul sur les spécifications d’outils natives sérialisées sous forme de tableau JSON, présent uniquement lorsque des spécifications d’outils sont jointes, de sorte qu’un ensemble d’outils réordonné produit une empreinte différente). Elles décrivent ce que le runtime a transmis à l’adaptateur du fournisseur, une ligne par requête logique ; les transformations côté fournisseur (nettoyage des schémas, placement de cache-control, préfixe système OAuth, fusion des messages système sur les protocoles compatibles, second message système initial que l’adaptateur Anthropic ignore, nouvelles tentatives dans l’adaptateur) ne font pas partie des octets hachés. Considérez un changement comme une indication de diagnostic, et non comme un verdict sur le cache : un system_sha256 modifié signifie qu’un des messages système initiaux a changé, ou qu’un message a été inséré ou supprimé, par exemple après la modification d’un fichier d’espace de travail inclus, un hook avant appel qui a modifié ou ajouté un message, ou un basculement du cadrage des outils entre le protocole natif et le protocole texte ; un tools_sha256 modifié signifie que la sérialisation runtime des spécifications d’outils a changé, y compris pour des champs de métadonnées que le protocole ne transporte pas. En mode protocole texte, les instructions d’outils se trouvent dans le texte système : tools_count vaut 0 et tools_sha256 est absent, tandis qu’un changement de jeu d’outils apparaît sous system_sha256. Confirmez un cache hit ou miss réel à partir de l’utilisation du cache rapportée par le fournisseur. Les empreintes sont des compteurs et des hachages tronqués non salés, et non du contenu : elles révèlent si une entrée a changé et permettent de tester l’égalité avec un texte candidat connu, mais ne conservent aucun texte de message. Toute personne pouvant lire la trace peut tester l’égalité d’un prompt ou d’un jeu d’outils candidat connu ; traitez donc les fichiers de trace avec les mêmes contrôles d’accès que la capture de charge utile en option qui les accompagne.
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, until_segment_cursor, action, category, outcome, severity_min, trace_id, q (sous-chaîne recherchée dans message + attributes), hide_internal (exclut event.category = "internal"), limit. Le champ hérité until_id reste disponible pour la compatibilité des 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>"
# Chaque événement conservé attribué à une exécution de SOP (sur l'ensemble de ses tours d'étape) :
curl $ZEROCLAW_GATEWAY/api/logs?sop_run_id=<run-id>
La pagination des journaux parcourt à rebours avec un curseur conscient des segments. Tant que at_end est faux :
- Privilégiez
next_segment_cursor, renvoyé sans modification commeuntil_segment_cursor. Traitez-le comme un jeton opaque : sa structure interne dépend du segment dans lequel la page se termine et peut changer d’une version à l’autre. Les clients doivent renvoyer la chaîne retournée telle quelle, sans la construire ni l’analyser. C’est le seul curseur qui peut avancer lorsque l’événement le plus ancien d’une page se trouve dans une archive de rotation. - Se rabattre sur
next_cursor_line_offsettransmis en tant queuntil_line_offsetlorsque le curseur de segment est absent. Il s’agit d’un décalage en octets simple dans le fichier actif, et il vautnulllorsque l’événement le plus ancien se trouve dans une archive. - La réponse héritée
next_cursor: [timestamp, id] | nullest conservée pour des raisons de compatibilité ; la renvoyer sous forme deuntil_ts+until_idest déprécié, car le départage lexicographique par ID peut ignorer silencieusement des événements ayant le même horodatage.
Recommencez à partir de la page la plus récente après avoir modifié les filtres. Considérez at_end: true comme le signal d’arrêter de demander des pages plus anciennes pour ce parcours.
at_end est limité aux segments que le démon a pu réellement lire. Lorsqu’un segment conservé ne peut pas être ouvert, ou cesse de se décoder en cours de fichier (par exemple en cas d’UTF-8 invalide), il est consigné dans le journal, la partie non lue est exclue de la vue fusionnée, et la réponse définit incomplete: true. La page est tout de même renvoyée, mais at_end signifie alors « aucun événement plus ancien parmi les segments qui ont pu être lus » plutôt que « aucun événement plus ancien n’existe ». Présentez un tel parcours comme partiel plutôt que complet. Il en va de même pour une recherche d’un événement unique : au lieu de signaler un échec par not found, le démon indique que l’événement n’a pas été trouvé et qu’une partie de l’historique conservé était illisible, si bien qu’il pourrait toujours exister. Un curseur de segment non résolvable suit la même règle : lorsque l’ancre qu’il désigne n’a pas pu être validée parce que son segment ne s’ouvrait pas ou ne se décodait pas, la page de fin d’historique porte également incomplete: true. Les anciens démons omettent ce champ ; son absence est traitée comme false.
until_line_offset est une position dans le fichier actif courant. La rotation des archives, la migration au démarrage et un changement de chemin configuré remplacent les octets ou le fichier actif auquel elle fait référence ; reprenez depuis la page la plus récente après ces limites.
until_segment_cursor reste résilient à travers ces frontières, par des moyens différents selon l’endroit où la page s’est terminée :
- Une page se terminant par une archive est adressée par l’identité propre de l’archive, laquelle est fixée lors de l’écriture de l’archive et n’est jamais réattribuée à un contenu différent. Les rotations ultérieures ne peuvent donc pas l’invalider. Si la rétention a depuis supprimé cette archive, le lecteur signale que l’historique est terminé plutôt que de reprendre silencieusement à une position sans rapport.
- Une page se terminant dans le fichier actif porte un identifiant d’événement d’ancrage en plus de l’offset, car le chemin du fichier actif reste stable alors que son contenu est remplacé à chaque rotation. À la reprise, le lecteur vérifie que l’événement situé à la limite du curseur correspond toujours à l’ancrage. En cas de non-correspondance, il recherche cet événement dans les segments conservés et reprend à l’endroit où il se trouve désormais, de sorte que la pagination traverse une rotation sans dupliquer ni sauter d’événements. Si l’événement ancré a totalement disparu, le lecteur signale que l’historique est terminé.
Un curseur émis par un démon plus ancien, qui désignait un segment par son nom de fichier sans ancre, reste accepté. Cette forme ne permet pas de savoir s’il désigne une archive rotée ou le fichier actif ; le lecteur essaie donc d’abord les archives, où un nom n’est jamais réattribué, puis se replie sur le fichier actif.
La réponse du journal inclut persistence_enabled: boolean, qui permet de distinguer « aucun événement correspondant » d’un daemon dont la persistance des traces d’exécution est désactivée. La réponse de /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 daemon » sans requête 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
Avec une build observability-otel, privilégiez le récepteur OTLP natif ; aucun parsing ni transformation de champs n’est nécessaire :
destinataires:
otlp:
protocoles:
http:
Point de terminaison: 127.0.0.1:4318
Pointez observability.otel_endpoint vers cet écouteur. Le récepteur filelog existant reste une solution de repli utile pour les builds par défaut, pour le rattrapage des JSONL conservés, ou pour l’exécution d’un expéditeur de fichiers séparé :
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-log/src/export_bridge.rs: pont d’événements canoniques non bloquant utilisé par l’exportateur natif de journaux OTLP.crates/zeroclaw-runtime/src/observability/otel_logs.rs: mappage du modèle de données OTel Logs et exportateur protobuf OTLP/HTTPcrates/zeroclaw-gateway/src/api_logs.rs: l’adaptateur HTTP.
Vérifiez la source avant de vous fier au texte de cette page.