リファレンスと解説従来の運用ドキュメント

セッションデータが流れる経路

セッションイベントの保存先、秘密値を隠す場所、内容を読める人、Langfuseのサンプルコレクター

workerがエンジンから受け取ったセッションデータ(モデルの回答、ツール呼び出しと結果、権限リクエスト、状態の変化)は、DBのeventsテーブルに入ります。このテーブルが、機能が頼る正本です。このページでは、テーブルが何を持つか、途切れると何が壊れるか、秘密値をどこで隠すか、誰が内容を読めるかを説明し、Langfuseコレクターのサンプルを起動する方法を示します。 現在の監視(stdoutのJSONログ、/readyz、DB queryをDatadog Agentが収集)はDatadog監視基準にあります。トレースは、運用者が設定すると標準のOTLPで出力します(デフォルトはオフ。キーと有効化の方法)。有効にすると、ログのtrace_idとspan_idがアクティブなspanの中で入ります。

全体像

engine (Claude Agent SDK)
  -> worker
  -> API Worker Gateway          (events)
  -> PostgreSQL events table     (source of truth)
  -> SSE and Last-Event-ID reconnect, public API clients,
     turn finalization, pending-request notification

eventsテーブル

項目内容
持つものsystem・assistant・tool_use・tool_result・question・result・status・errorのイベント
使う場所公開APIのSSEクライアント、turn終了の判定、待機リクエストの通知
配信同じattemptの中で順番どおり、隙間なく保存されないとturnが終わらない
途切れると機能が壊れる。SSEが止まり、turnをfinalizeできない
内容の長さworkerは切り詰めない
届く先インストールしたPostgreSQL

イベントの種類とpayloadの契約は、公開APIスキーマのセッションイベント定義です。workerのClaude adapterが、エンジンのメッセージをこのイベントに変換します。assistantメッセージはブロックごとに1つのイベントになり、ツール結果ブロックは受け取ったままtool_resultイベントになります。

workerのイベント送信の段階が、イベントにattempt単位のsource_sequenceを付け、Worker Gatewayの/append-eventsへ順番に送ります。gatewayが受け取ったと答えるまでは、キューから消しません。APIはイベントをeventsテーブルに書きます。同じattemptの同じsequenceは、ユニークインデックスevents_attempt_sequence_uniqが1回だけ受け付けます。

このテーブルに頼る機能は次のとおりです。

  • SSEと再接続。 APIが、Last-Event-IDの次からこのテーブルを読み、ストリームとして送ります。使い方はAPIユースケースガイドの5節にあります。
  • turn終了の判定。 前のsequenceが欠けていればsequence_gap、finalizeのときにイベントの保存が足りなければevents_incompleteで拒否します。
  • 待機リクエストの通知。 質問・権限リクエストのquestionイベントと、セッションのneeds_input状態は、同じトランザクションでコミットされます。そのため、クライアントは質問を見る前にneeds_inputを受け取りません。答えはpending_requestsテーブルに残ります。workerがリクエストをどう閉じたかも、同じ行に残ります。settled_outcomeはanswered・expired・cancelledです。settled_reasonは、workerが取り消した理由(park・shutdown・interrupt・refused)です。settled_answer_resultは、ランタイムが答えをどう受け取ったか(applied・stale・unknown)です。人が拒否したリクエストはansweredで、answerのdecisionがdenyなので、取り消しと区別できます。
  • 状態遷移。 復旧・再開・一時停止・interruptのような制御の段階が、状態と一緒にコミットされます。
  • クライアントによる作業完了の判断。 公開APIのクライアントは、assistant・tool_use・tool_resultイベントを読んで、仕事が終わったかを判断します(APIユースケースガイド)。

コストと使用量の正本は、イベントではなくprovider_usageテーブルです。egress proxyがMessages応答から使用量を読み、APIがこのテーブルに書きます。

内容の収集と秘密値のマスキング

何を持つか

イベントは内容を持ちます。assistantのテキスト、ツール入力(tool_use)、ツール結果の本文(tool_result)、質問と権限リクエストの入力が入ります。

エンジン(Claude Code CLI)自体のtelemetryは有効にしません。エンジンに渡す環境変数はallowlistで、OTelの変数は1つも渡しません。

秘密値をどこで隠すか

現在の規則は3層です。

  1. エンジンのメッセージ。 Claude adapterのメッセージ整形の段階が、エンジンのメッセージごとに、機密性の高いキーの値、Bearer …のような秘密の形の値、password=…のような名前付きの値を[REDACTED]に置き換えます。イベントはこの結果から作ります。
  2. claimが渡した秘密値。 workerの秘密値を隠す段階が、このattemptが受け取った秘密値そのものを探して<redacted>に置き換えます。この処理は、イベントの送信と待機リクエストの登録に掛かります。エンジンがbase64のように再エンコードした値は捕まえられません。
  3. プロセスのログ。 ログのマスキング規則が、APIとworkerのstdoutログを隠します。egress proxyは同じ規則のコピーを使います。規則の一覧はAPIサーバーの「providerキーとリポジトリの認証情報はworkerに渡らない」の節にあります。この規則はログ用で、イベントには掛かりません。

1と3のパターン規則は、隠す範囲が違います。ログの規則sanitizeTextは、秘密の形の値やtoken=のような断片を1つでも見つけると、文字列全体を[REDACTED]に置き換えます。mapperは多くの場合、一致した部分だけを置き換え、name=valueの形で値の終わりが明確なら、値だけを隠します。ただし、mapperのhideNamedValuesも、password: …のようにコロンでつながる値や、終わりを区切れない値を見つけると、テキスト全体を隠します。そのため、password:を含むYAMLファイルをReadしたツール結果は、イベントから丸ごと消えます。2つのファイルのパターン一覧も、互いに異なります。

誰が内容を読めるか

セッションイベントは、sessions:read scopeを持つ主体がそのセッションを見られるときだけ読めます。まず、セッションが主体と同じ範囲にある必要があります。人は自分のworkspace、API keyはそのkeyのownerです。その中で、セッションの共有範囲がworkspace(一度も変えていないセッションの既定)ならworkspaceの全メンバーが読め、peopleやteamなら作成者と、指定された人・チームのメンバーだけが読めます。個人keyは、そのkeyを作った人として判定します。エージェントkeyはメンバーではないので、key自身として判定し(そのエージェントのセッションに限られます)、自分が作ったセッション(作成者のいないセッション)では、作成者の位置に立ちます。見られないセッションは404です。共有範囲の変え方と、ownerやadminによる監査閲覧は、APIサーバーの「セッションの共有範囲と監査閲覧」の節にあります。scopeはHTTPの境界で検査します。

保存期間

eventsの内容は削除しません。長く止まっているセッションの行はobject storeに移り、APIはそのobjectから同じレスポンスを読みます(APIユースケースガイドの410 CURSOR_EXPIREDの説明)。保存期間のポリシーはまだありません。イベント以外にプラットフォームが残すデータ全体の場所・保存期間・削除条件と、セッション1つを追って確認する手順は、プラットフォームが保持するデータにあります。

Langfuseのサンプルコレクターを起動する

infra/observability/にコレクターのサンプルが2つあります。Grafana LGTMを起動してtraceを探す方法はオブザーバビリティ (OTel)にあり、ここではLangfuseのサンプルを扱います。どちらのサンプルも、Werftのスタックとは別に起動するcompose projectで、手前にOpenTelemetry Collectorを1つずつ置いて、標準のOTLP/HTTPを受けます。1台のコンピューターでOTLPの受信を確認するための用途で、運用の推奨構成ではありません。どのコレクターを使い、どう運用するかは、運用者が決めます。

確認できる範囲。 この節のサンプルは、トレース出力をオフにした状態を前提にします。出力をオンにするとWerftがtraceを送りますが、この節で送るものではありません。この節で確認できるのは、サンプルのコレクターがOTLPのtraceを受け取って画面に表示するところまでで、送るtraceはsend-test-span.shが作る偽のspanです。

サンプルディレクトリOTLP/HTTPの受け口画面受けるシグナル
Langfuseinfra/observability/langfuse/http://127.0.0.1:25318http://localhost:25000トレース
Grafana LGTMinfra/observability/lgtm/http://127.0.0.1:26318http://localhost:26000トレース、ログ、メトリクス

ポートはすべてloopbackにだけ開き、既定のスタック(3000、3001、5432)と重なりません。データの流れは送る側 → collector(collector.yaml) → Langfuseまたはgrafana/otel-lgtmです。コレクターのcredentialを持つのはcollectorだけです。Langfuseのcompose内の秘密値の既定値(local-…で始まる値と例のキーペア)は、ローカル確認用のプレースホルダーです。このコンピューターの外で使うには、次の変数をすべて新しい値で渡します。LANGFUSE_POSTGRES_PASSWORD、LANGFUSE_CLICKHOUSE_PASSWORD、LANGFUSE_REDIS_PASSWORD、LANGFUSE_MINIO_PASSWORD、LANGFUSE_SALT、LANGFUSE_ENCRYPTION_KEY(openssl rand -hex 32で作った64文字)、LANGFUSE_NEXTAUTH_SECRET、LANGFUSE_INIT_USER_EMAIL、LANGFUSE_INIT_USER_PASSWORD。パスワードとアカウントはvolumeを最初に作るときに決まるので、すでに起動した後なら、down -vで消してから起動し直します。collectorのキーペアは、下の節のように別に変えます。

起動と停止

リポジトリのルートで実行します。Langfuseは初回の起動で、DB migrationのため1〜2分かかります。

docker compose -p werft-obs-lgtm -f infra/observability/lgtm/compose.yml up -d --wait
docker compose -p werft-obs-langfuse -f infra/observability/langfuse/compose.yml up -d --wait

止めるときは、次を実行します。-vは保存したトレースも消します。

docker compose -p werft-obs-lgtm -f infra/observability/lgtm/compose.yml down -v
docker compose -p werft-obs-langfuse -f infra/observability/langfuse/compose.yml down -v

テストspanを送る

infra/observability/send-test-span.shで偽のトレースを1つ送ります。werft.turnの下にwerft.llm_requestとwerft.toolがあり、turn spanにはユーザー入力を、モデル応答のspanには回答テキストを、gen_ai.*属性として載せます。--with-logを付けると、同じtraceに結び付いたログも1行送ります。Langfuseのサンプルのcollectorはログを受けないので、Langfuseには付けません。

infra/observability/send-test-span.sh http://127.0.0.1:26318 --with-log
infra/observability/send-test-span.sh http://127.0.0.1:25318

出力のtrace_idとsession_idで、下の画面から探します。

Langfuseのプロジェクトキーを作る

Langfuseのサンプルは、最初の起動時にLANGFUSE_INIT_*の設定で、組織Werft local、プロジェクトwerft-local、例のキーペア、ログインアカウントを作ります。ログインアカウントの既定は、メールadmin@example.comとパスワードlocal-passwordです。collectorは同じ例のキーペアでLangfuseに送ります。

別のキーを使うには、次のようにします。

  1. http://localhost:25000にログインし、Settings > API Keys > Create new API keysでキーペアを作ります。
  2. 2つの値を環境変数で渡して、collectorだけを作り直します。値をファイルに書かないでください。
LANGFUSE_PUBLIC_KEY='<public key>' LANGFUSE_SECRET_KEY='<secret key>' \
  docker compose -p werft-obs-langfuse -f infra/observability/langfuse/compose.yml up -d --no-deps otel-collector

画面で探す

Langfuse. http://localhost:25000に例のアカウントでログインし、Tracingでtrace idを使って探します。直接開くには、http://localhost:25000/project/werft-local/traces/<trace_id>へ行きます。セッション単位で見るには、Sessionsでsession_idを探します。サンプルのテストspanでLangfuse 4.66.0で確認したマッピングは次のとおりです。

spanの属性Langfuseの画面
gen_ai.input.messagesInput
gen_ai.output.messagesOutput
session.idSessions
gen_ai.request.model、gen_ai.usage.input_tokens、gen_ai.usage.output_tokensGeneration種別の観測。モデル名とtoken数が付きます
gen_ai.operation.name=execute_tool、gen_ai.tool.nameTool種別の観測。名前はツール名です
その他の属性(werft.*、resource属性)Metadata

Grafana. http://localhost:26000はログインなしで開きます。トレースは、Explore → Tempo → TraceQLにtrace idを入れて探します。1つのセッションのturnをすべて見るには、{ span.werft.session_id = "<session_id>" }で検索します。ログは、Explore → Lokiで{service_name="werft-api"}で探します。

このページ