ai-machine-learning

LLMロギングのベストプラクティス:本番環境で守っている9ルール [2026]

著者: Mert Batur
Aug 2, 2026
2
LLMロギングのベストプラクティス:本番環境で守っている9ルール [2026]

LLMロギングのベストプラクティス:本番環境で守っている9ルール [2026]

ここで紹介する9つのLLMロギング ベストプラクティスは、私たちの本番スタックが実際に従っているルールです。4つのサービスで月間120万件のLLMリクエストをログに記録し、そのすべてが model、tokens、latency、cost_usd、trace_id を備えた1行のJSONとして Grafana Loki に届きます。レコードを書き込むのは structlog 25.4.0、その前にPIIを取り除くのが Presidio、そしてパイプライン全体はオブザーバビリティスタックのロギング半分を担っています。

要点

  • すべてのLLMリクエストは14以上の命名フィールドを持つ構造化JSONで記録し、フリーテキストでは絶対に記録しない。
  • PIIのマスクは Presidio などでログ書き込みの前に実施し、書き込んだ後に消そうとしない。
  • OpenTelemetry GenAI セマンティック規約の属性をすべてのトレースに付与する。
  • 1日100万件リクエストのとき、同じ60GBの月額コストは Datadog で108ドル、Loki で30ドル、ClickHouse で1.20ドル。

LLMロギングとは実際何を意味するのか(そして「全部記録すればよい」が失敗する理由)

LLMロギングとは、モデルへのリクエストとレスポンスのすべてを構造化レコードとして取得することです。プロンプト、補完結果、トークン数、レイテンシ、コスト、そしてこれらをユーザーセッションに結びつけるトレースまでを含みます。インフラのログとは別物です。CPU、メモリ、Podの再起動はメトリクススタックの管轄で、この記事が扱うのは、モデル挙動のデバッグ、コスト管理、監査を可能にするリクエストレベルの記録だけです。

「とにかく全部記録しろ」という発想は根強く、そして高くつきます。1日100万件のリクエストに対してプロンプトと補完結果を丸ごと保存すると、テキストは月あたりおよそ60GBに達し、そのかなりの部分が顧客のPIIとして無期限に保管されます。GDPRの第5条データ最小化原則は、個人データを「適切で、関連性があり、必要な範囲に限定する」ことを求めており、プロンプトの生ダンプは初日からこの基準に落ちます。全記録は戦略ではなく、月額請求書つきのリスクです。

9つのLLMロギングルールとは?

9つのルールを、私たちが導入する順番で挙げます。完全なプロンプトとレスポンスをハッシュ化識別子つきで記録する、構造化JSONを出力する、トークン数とコストをリクエストごとに取得する、OpenTelemetryのトレースコンテキストを付与する、書き込み前にPIIをマスクする、大量時にはサンプリングする、保持階層を設ける、セーフティイベントを分離する、そして結果をクエリ可能にする。以下では各ルールについて、それを支えるコードまたは表を添えています。

ルール1:完全なプロンプトとレスポンスを記録する(生PIIではなくハッシュで)

リクエストごとにプロンプトも補完結果も完全な形で記録してください。部分的なログこそが、モデルが実際何を見たのか記録ゼロのまま障害対応に臨む羽目になる原因だからです。唯一の例外は識別情報です。生のユーザーID、メールアドレス、氏名をレコードに書き込んではいけません。代わりにユーザーIDのSHA-256ハッシュを保存します。ハッシュがあれば、オフラインのルックアップで1人のユーザーの完全なセッション履歴を再構築でき、ログ行そのものは読む権限のない人間にとって無意味なままです。システムプロンプトも同じ理屈で、ハッシュ化してハッシュを記録し、平文はすでにバージョン管理されているプロンプトレジストリに置きます。

ルール2:構造化JSONを使う(全フィールドを命名し、フリーテキストをゼロに)

Pythonであれ他の言語であれ、LLMロギング ベストプラクティスの中で構造化ロギングだけは譲れません。JSONですべてのフィールドが命名、型付けされ、クエリ可能で、フォーマット済み文字列として投げ込まれるものが何もない状態です。INFO called gpt-4o, took 812ms のようなフリーテキスト行は grep しかできません。JSONレコードならモデル別の集計、コストの合計、トレースとの結合ができます。OpenAI自身の本番運用ベストプラクティスも同じ方向を向いており、print文ではなくSDKレイヤーで構造化メタデータを取得することを推奨しています。

以下が Techsy の全サービスが出力しているスキーマで、14フィールドあります。

json
{
  "request_id": "req_01J9XK4M7Q",
  "timestamp": "2026-07-18T09:24:31.482Z",
  "environment": "production",
  "model": "claude-sonnet-4-20250514",
  "prompt_tokens": 1284,
  "completion_tokens": 396,
  "latency_ms": 812,
  "cost_usd": 0.0098,
  "system_prompt_hash": "sha256:9f2c1a4e...",
  "user_id_hash": "sha256:b7e4d831...",
  "rag_sources": ["kb/pricing-2026.md", "kb/refund-policy.md"],
  "guardrail_result": "pass",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7"
}

3つのフィールドには補足が必要です。cost_usd はリクエスト時にトークン数とモデルの公表単価から計算され、夜間ジョブで後から埋めることは決してありません。2つのハッシュフィールドはルール1の妥協点で、オフラインでは相関可能、ログの中では不透明です。そして trace_idspan_id は W3C トレースコンテキストの値で、まさにルール4の主題そのものです。

4つのサービスがそれぞれプロバイダーを直接呼んでいると計測箇所も4つになる、と感じるなら、LiteLLM プロキシで一元化できます。すべてのプロバイダーの手前にロギングフックを1つ置けば済みます。

ルール3:トークン数とリクエストごとのコストを取得する

トークン使用量の追跡は、翌日走るウェアハウスのジョブではなく、ログ行そのものに含めるべきものです。すべてのプロバイダーはレスポンスに入出力トークン数を返します。その瞬間のモデルのトークン単価を掛けて cost_usd をレコードに書き込みます。単価は変わりますし、キャッシュ入力トークンと新規入力トークンでも異なります。そのため、静的な価格表で後からコストを計算すると、知らないうちに履歴が書き換えられます。すべての行にコストがあれば、「どの機能が重いのか?」は財務プロジェクトではなく1行のクエリで答えが出せるようになり、LLM APIコスト削減の取り組みにもそのままつながります。

ルール4:トレースコンテキストを付与する(OpenTelemetry GenAI Semconv)

トレースIDのないログ行は孤立しています。読めても、どのリトライで、どのRAGステップで、どのユーザーターンで生まれたのか特定できません。解決策は OpenTelemetry の GenAI セマンティック規約で、モデル呼び出しを計測するための標準属性名です。アクティブなスパンの中でログを出すと trace_idspan_id は自動で付与されるので、Grafana で1クリックすればトレースのウォーターフォールから生のレコードまで飛べます。

すべての gen_ai スパンに設定すべき属性です。

属性目的
gen_ai.systemstring"anthropic"プロバイダー名
gen_ai.request.modelstring"claude-sonnet-4-20250514"リクエストしたモデル
gen_ai.response.modelstring"claude-sonnet-4-20250514"実際に回答したモデル
gen_ai.usage.input_tokensint1284プロンプトサイズ
gen_ai.usage.output_tokensint396補完サイズ
gen_ai.response.finish_reasonsstring[]["stop"]生成が終了した理由
gen_ai.response.idstring"msg_01XK9..."プロバイダーのレスポンスID
python
from opentelemetry import trace

tracer = trace.get_tracer("ai-sdr")

with tracer.start_as_current_span("chat.completion") as span:
    span.set_attribute("gen_ai.system", "anthropic")
    span.set_attribute("gen_ai.request.model", "claude-sonnet-4-20250514")
    response = client.messages.create(**payload)
    span.set_attribute("gen_ai.usage.input_tokens", response.usage.input_tokens)
    span.set_attribute("gen_ai.usage.output_tokens", response.usage.output_tokens)
    span.set_attribute("gen_ai.response.finish_reasons", [response.stop_reason])
    log.info("llm.request", cost_usd=cost)  # Rule 2 record, trace IDs auto-attached

ルール5:ログ書き込み前にPIIをマスキングする

PIIマスキングは、レコードが書き込まれる前に実施する必要があり、書き込んだ後に掃除するものではありません。メールアドレスが一度 Loki に入れば、オブジェクトストレージのバックアップにも入ります。「後で削除しました」は GDPR の回答になりません。私たちの構成では、Microsoft Presidio を structlog のプロセッサーとして動かし、メールと電話番号の**94%**を Loki に届く前に捕捉しています。取りこぼしはほぼすべて特殊なフォーマットが原因で、見つかるたびにカスタムリコグナイザーとして追加しています。

フック全体でたった15行です。

python
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

PII_ENTITIES = ["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD", "IBAN_CODE"]

def redact_pii(text: str) -> str:
    results = analyzer.analyze(text=text, language="en", entities=PII_ENTITIES)
    return anonymizer.anonymize(text=text, analyzer_results=results).text

# In the structlog processor chain, before JSONRenderer:
# event_dict["prompt"] = redact_pii(event_dict["prompt"])

マスキングは入力フィルター、出力フィルターと同じパイプライン層に置かれ、同じ方法でテストされるべきです。私たちのガードレールパイプラインでは、ログに漏れたメールアドレスを運用上の注記ではなく、失敗した評価として扱います。

ルール6:大量トラフィックでは賢くサンプリングする

1日あたりおよそ10万件を下回るなら、すべて記録してください。それを上回ると、全量記録は二度と読まないデータへのストレージ税になり、サンプリングで重要なレコードだけを残すことになります。落とし穴があります。ランダムサンプリングはLLMトラフィックに対して最悪の選択肢だということです。失敗、拒否、5ドル級のリクエストは定義上まれなので、一律10%のサンプリングはまさにデバッグしたいイベントだけを捨ててしまいます。コイントスではなく、結果に基づいてサンプリングしてください。

戦略使う場面複雑度
ランダム(固定10%)安定トラフィックでのベースライン量指標
ルールベース特定のモデル、テナント、ルートを常に保持
テールベース遅い・高い・エラーのリクエストを保持し、通常を捨てる
トリガーベースガードレール発火や評価失敗時のみ完全コンテキストを保持
アダプティブトラフィック量に応じてサンプリング率が上下

よくある構成は、両端をルールベース(本番とエンタープライズテナントは常に記録)、中央をテールベースにするものです。このフレームワークに対するロギング固有の視点としては、guardrail_resultcost_usd フィールドがサンプリングシグナルそのもので、ルール2と8に従っていればすでに手元にあります。

ルール7:必要になる前に保持ポリシーを決めておく

ログ保持ポリシーは、落ち着いているうちに決めておくべき判断です。さもないと、倍のデータ量を前にしたコストレビューの最中に決めることになります。GDPRの第5条保存制限原則は、個人データを「必要な期間を超えて」保持しないことを求めており、実務上は段階的な保持を意味します。

階層保持期間ストレージユースケース
ホット7日Loki / ClickHouse ローカルディスクライブデバッグ、オンコールクエリ
ウォーム30日オブジェクトストレージ backed インデックス(S3)スプリントコスト分析、障害レビュー
コールド1年圧縮 S3/GCS アーカイブコンプライアンス要求、年次監査

ホットは「10分前に何が起きたか?」に速く、高く答えます。コールドは「3月にこの顧客に何を伝えたか?」に遅く、安く答えます。スケジュール通りに自動で削除しなければ、階層はただの図です。

ルール8:ガードレールとセーフティイベントは分けて記録する

セーフティイベント(ガードレールブロック、拒否、ポリシー違反)はテレメトリではなく監査レコードであり、専用のストリームに属します。理由は3つ。アラート:プロンプトインジェクションブロックの急増は誰かをページ呼び出しすべきで、100万件の定常行を相手にそのアラートをチューニングすることはできません。保持:コンプライアンス上、セーフティレコードはデバッグログより数年長く保存が求められることがあります。アクセス:監査人に渡すのはセーフティストリームであって、ファイアホース全体ではありません。判定はメインレコードにタグ付けし(guardrail_result: "block")、完全なレコードは別のストリームにルーティングします。何がセーフティイベントに該当するかはガードレールイベントのガイドで扱っています。

ルール9:ログを「保存するだけ」でなく「クエリ可能」にする

1分以内にクエリできないログは、オブザーバビリティシグナルではなくバックアップです。クエリ可能とは、インデックス済みフィールド、オンコール担当者が実際に知っているクエリ言語、そして障害の前に作ってあるダッシュボードを意味します。私たちは Loki を運用し、コスト異常、レイテンシリグレッション、「昨日テナントXで起きた拒否を全部見せて」のために週30回以上クエリしています。構文のリファレンスは Grafana Loki ドキュメントで、本当に効いてくるパターンはパース済みJSONフィールドを直接フィルタリングすることです。

logql
{service="ai-sdr"} |= "llm.request" | json
  | model = "claude-sonnet-4-20250514"
  | cost_usd > 0.05
  | line_format "{{.timestamp}} {{.request_id}} ${{.cost_usd}} {{.latency_ms}}ms"

5行で済み、ノートブックへのエクスポートは不要です。今のストアでこれができないなら、それが最初に直す問題です。

本番環境で実際に記録している内容

理論はこれくらいにします。以下は私たちの AI SDR パイプラインから、個人情報を伏せた設定です。冒頭の月間120万件リクエストという数字の裏にあるサービスで、structlog 25.4.0 が JSON にレンダリングし、Promtail 経由で Grafana Cloud Loki に送出しています。このパイプラインのモデルは claude-sonnet-4-20250514 で、すべての呼び出しがルール2と5のプロセッサーチェーンを正確に通過します。

python
import structlog

structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        redact_pii,          # Rule 5: Presidio, before serialization
        add_otel_trace_ids,  # Rule 4: trace_id + span_id from the active span
        structlog.processors.JSONRenderer(),
    ],
)
log = structlog.get_logger()

この構成で最初の四半期に出た数字を2つ。月間インジェスト量は4サービス合計で47GBに落ち着き、p95 のログ書き込みレイテンシは3ms、つまりパイプラインはリクエスト時間に測定できるものを何も載せていません。

元を取った設定変更:2026年3月にすべてのログエントリに cost_usd を追加しました。1週間以内に、リトライループで月340ドルを燃やしていたプロンプトテンプレートを1つ発見しました。一時的な API エラーが3回のリトライを誘発し、そのたびに4,000トークンのコンテキスト全体を再送していたのです。ログのおかげで1行のクエリで特定できました。リクエストごとのコストがなければ、次の四半期予算レビューで説明のつかない明細として浮かび上がっていたはずです。

LLMログのストレージコストはスケール時にどれだけかかるのか?

1日100万件のリクエストでは、LLMログのストレージコストは同じデータに対してストア次第で月あたりおよそ1.20ドルから108ドルの幅になります。計算はこうです。完全な構造化レコードは平均約2KBなので、1日100万件は1日2GB、つまり月60GBです。以下のベンダー公表価格(2026年7月時点)は、その60GBがよく使われる3つのバックエンドでいくらになるかを示しています。

バックエンド料金モデル(ベンダー公表、2026年7月時点)月60GB備考
Datadog LLM Observabilityインジェスト $0.10/GB + インデックス $1.70/GB約$108インデックスが高額な項目
Grafana Cloud Lokiオブジェクトストレージ経由で約$0.50/GB約$30セルフホストならさらに安い
ClickHouse(セルフホスト、S3)圧縮ストレージ約$0.02/GB約$1.20 + コンピューティング実際のコストはコンピューティング

出典:Datadog 価格Grafana LokiClickHouse オブザーバビリティドキュメント

2つ注意があります。これはベンダー価格からの私たちの試算であり、私たちが走らせたベンチマークではないからです。1つ目、Datadog の数字はすべてをインデックスする前提で、多くのチームは一部だけをインデックスしてずっと安く済ませています。一方 Loki と ClickHouse は主に保存量に対して課金されます。2つ目、セルフホスト ClickHouse の1.20ドルは実際の請求を隠しています。クラスターを動かすコンピューティングと、運用するエンジニアの時間です。月60GBなら、マネージドサービスがほぼ常にトータルで安い答えになります。セルフホストが合理的になるのは月およそ1TBを超えてからで、GBあたりの差が運用オーバーヘッドを上回る地点です。

この開きこそが要点です。1日100万件リクエストでは、Datadog のフルインデックスとセルフホスト ClickHouse の差はおよそ90倍で、同じ60GBに対して108ドル対1.20ドルです。ストアはアーキテクチャ設計時に選んでください。請求書が届いた後ではなく。

どのロギングツールを選ぶべきか?

ほとんどのチームにとって、選択肢は4つに絞られます。LLMネイティブプラットフォーム(Langfuse または LangSmith)、プロキシ層ツール(Helicone)、あるいはすでに運用しているインフラへ流す素の OpenTelemetry パイプラインです。表で扱っているのは実際に差がつく判断ポイントで、ダッシュボード、リプレイ、プロンプトバージョン管理は4つすべてで標準装備です。

LangfuseLangSmithHeliconeOTelネイティブ(Loki/ClickHouse)
セルフホスト可可(オープンソースコア)不可(SaaS)可(オープンソース)完全可
OTel互換可(OTLPインジェスト)一部(OTLPエクスポート)一部ネイティブ
コスト追跡自作(cost_usd を自前計算)
PIIマスキング内蔵不可(前処理)不可不可不可(ルール5に従い Presidio)
無料枠可(クラウド + セルフホスト)可(制限あり)ソフトは無料、インフラは自弁

私たちの意見を率直に言えば、OTelネイティブ + Loki で運用しています。他のすべてに Grafana スタックがすでにあり、データソースを1つ足す方が4社目のベンダー採用より勝ったからです。オブザーバビリティスタックがゼロからのスタートなら、Langfuse のトレーシングモデルと無料枠が有用な状態への最速ルートで、セルフホスト選択肢は出口を開いたままにします。LLMネイティブの2強で迷っているなら、Langfuse vs LangSmith の比較でフル比較をしています。ロギングがより大きなモニタリング判断の一部なら、プラットフォーム全体の比較が広い視野をカバーします。

最もよくあるLLMロギングの失敗とは?

私たちが目にしてきた壊れたLLMロギング構成の大半は、6つの失敗で説明がつきます。いずれもログ量が先に膨らむ前に気づけば、回避コストは低いものです。

  • PIIをマスキングせずに生で記録する。 最頻出かつ最高額です。サポートエクスポート1件、あるいは漏洩したバケット1つで、プロンプトログはデータ保護インシデントに変わります。読み取り時ではなく、書き込み前にマスクしてください(ルール5)。
  • 保持ポリシーがない。 無期限保存はどこでもデフォルトで、毎年静かに請求を倍にします。一度も削除しないなら、それはロギングシステムではなく、肥大化したアーカイブです。
  • 非構造化テキストログ。 grep しかできない print 出力はデモ規模では動きますが、1日10万件で崩壊します。「モデルXの失敗リクエストを全部探す」がクエリではなくシェルスクリプトの午後仕事になる瞬間です。
  • エラーだけを記録する。 成功リクエストはドリフト検知のベースラインであり、評価パイプラインの原材料です。勝ちも記録してください。量が強制するならサンプリングで構いません。
  • コストフィールドを無視する。 リクエストごとの cost_usd がなければ、コストアラートも機能別配賦もなく、上の本番セクションの月340ドルリトライループは四半期請求まで見えません。
  • トレースとの相関がない。 スパンから切り離されたログは、マルチステップエージェントのデバッグを推測ゲームにします。ログ行に trace_id が欠けているなら、ルール4が解決策です。

著者について

Mert Batur は Techsy.io の共同創業者で、チームは B2B クライアント向けに AI エージェント、自動化システム、音声/SDR パイプラインを提供しています。Techsy チームが本番環境で実際に使っている LLM ツールスタックについて執筆しています。LinkedIn でつながってください。

よくある質問

LLMリクエストごとに何を記録すべきですか?

最低限、PIIをマスクした完全なプロンプトと補完結果、モデル名、入出力トークン数、レイテンシ、USD建てコスト、ハッシュ化したユーザー識別子、そして OpenTelemetry のトレースIDとスパンIDです。パイプラインにその段階があるなら、RAGソースIDとガードレール判定も加えます。14の命名フィールド、リクエストごとに1行のJSONです。

LLMログに最適なフォーマットは?

構造化JSONで、リクエストごとに1オブジェクト、すべてのフィールドを明示的に命名します。フリーテキストログは grep しかできませんが、JSONレコードはモデル別集計、コスト合計、トレース結合が可能です。Python の structlog や Node の pino といった構造化ロガーでレコードを出し、JSONシリアライザーでレンダリングしてください。文字列フォーマットは使わないこと。

LLMログのPIIはどう処理しますか?

ログ書き込みの前にマスクし、後から処理しないこと。ロギングパイプラインの中で Microsoft Presidio のような検出器にプロンプトと補完結果を通し、氏名、メール、電話番号を <EMAIL_ADDRESS> のようなトークンに置き換えます。生のPIIがログストアに届いた時点でバックアップにも入っており、事後削除が GDPR の最小化基準を満たすことはほぼありません。

大規模運用時のLLMログストレージコストは?

1日100万件リクエスト、レコードあたり2KBで月約60GBの場合、Datadog のインデックス付き LLM Observability 価格で月約108ドル、Grafana Cloud Loki で月30ドル、セルフホスト ClickHouse では圧縮 S3 ストレージ月約1.20ドルにコンピューティングが加わります。いずれも2026年7月時点のベンダー公表価格で、セルフホストにはエンジニアリング時間が上乗せされます。

OpenTelemetry GenAIセマンティック規約とは?

LLM呼び出しを計測するための OpenTelemetry 標準属性名です。プロバイダーに gen_ai.system、モデルに gen_ai.request.model、トークン数に gen_ai.usage.input_tokens と output_tokens、生成停止理由に gen_ai.response.finish_reasons を使います。これらを使えば、Jaeger から Tempo、Langfuse まで、OTel互換バックエンドならカスタムパーサーなしでトレースを読み取れます。

トラフィックが多い場合、LLMログをどうサンプリングしますか?

すべてのエラー、すべてのガードレールブロック、コスト閾値を超えるすべてのリクエストを保持し、残りをサンプリングします。このテールベースのアプローチは実際にデバッグするまれなイベントを保持しますが、一律ランダムサンプリングは退屈なトラフィックと同じ割合でそれらを捨てます。1日10万件を下回るなら、サンプリング自体を飛ばしてすべて記録してください。

LLMログの保持期間はどれくらいが適切ですか?

段階化します。ライブデバッグ用にホット7日、障害レビューとコスト分析用にウォーム30日、コンプライアンスと監査用に圧縮オブジェクトストレージでコールド最大1年です。GDPR の保存制限原則は個人データを必要以上に長く保持することを禁じているため、各階層には手動クリーンアップではなく自動削除を紐づけてください。

LLMロギングとLLMトレーシングの違いは?

ログは1イベントのフラットなレコードで、このリクエストがこれらのフィールドで起きた、と記録します。トレースはリクエストパス全体にまたがるスパンの因果ツリーで、たとえば検索、モデル呼び出し、2つのツール呼び出し、という流れです。ログは「何を」を伝え、トレースは「どこで、なぜ」を伝えます。本番構成は両方を出力し、trace_id で結合します。

まとめ

振り返ります。すべてのリクエストを命名フィールドJSONで記録し、コストはリクエスト時に計算し、OTelトレースコンテキストを付与し、PIIは書き込み前にマスクし、1日10万件を超えたら結果ベースでサンプリングし、実際にクエリできるストアを選んでください。9つのルールは1スプリントに1つずつ導入できる順番にしてあり、中でもルール2、4、5が最も早く回収できます。ログを取り巻くより広いモニタリングスタックを選定中なら、まずAIオブザーバビリティプラットフォームのまとめから始めてください。LLMスタックの構造化ロギング配線で支援が必要なら、無料相談をご利用ください。

タグ

llmロギング ベストプラクティス構造化ロギングopentelemetry genaipiiマスキングllmオブザーバビリティログ保持

記事をシェアする

関連記事

その他の記事 ai-machine-learning

プロジェクトを始めよう

さあ、何かを作ろう。 特別なものへ?

ビジョンを、かたちに。変化を生むソフトウェアづくりは、私たちのチームにお任せください。