Advanced (更新: 2026/6/2)

Claude Codeでログ監視基盤を作る実践ガイド

構造化ログ、OpenTelemetry、アラート、監査ログをClaude Codeで安全に設計する方法。

Claude Codeでログ監視基盤を作る実践ガイド

最初に決めるべきこと

Claude Codeに「ログを入れて」とだけ頼むと、たいていはconsole.logが増えます。開発中はそれでも動いたように見えますが、本番障害では役に立ちません。必要なのは、誰のどの操作が、どのリクエストIDで、どのサービスを通り、どの時点で遅くなったかを追える観測設計です。

OpenTelemetryの説明では、可観測性は外側からシステムを理解し、未知の問題にも答えられるようにする考え方です。ログ、メトリクス、トレースは別々のものではなく、同じリクエストを別角度から見るための信号です。公式のOpenTelemetry Observability primerWhat is OpenTelemetry?を前提にして、この記事ではClaude Codeへどんな制約を渡すかまで落とし込みます。

Masaが小さな決済APIで試したとき、最初の失敗は「ログを詳しく」という曖昧な依頼でした。Claude Codeはエラー原因を残そうとして、リクエストボディ全体をログに出す実装を提案しました。カード番号は入っていませんでしたが、メールアドレス、クーポン、配送先が混ざる危険がありました。そこで、PII、つまり個人を識別できる情報は残さない、requestIdtraceparentだけを相関キーにする、アラートはログ文字列ではなくメトリクスで作る、というルールに変えたらレビューがかなり楽になりました。

信号使いどころClaude Codeに渡す制約
ログ何が起きたかを読むJSON、固定フィールド、PIIマスク
メトリクスどれくらい悪いかを見るrate、p95、エラー率を集計
トレースどこで遅いかをたどるtraceparentを伝播しspanを作る
ヘルスチェック依存先の死活を確認依存先ごとにlatencyを返す

Claude Codeに渡す安全なプロンプト

Claude Codeの公式ドキュメントでは、CLAUDE.mdはプロジェクトや組織の指示を毎回読ませるためのファイルです。詳しい使い方はCLAUDE.mdベストプラクティスで扱っていますが、ログ監視では「何を記録しないか」を先に書くのが重要です。権限境界はClaude Code permissionshooksも確認してください。

Claude Code task:
- Add observability to the checkout API only.
- Keep all changes inside src/checkout and tests/checkout.
- Use structured JSON logs with requestId and traceparent.
- Never log passwords, tokens, cookies, email, phone, address,
  raw prompt text, or full request/response bodies.
- Add tests proving redaction and requestId propagation.
- Add a /healthz report with database and cache latency.
- Add alert rules for 5xx rate, p95 latency, and redaction failure.
- Show a diff summary and remaining manual checks at the end.

このプロンプトの狙いは、Claude Codeの出力を「実装」だけでなく「レビュー可能な運用差分」にすることです。CLAUDE.mdには、ログレベル、フィールド名、禁止フィールド、テストコマンド、ダッシュボード名、オンコール引き継ぎの形式まで置いておくと、次回の修正でもぶれません。

構造化ログとリクエストID

OWASPのLogging Cheat Sheetは、ログをセキュリティレビュー、テスト、アクセス制御の対象にすること、ログ注入やディスク枯渇などの失敗も検証することを求めています。Claude Codeに頼むときも、ログは「後から読むテキスト」ではなく「本番データを扱う機能」と考えます。

まずは依存ライブラリなしで動く最小のJSONロガーです。structured-logger.mjsとして保存し、Node.js 18以上でnode structured-logger.mjsを実行できます。

import { randomUUID } from "node:crypto";

const rank = { debug: 10, info: 20, warn: 30, error: 40 };
const current = process.env.LOG_LEVEL || "info";
const threshold = rank[current] ?? rank.info;

const secretKeys = [
  "password",
  "token",
  "authorization",
  "cookie",
  "set-cookie",
  "apikey",
];

function cleanText(value) {
  return String(value).replace(/[\r\n\t]/g, " ").slice(0, 500);
}

function redact(value) {
  if (Array.isArray(value)) return value.map(redact);
  if (!value || typeof value !== "object") return value;

  return Object.fromEntries(
    Object.entries(value).map(([key, item]) => {
      if (secretKeys.includes(key.toLowerCase())) {
        return [key, "[REDACTED]"];
      }
      return [key, redact(item)];
    }),
  );
}

export function log(level, message, fields = {}) {
  if ((rank[level] ?? 99) < threshold) return;

  const entry = {
    ts: new Date().toISOString(),
    level,
    service: process.env.SERVICE_NAME || "checkout-api",
    env: process.env.NODE_ENV || "development",
    requestId: fields.requestId || randomUUID(),
    msg: cleanText(message),
    ...redact(fields),
  };

  process.stdout.write(`${JSON.stringify(entry)}\n`);
}

log("info", "payment accepted", {
  requestId: "req_demo_001",
  userId: "user_123",
  amount: 4980,
  token: "sk_live_should_not_leak",
});

ExpressなどのWebアプリでは、requestIdをレスポンスヘッダーにも返します。W3CのTrace Contextは、traceparentがない場合は新しいtrace IDを作り、ある場合は引き継ぐ処理モデルを示しています。自前のx-request-idと標準のtraceparentを混同しないのがコツです。

import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import type { Request, Response, NextFunction } from "express";
import { log } from "./structured-logger";

type RequestContext = {
  requestId: string;
  traceparent?: string;
  userId?: string;
};

const storage = new AsyncLocalStorage<RequestContext>();

export function getRequestContext() {
  return storage.getStore();
}

export function requestContext(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const started = performance.now();
  const user = (req as Request & { user?: { id?: string } }).user;
  const requestId =
    req.get("x-request-id") ||
    req.get("cf-ray") ||
    randomUUID();

  const context = {
    requestId,
    traceparent: req.get("traceparent"),
    userId: user?.id,
  };

  res.setHeader("x-request-id", requestId);

  storage.run(context, () => {
    res.on("finish", () => {
      const durationMs = Math.round(performance.now() - started);
      const level = res.statusCode >= 500
        ? "error"
        : res.statusCode >= 400
          ? "warn"
          : "info";

      log(level, "http request completed", {
        requestId,
        method: req.method,
        path: req.path,
        statusCode: res.statusCode,
        durationMs,
      });
    });

    next();
  });
}

ログレベルは、debugを本番で常時出さない、infoは正常な重要イベント、warnは回復可能な異常、errorは人間が調査する状態、と定義します。Claude Codeには「既存のエラー処理を消さない」「ログで制御フローを変えない」「失敗時にログ出力がアプリを落とさない」と明記してください。

OpenTelemetryでつなぐ

OpenTelemetryはベンダー中立の計装レイヤーで、バックエンドそのものではありません。つまり、Jaeger、Prometheus、Grafana、Datadogなどへ送る前に、アプリ側の信号をそろえる役割です。Node.jsの設定は公式のJavaScript Node.js getting startedJavaScript exportersを確認しながら更新してください。

npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-proto \
  @opentelemetry/exporter-metrics-otlp-proto \
  @opentelemetry/sdk-metrics
const opentelemetry = require("@opentelemetry/sdk-node");
const {
  getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node");
const {
  OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-proto");
const {
  OTLPMetricExporter,
} = require("@opentelemetry/exporter-metrics-otlp-proto");
const {
  PeriodicExportingMetricReader,
} = require("@opentelemetry/sdk-metrics");

process.env.OTEL_SERVICE_NAME ||= "checkout-api";

const endpoint =
  process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
  "http://localhost:4318";

const sdk = new opentelemetry.NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: `${endpoint}/v1/traces`,
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: `${endpoint}/v1/metrics`,
    }),
    exportIntervalMillis: 30000,
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

process.on("SIGTERM", () => {
  sdk.shutdown().finally(() => process.exit(0));
});

運用で見る流れは次のようになります。概念図を先に置くと、Claude Codeにも「どこに何を送るのか」を説明しやすくなります。

flowchart LR
  A["利用者の操作"] --> B["アプリ"]
  B --> C["構造化ログ"]
  B --> D["メトリクス"]
  B --> E["トレースspan"]
  C --> F["ログ基盤"]
  D --> G["アラート"]
  E --> H["トレース基盤"]
  F --> I["インシデント引き継ぎ"]
  G --> I
  H --> I

ヘルスチェックとアラート

ヘルスチェックは単なる200 OKでは足りません。DB、キャッシュ、外部APIなど依存先ごとに、成功、失敗、遅延を返します。ただし、秘密情報や接続文字列は絶対に返しません。

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("timeout")), ms);
  });
}

export async function buildHealthReport(checks) {
  const started = Date.now();
  const results = {};

  for (const [name, check] of Object.entries(checks)) {
    const before = Date.now();
    try {
      await Promise.race([check(), timeout(800)]);
      results[name] = {
        status: "ok",
        latencyMs: Date.now() - before,
      };
    } catch (error) {
      const message =
        error instanceof Error ? error.message : String(error);
      results[name] = {
        status: "fail",
        latencyMs: Date.now() - before,
        reason: message.slice(0, 120),
      };
    }
  }

  const failed = Object.values(results)
    .filter((item) => item.status === "fail")
    .length;

  return {
    status: failed ? "degraded" : "ok",
    uptimeSec: Math.round(process.uptime()),
    totalLatencyMs: Date.now() - started,
    checks: results,
  };
}

アラートは「1件のerrorログ」ではなく、一定時間の率や分位点で作ります。以下はPrometheus形式の例です。環境に合わせてラベル名は調整してください。

groups:
  - name: checkout-api
    rules:
      - alert: CheckoutHigh5xxRate
        expr: |
          sum(rate(http_requests_total{
            service="checkout-api",
            status_code=~"5.."
          }[5m]))
          /
          sum(rate(http_requests_total{
            service="checkout-api"
          }[5m])) > 0.02
        for: 10m
        labels:
          severity: page
        annotations:
          summary: "Checkout 5xx rate is above 2%"

      - alert: CheckoutP95LatencyHigh
        expr: |
          histogram_quantile(
            0.95,
            sum by (le) (
              rate(http_request_duration_seconds_bucket{
                service="checkout-api"
              }[5m])
            )
          ) > 1.5
        for: 15m
        labels:
          severity: ticket
        annotations:
          summary: "Checkout p95 latency is above 1.5s"

3つの具体的なユースケース

1つ目はECの決済APIです。orderIdrequestIdpaymentProvideramountは残しますが、カード番号、メールアドレス、住所、アクセストークンは残しません。アラートは5xx率、決済失敗率、外部決済プロバイダのp95遅延で分けます。障害時は、ログから注文番号を見つけ、トレースで決済プロバイダ呼び出しを確認し、メトリクスで全体影響を見る流れになります。

2つ目はSaaSの管理画面です。ログイン、権限変更、メンバー招待、請求プラン変更は監査ログとして残します。ただし、招待メール本文や個人メモを出す必要はありません。Claude Codeには、監査ログをアプリログと分けること、管理者IDと対象ユーザーIDを別フィールドにすること、RBACテストを追加することまで依頼します。

3つ目はメディアサイトやブログCMSです。記事公開、CTAクリック、問い合わせフォーム送信、画像生成失敗、翻訳未完了を追います。PVだけを見ると収益改善にはつながりません。cta_clickgenerate_leadを分け、Claude Code分析実装と合わせてダッシュボードを確認します。

マイクロサービスがある場合は、マイクロサービス設計も合わせて読んでください。サービスごとにservice.namedeployment.environmentがぶれると、せっかくのOpenTelemetryも検索しづらくなります。

失敗例と落とし穴

よくある失敗は5つあります。1つ目は、エラー原因を知りたいあまり、リクエストボディを丸ごと出すことです。2つ目は、ログメッセージが自由文で、検索条件が毎回変わることです。3つ目は、requestIdtraceIdを別名で乱立させることです。4つ目は、ヘルスチェックが依存先を見ずに常にokを返すことです。5つ目は、アラートを作っただけで、誰が何を見るかを決めていないことです。

Claude Code特有の落とし穴もあります。本番ログをそのまま貼ると、プロンプト内に個人情報や秘密情報を入れてしまいます。障害調査を頼むときは、事前にマスクしたログ、集計済みメトリクス、短いtrace ID一覧だけを渡してください。安全な進め方はClaude Code permissions guideにもつながります。

インシデント引き継ぎは次のようなJSONで残すと、チャットでもチケットでも再利用できます。

{
  "incident_id": "INC-2026-06-02-001",
  "severity": "SEV2",
  "owner": "oncall-api",
  "customer_impact": "Checkout errors for some card payments",
  "first_seen": "2026-06-02T09:15:00+09:00",
  "request_ids": ["req_7f3a", "req_8b21"],
  "trace_ids": ["7bba9f33312b3dbb8b2c2c62bb7abe2d"],
  "dashboards": ["Checkout API overview"],
  "current_hypothesis": "Payment provider latency spike",
  "actions_taken": ["Disabled checkout_v2 feature flag"],
  "next_checks": ["Compare p95 latency by region"],
  "do_not_do": ["Do not paste raw customer data into prompts"]
}

ダッシュボードレビューとCTA

ダッシュボードは作って終わりではありません。週1回、上位5件のエラー、p95遅延、ログ量の増加、アラートの空振り、PIIマスクの失敗を見ます。月1回は、実際の障害チケットから「このログで原因にたどり着けたか」を逆算します。ここをClaude Codeにレビューさせるときは、ダッシュボード画像ではなく、メトリクス名、クエリ、アラート条件、最近の障害要約を渡すほうが正確です。

個人で始めるなら無料チートシートで日常の確認コマンドを固定し、テンプレート化したい場合は商品一覧を見てください。チームでログ設計、CLAUDE.md、権限、CI、インシデント運用までまとめて整えるなら、Claude Code研修・導入相談から実リポジトリ前提で相談できます。

この記事で紹介した内容を実際に試した結果、最も効いたのは「禁止フィールドを先に書く」ことでした。structured-logger.mjsではトークンが[REDACTED]になること、改行が1行に正規化されることを確認しました。さらに、ヘルスチェックでキャッシュ失敗を模擬するとdegradedになり、インシデントJSONにrequestIdtraceIdを入れるだけで、レビュー時の会話がかなり短くなりました。

#Claude Code #ログ監視 #OpenTelemetry #構造化ログ #可観測性
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。