ログと観測性
ZeroClaw が発行するすべてのイベントは、単一のクレート zeroclaw-log を経由します。このクレートは、ディスク上の JSONL スキーマ、ダッシュボードが読み取るプロセス内ブロードキャストストリーム、型付き Observer(Prometheus / OTel)へのオプションのブリッジ、およびサブシステムが呼び出すマクロ(record!、scope!、spawn!)を管理します。
このページでは、オペレーターが必要とする情報を扱います。設定、ログの保存場所、イベントの構造、そしてそれらをクエリする方法です。
設定([observability])
デフォルト: log_persistence = "rolling", log_persistence_max_entries = 200, log_tool_io = "redacted", log_tool_io_truncate_bytes = 40960, log_llm_request_payload = "off"。新規インストールでは ~/.zeroclaw/data/state/runtime-trace.jsonl に 200 イベントのローリング JSONL が生成され、ダッシュボードの Logs ページは追加の設定なしで動作します。
log_persistence = "none" は永続化を完全に無効化しますが、ダッシュボード SSE で使用されるブロードキャストストリームは遮断しません。オプションの型付き Observer ブリッジも永続化から独立していますが、明示的にバインドされた場合にのみ正規のログイベントを受信します。現在の本番ブートストラップでは、そのバインディングはインストールされません。
永続化はベストエフォートであり、トランザクション型の監査保証ではありません。Observer ブリッジ(バインドされている場合)とブロードキャスト配信は、イベントが容量制限付きのバックグラウンドライターキューに投入される前に行われます。キューが満杯になるか、ワーカーの書き込みに失敗すると、イベントが JSONL に記録されない可能性があります。定期的な同期は現在のアクティブファイルを対象としますが、新しい UTC 日の最初の追記前に行われる日次ローテーションや、しきい値を超える追記後に行われるサイズローテーションでは、アクティブファイルを先に同期せずに名前変更することがあります。そのため、この同期間隔は、ローテーション直後のアーカイブの耐久性を上限付きで保証するものではありません。ロギングアーキテクチャで、異なる配信契約について説明しています。
アーカイブのローテーション (log_persistence = "rotating")
rotating は full と同様に、バックグラウンドライターが受け付けたイベントに対してエントリ数によるトリミングを行いませんが、ZeroClaw はアクティブファイルを管理します。サイズおよび/または日次の境界に達すると、アクティブファイルはタイムスタンプ付きアーカイブにローテーションされ、古いアーカイブは件数と経過期間に基づいて削除されます。これは、アクティブファイルから古いエントリをトリミングする rolling とは異なります。ローテーションされたイベントは、後の診断のためにアーカイブファイルに保持されます。
| キー | デフォルト | 効果 |
|---|---|---|
log_persistence_max_bytes | 0 | 追記によってアクティブファイルがこのバイト数以上になった時点で、一度ローテーションを行います。0 を指定するとサイズによるローテーションが無効になります。 |
log_persistence_rotate_daily | true | 新しいUTC日の最初のイベントの前に、最終書き込みがそれより前の日に行われたファイルをアーカイブします。 |
log_persistence_retention_max_files | 7 | 最大でこの数のアーカイブを保持します。ローテーション後、上限を超えた最も古いものが削除されます。0 はすべてを保持します。 |
log_persistence_retention_max_age_days | 0 | ローテーション後にこの日数より古いアーカイブを削除します。0 は経過日数ベースのクリーンアップを無効にします。 |
アーカイブはアクティブなファイルの隣に配置され、その拡張子を保持したまま、ソート可能な UTC スタンプが拡張子の前に挿入されます。たとえば、runtime-trace.jsonl は runtime-trace.20260624-031500.jsonl にローテートされます。ダッシュボードと /api/logs エンドポイントはアクティブなファイルのみを読み取るため、アーカイブはライブクエリの対象ではなく、オフライン検査用のディスク上の記録です。
日次ローテーションはUTCカレンダーを基準とするため、その境界は他のタイムゾーンのローカル深夜と一致しない場合があります。これらのキーは log_persistence = "rotating" でない限り無視され、none、rolling、および full モードは変更されません。
GenAI スパン属性 (observability-otel)
llm.response スパンは、OTel GenAI メッセージコンテンツ属性 gen_ai.input.messages、gen_ai.output.messages、gen_ai.system_instructions(JSON 文字列エンコード)を保持しており、これらは Langfuse/Tempo の Input/Output/System ペインに反映されます。
プライバシーとコスト。 キャプチャされたコンテンツはベストエフォートでサニタイズされます: インライン画像データは省略され、既知の認証情報の形式(key=value、bearer、および
sk-/ghp_/xoxb--style のプレフィックス)はリダクションされます。これはすべてのシークレットまたは PII の削除を保証するものではありません。会話に機密情報が含まれる可能性がある場合は、アクセス制御されたトレースバックエンドを優先してください。キャプチャのコストは O(prompt size) エージェントループのイテレーションごと です(成長する履歴が各ラウンドで再スキャンされます)。またフルテキストは span ごとのペイロードに比例して増加します。バイト単位のバックエンドでは、属性を削除するのではなく、エクスポーター側で切り捨てを適用してください。
OTel コンテンツキャプチャ
OTel コンテンツキャプチャは、ログベースのキャプチャ (log_tool_io, log_llm_request_payload) とは独立しています。OpenTelemetry スパン属性として出力されるコンテンツを制御します。
生成AIコンテンツ
gen_ai.system_instructions、gen_ai.input.messages、および gen_ai.output.messages を OTel スパン上で制御します。
[observability]
otel_genai_content = "off" # off | redacted | full
otel_genai_content_max_chars = 1000 # フィールドごとの切り詰め上限
off(デフォルト): コンテンツ属性なし、メタデータのみ。redacted: コンテンツはリークスキャンされ、フィールドごとにmax_charsで切り詰められます。full: コンテンツはリークスキャンされますが、切り詰められません。
ツール I/O
OTel スパン上の gen_ai.tool.arguments、input.value、gen_ai.tool.result、および output.value を制御します。
[observability]
otel_tool_io = "off" # off | redacted | full
otel_tool_io_max_chars = 1000 # フィールドごとの切り詰め上限
off(デフォルト): コンテンツ属性なし、ツール名 + 結果のみ。redacted: コンテンツはリークスキャンされ、フィールドごとにmax_charsで切り詰められます。full: コンテンツはリークスキャンされますが、切り詰められません。
動作に関する注意事項
*_max_chars = 0を設定すると、そのポリシーではoffと同じ意味になります。- コンテンツは常に切り詰めの前にスクラブされます(認証情報パターン + シークレットパターン)。
- 切り詰めはツール引数の JSON 構造を保持します(末端の文字列が切り詰められます)。
- 切り詰められたフィールドには
…[truncated {n} of {total} chars]マーカーが付きます。このマーカーはメタデータであり、max_charsにはカウントされません。保持されるコンテンツは正確にmax_chars文字で、その上にマーカーが付加されます。 - デフォルト
offは、以前の動作からのプライバシー優先の変更です(機能ゲート付きだが、有効時は常にオン)。 - コンテンツポリシーは、プロセスではなくオブザーバー/設定インスタンスにバインドされます。プロセスグローバルな OTel コンテンツポリシーは存在しません。各
OtelObserverは構築時にObservabilityConfigからイミュータブルなコンテンツ設定を導出し、OTel エクスポート境界でそれを参照します。同一プロセス内の複数のオブザーバーはそれぞれ独立したポリシーを保持します。後から作成されたオブザーバーが、先に作成されたオブザーバーのプライバシー設定を上書きしたり無効化したりすることはできません(後勝ちも、オブザーバー間のドリフトも発生しません)。
ターン入れ子メモリおよびRAGスパン(observability-otel)
memory.recall、memory.store、rag.retrieve の各スパンは、操作が属性付きのエージェントターン内で実行されると常に gen_ai.agent.invoke ターンスパンの下にネストされます。これにより、1回の完全なターン(メモリのリコール、自動保存によるストア、LLM呼び出し、ツール呼び出し)が Langfuse/Tempo で1つのトレースとして表示されます。これら3つのイベントは、LLMイベントやツールイベントと同じ channel / agent_alias / turn_id の3つ組を持ち、zeroclaw.channel、gen_ai.agent.name、zeroclaw.turn_id のスパン属性として公開されます。
相関付けられたターンの外側でのメモリ操作は、引き続きルートスパンを生成します: gateway REST メモリストアと、process_message hardware-RAG 取得です。これはターンのブラケットが開く前に実行されるため、一致する zeroclaw.turn_id 属性を持つルートスパンのままになります (そのスパンの完全なネストは #8844 で追跡されています)。ライブターンに一致しなくなった turn_id も、親を推測するのではなくルートスパンにフォールバックします。
LLM リクエストペイロードのキャプチャ (log_llm_request_payload)
log_llm_request_payload は、llm_request イベントが messages_count に加えて送信されるプロンプトと会話を記録するかどうかを制御します。これはデフォルトで無効であり、プライバシーに関わる部分です。有効にすると、ZeroClaw はターンごとにシステムプロンプト全体と会話履歴全体を保存します。
| 値 | キャプチャされる内容 |
|---|---|
off(デフォルト) | messages_count のみ。メッセージの内容は記録されません。既存の動作。 |
redacted | 完全なメッセージ履歴(role + content)。raw_response およびツール I/O に使用されるものと同じ scrub_credentials パスでクレデンシャルスキャンされ、その後 log_tool_io_truncate_bytes で切り詰められます。切り詰めは request_messages_truncated と request_messages_original_bytes でフラグ付けされます。 |
full | redacted と同じ認証情報の除去を行いますが、切り詰めは行いません(リプレイの忠実性を確保し、raw_response を反映します)。 |
redacted と full はどちらも常に資格情報のスクラビングを実行します。両者の唯一の違いは切り捨てです。キャプチャは 2 つ目の上限を導入するのではなく、既存の log_tool_io_truncate_bytes の上限を再利用します。キャプチャを即座に無効にするには、log_llm_request_payload = "off" に設定するかそのままにしてください。再デプロイは不要です。
ディスク上のフォーマット
JSONL: 1行に1イベント、UTF-8、Unix 上では 0o600 パーミッション。ホットパスはノンブロッキングです: record_event はシリアライズされたイベントを有界チャネル経由で専用のバックグラウンドスレッド(zeroclaw-log-writer)に渡し、即座に返します。ワーカーは定期的な間隔で sync_all を呼び出します: 100回の書き込みごと、またはウォールクロック時間で1秒ごと(いずれか先に到達した方)、加えて通常シャットダウン時にチャネルが閉じる際の最終的な sync_all。これにより、イベントごとの耐久性(以前の同期的な動作)を有界な書き込みレイテンシと引き換えにします: プロセスがクラッシュすると、最大で1同期間隔分の保留中の書き込みが失われる可能性があります。ワーカーが遅延した場合、record_event は非同期ランタイムをブロックするのではなく、tracing::warn! でイベントをドロップします。ワーカーはプロセスごとのシングルトンです。init_from_config 経由で永続化を無効化して再有効化すると、古いワーカーが破棄され(チャネルのクローズにより最終同期とスレッド終了がトリガーされる)、新しいワーカーが生成されます。
Line形状は zeroclaw_log::event::LogEvent をミラーします。トップレベルのキー:
| キー | タイプ | 注釈 |
|---|---|---|
id | UUID v4 文字列 | 永続的なイベント ID。 |
@timestamp | RFC 3339 + ミリ秒、UTC | 辞書順でソート可能です。リーダーはこれを基準にソートします。 |
severity_number | u8 | OTel: 1 TRACE、5 DEBUG、9 INFO、13 WARN、17 ERROR。 |
severity_text | string | severity_number のバケットラベル。 |
event.category | string | agent、channel、cron、memory、tool、provider、session、system、または internal。 |
event.action | string | 安定した識別子 (llm_request、channel_message_inbound、…)。 |
event.outcome | string | 省略 | success、failure、unknown(unknown の場合は省略)。 |
service.name | string | Constant "zeroclaw". |
service.version | string | 実行中のデーモンの Crate バージョン。 |
trace_id | 16進数文字列 | 省略 | ターンごとの相関。1つのエージェントターン = 1つの trace_id。 |
span_id | 16進数文字列 | 省略 | ターン内のサブスパン。 |
zeroclaw.* | フラットな文字列マップ | エイリアスにバインドされた属性(以下を参照)。 |
message | string | 省略 | 人間が読み取り可能な行本文。 |
attributes | object | omitted | アクションごとの自由形式のペイロード。 |
schema_version | u8 | 現在は 2。v1 の行は起動時にインプレースで移行されます。 |
zeroclaw.* 属性
信頼できる情報源となるRustの定義は、crates/zeroclaw-log/src/event.rs内のATTRIBUTION_FIELDSとCOMPOSITE_PREFIXESです。/api/logsのレスポンスは正規のリストをattribution_keysとして保持しているため、ハードコードする代わりにこれを取得してください。
プレーンフィールド(ATTRIBUTION_FIELDS)はそれぞれ単一の文字列を保持します。複合プレフィックスには3つのキーが付与されます: <prefix>、<prefix>_type、<prefix>_alias(例: channel = "discord.glados"、channel_type = "discord"、channel_alias = "glados")。フィルターは粗いマッチングと精密なマッチングのいずれにも対応できます。
トレース呼び出しが複合プレフィックスフィールドを(. を含まない)裸の型に設定した場合、_type スロットのみが設定されます。これにより、すでに完全な <type>.<alias> 複合を保持しているスパン内での tracing::*!(model_provider = name, …) 呼び出しが、リーフ→ルートのマージ時にそれを上書きしてしまうことを防ぎます。
クエリの実行
ダッシュボードのLogsページがメインの操作画面です。その内部では以下のように動作します。
GET /api/logs
トップレベルフィルター(クエリパラメーター): since_ts、until_ts、until_line_offset、action、category、outcome、severity_min、trace_id、q(message + attributes にわたる部分文字列)、hide_internal(event.category = "internal" を除外)、limit。レガシーフィールド until_id は、タイムスタンプ/IDカーソルの互換性のために引き続き使用可能です。
他のすべての ?<key>=<value> は属性ごとの等価フィルターとして扱われ、ゲートウェイは is_attribution_field に対してキーを検証し、不明なものは 400 で拒否します。レスポンスには attribution_keys: string[] が含まれるため、呼び出し元が推測する必要はありません。
例:
sh
# デーモン起動以降のすべてのWARN+イベント。
curl "$ZEROCLAW_GATEWAY/api/logs?severity_min=13"
# 特定のエージェントのイベント:
curl "$ZEROCLAW_GATEWAY/api/logs?agent_alias=glados"
# 1つのボットのDiscordトラフィック:
curl "$ZEROCLAW_GATEWAY/api/logs?channel=discord.glados"
# 単一のエージェントターン:
curl "$ZEROCLAW_GATEWAY/api/logs?trace_id=<value-from-a-prior-event>"
ログのページネーションはバイトオフセットカーソルを使って後方へ辿ります。at_end が false の間は、非 null の next_cursor_line_offset を同じ非カーソルフィルターとともに until_line_offset として渡し直すことで、新しいバイトを再読み込みせずに古いイベントを読み込めます。フィルターを変更した後は、最新のページから再開してください。at_end: true は、そのページネーションの走査において古いページの要求を停止する合図として扱ってください。従来の next_cursor: [timestamp, id] | null レスポンスは互換性のために残されていますが、その timestamp/ID のペアを until_ts および until_id としてページネーションに使用することは非推奨です。辞書順の ID タイブレークにより、同一の timestamp を持つイベントが警告なくスキップされる可能性があるためです。
until_line_offset は現在アクティブなファイル内の位置であり、永続的なイベントチェックポイントではありません。追記のみの場合はその位置が維持されますが、ローリングトリム、アーカイブのローテーション、起動時の移行、設定されたパスの変更によって、それが参照するバイト列またはアクティブなファイルが置き換えられます。これらの境界を越えた後は、古いオフセットを再利用せず、最新のページから再開してください。/api/logs はアクティブなファイルのみを読み取ります。より古いローテーション済み履歴が必要な場合は、タイムスタンプ付きアーカイブを直接調べてください。
/api/status のレスポンスには daemon_started_at: string (RFC 3339) が含まれているため、ダッシュボードは追加のラウンドトリップなしで「デーモン起動時から」をデフォルトにできます。
外部ログビューア
JSONLスキーマはOTelログとECSのハイブリッドです。@timestamp、severity_number + severity_text、event.{category,action,outcome}、service.{name,version}、attributesに加え、zeroclaw.*ベンダー名前空間が含まれます。ほとんどのログビューアは、ほぼ変換なしでこれを取り込めます。以下の例では、<install>をインストールディレクトリの絶対パス(通常は~/.zeroclawを展開したもの)に置き換えてください。
Grafana Loki
Promtail のラベルは agent_alias、channel、severity_text を抽出し、Grafana でフィルター可能にします:
scrape_configs:
- ジョブ名: ゼロクロー
static_configs:
- ターゲット: [localhost]
ラベル:
ジョブ: ゼロクロー
__path__: <install>/data/state/runtime-trace.jsonl
pipeline_stages:
- json:
式:
エージェント: zeroclaw.agent_alias
チャンネル: zeroclaw.channel
level: severity_text
- ラベル:
エージェント:
チャンネル:
level:
- タイムスタンプ:
source: @timestamp
フォーマット: RFC3339
OpenTelemetry Collector
filelog レシーバーはスキーマを直接マッピングします。その後、任意の OTel シンク(Tempo、Honeycomb、Datadog など)にエクスポートできます:
レシーバー:
filelog/zeroclaw:
include: [<install>/data/state/runtime-trace.jsonl]
演算子:
- 型: json_parser
タイムスタンプ:
parse_from: attributes["@timestamp"]
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
重要度:
parse_from: attributes.severity_number
Kibana / Elastic
取り込みはそのまま機能します。厳格な ECS パイプラインでは、severity_text の代わりに log.level が必要です。severity_text を log.level に(さらに severity_number を log.syslog.severity.code に)リネームする Filebeat 取り込みパイプラインによって、このギャップを埋められます。@timestamp と event.{category,action,outcome} は既に正規の位置に配置されています。
Vector / Fluent Bit
どちらも JSON パーサーステージで JSONL を tail します。任意のバックエンドへ送信する前のスキーマ変換は不要です。
ターミナル形式
デーモンの stderr フォーマッタは、各行の先頭に、最も近い外側のエイリアスにバインドされた識別子を付加します:
- エージェントコンテキスト →
[<agent_alias>] - channel専用のコンテキスト(channelリスナー、まだagentなし)→
[<channel_composite>](例:[discord.glados]) - otherwise →
[system]
スパンチェーンは次のとおりです: channel_listener{channel=discord.glados}: …。スパンフィールドはインラインで表示されます。
スキーマ移行
起動時、log_persistence が有効でファイルが存在する場合、ライターは最初の追記の前に、すべての schema-1 行をインプレースマイグレーションによって schema-2 へストリーミング処理します。これは純粋なストリーミング処理であり、ファイルサイズに関係なく1行分のメモリ割り当てに制限されます。マイグレーション後のファイルはアトミックにリネームされて配置されます。すでに v2 のファイルはそのまま変更されません。
移行に失敗した場合、デーモンは warn をログに記録し、v2 の追記書き込みを継続します。古い v1 の行は、v1 をまだ解釈できるツールでは引き続き読み取り可能ですが、v2 リーダーのデシリアライザーは通過しません。
internalとは何ですか?
event.category = "internal" は、オペレーターがデフォルトではダッシュボードに表示する必要のない運用ノイズ用のバケットです。ハートビートのティック、アイドル状態のブロードキャスト、ロスのある同期リトライなどが該当します。ダッシュボードの「内部を非表示」トグル(デフォルトで有効)がこれらをフィルタリングします。
フォレンジック調査でその存在が重要であるものの、通常は存在しないことが正常な状態である高頻度イベントがある場合に使用します。本物のエラーに対する量の調整役として使用しないでください。
関連ファイル
crates/zeroclaw-log/src/event.rs: 正規のLogEvent形式。crates/zeroclaw-log/src/layer.rs: すべてのtracing::*呼び出しをキャプチャし、パイプラインに渡すtracing-subscriberLayer。crates/zeroclaw-log/src/macro.rs:record!、scope!、spawn!。crates/zeroclaw-log/src/writer.rs:追記、ローリングトリム、およびアーカイブローテーション。crates/zeroclaw-log/src/reader.rs:/api/logsリーダー。crates/zeroclaw-log/src/config.rs:StoragePolicy,ToolIoPolicy,ResolvedPolicy。crates/zeroclaw-log/src/migrate.rs:schema-1 → schema-2 のストリーミングマイグレーション。crates/zeroclaw-log/src/observer_bridge.rs: Prometheus / OTel コンシューマー向けの型付きObserverプロジェクション。crates/zeroclaw-gateway/src/api_logs.rs: HTTP アダプター。
このページの文章を信頼する前に、ソースを確認してください。