
Claude CodeのOTelログをCloudWatchダッシュボードで可視化してみた
はじめに
データ事業本部のkasamaです。
今回は Claude Code の OpenTelemetry(OTel)テレメトリを CloudWatch Logs に直接送信し、チームの利用状況を CloudWatch ダッシュボードで可視化してみたいと思います。ダッシュボードは作っただけだと誰も見に行かなくなるので、週次の Slack ダイジェストを生成・投稿する Skill までセットで構築します。
概要
Claude Code には OpenTelemetry でメトリクスとログを送信する機能が組み込まれています。詳細は Anthropic 公式ドキュメントに記載されています。
一方 AWS 側も CloudWatch が OTLP エンドポイントをネイティブで提供しており、Bearer Token 認証を使うと AWS 外のワークロードから collector なしで直接ログを送信できます。
Claude Code / Cowork から CloudWatch Logs へ collector なしで OTel ログを送信する基本手順は、以下の記事で詳しく解説されています。本記事はこの構成を土台に、Sanitizer Lambda による機密除去とダッシュボード化、週次 Slack ダイジェストまでを追加します。
この 2 つを組み合わせると、OTel Collectorのコンテナや EC2 を常駐させることなく、各メンバーの Claude Code からチームの AWS アカウントへテレメトリを集約できます。追加リソースはサーバーレスの Sanitizer Lambda 1 本だけです。
アーキテクチャ

処理の流れは以下の通りです。
- 各メンバーの Claude Code が OTLP HTTPS + Bearer Token で CloudWatch Logs の OTLP エンドポイントに直接ログを送信
- Raw LogGroup(保持 1 日)にイベントが着信
- Subscription Filter 経由で Sanitizer Lambda が起動し、allowlist 方式で機密フィールドを破棄
- Sanitized LogGroup(保持 60 日)に書き戻し
- CloudWatch ダッシュボードと週次ダイジェスト Skill は Sanitized のみを参照
なぜ Raw と Sanitized の 2 段構成にするのか
Claude Code の OTel ログは OTEL_LOG_TOOL_DETAILS=1 を有効にすると、スキル名だけでなく Bash コマンド本文やファイルパスなどもツール詳細として送出されます。スキル別の利用集計にはスキル名が必要ですが、コマンド本文やパスはリポジトリ構造を含み得るため、チームの共有アカウントに長期保存したくありません。そこで Raw 側は保持 1 日に抑え、Lambda が許可フィールド(コスト、トークン数、モデル名、スキル名、subagent 種別、MCP サーバ名等)だけを Sanitized 側へ通す構成にしています。プロンプト本文はそもそも OTEL_LOG_USER_PROMPTS を設定しない限り <REDACTED> のまま送信されます。
既存の確認手段との違い
| 手段 | 対象 | 形態 |
|---|---|---|
| /usage コマンド | 個人 | セッション内でその場確認 |
| /insights コマンド | 個人 | ローカル HTML レポート |
| OTel + CloudWatch ダッシュボード(本記事) | チーム | 継続収集 + ダッシュボード + 週次 Slack |
個人の利用状況は組み込みコマンドで十分ですが、「チームの誰がどのスキル・サブエージェント・MCP をどれだけ使っているか」を継続的に見るには OTel での集約が必要になります。
制約
- CloudWatch Logs OTLP エンドポイントの Bearer Token 認証は US リージョン限定です(us-east-1、us-east-2、us-west-1、us-west-2)。テレメトリが米国保管になるため、データ所在地の要件がある場合は事前に確認してください
前提条件
- Claude Code がインストール済みであること
- AWS CLI v2、SAM CLI、Python 3.13、pytest がインストール済みであること
- デプロイ先 AWS アカウントの認証が設定済みであること(
aws ... --profile <your-profile>で実行できる状態)
実装
実装コードはGitHubに格納しています。
プロジェクトの構成は以下の通りです。
70_claude_code_otel_dashboard/
├── cfn/
│ └── claude-otel-logs.yml # LogGroup、IAM User、ダッシュボード
├── sam/
│ └── claude-otel-sanitizer/
│ ├── template.yaml # Sanitizer Lambda + Subscription Filter
│ ├── samconfig.toml
│ ├── src/handler.py # allowlist フィルタ
│ └── tests/test_handler.py
└── .claude/skills/claude-usage-digest/ # 週次 Slack ダイジェスト Skill
├── SKILL.md
├── references/
├── scripts/queries.txt # Logs Insights クエリ集
└── templates/digest-template.txt
CloudFormation
claude-otel-logs.yml では Raw / Sanitized の 2 つの LogGroup、送信専用 IAM User、ダッシュボードを定義しています。IAM User の権限は logs:CallWithBearerToken と Raw LogGroup への PutLogEvents / CreateLogStream のみに絞っており、仮に Bearer Token が漏洩しても書き込み以外は何もできません。
ダッシュボードは Sanitized LogGroup への Logs Insights クエリで構成しており、以下のウィジェットを含みます。
- Team totals: チーム全体の合計(コスト・メッセージ数・スキル / サブエージェント / MCP 利用回数)
- Daily cost per user: ユーザー別の日次コスト推移(可読性のため表示期間内のコスト上位 10 名のみ表示)
- Per-user activity: ユーザー別サマリ(セッション数、モデル別呼び出し、1 メッセージあたりコスト、キャッシュ率)
- Top skills / subagents / MCP servers / models: 何が誰に使われているかのランキング
ダッシュボード変数(variables)でテーブルのソート軸を切り替えられるようにしている点と、コスト・メッセージ・スキル回数を 1 つの stats で集計している点がポイントです。クエリの例として Per-user activity の骨子を抜粋します。
SOURCE '/demo-team/claude-otel-sanitized'
| filter ispresent(attributes.user.email)
| parse attributes.user.email /(?<user>[^@]+)/
| stats sum(coalesce(attributes.cost_usd, 0)) as cost,
count_distinct(attributes.session.id) as sessions,
sum(if(body = "claude_code.user_prompt", 1, 0)) as messages,
sum(if(body = "claude_code.skill_activated", 1, 0)) as skills,
...
by user
SAM(Sanitizer Lambda)
template.yaml では Sanitizer Lambda と Subscription Filter を定義しています。SAM の CloudWatchLogs イベントソースを使うと、Subscription Filter の作成と CloudWatch Logs への Lambda 実行許可が自動で構成されます。Lambda の IAM ポリシーは Sanitized LogGroup への書き込みのみで、Raw 側の読み取り権限は不要です(Subscription Filter がイベントをプッシュしてくるため)。
handler.py が allowlist フィルタの本体です。許可するフィールドを ATTRS / RES_ATTRS の集合で宣言し、それ以外(tool_input、prompt、bash_command 等)はすべて破棄します。tool_parameters だけは特別扱いで、Agent ツールの subagent_type と MCP ツールの mcp_server_name のみを抽出して通します。denylist ではなく allowlist にしているのは、Claude Code 側のアップデートで新しい属性が増えても安全側に倒れるようにするためです。
test_handler.py では実イベントを模したフィクスチャで PII(user.id、organization.id、request_id 等)が漏れないことを完全一致で検証しています。allowlist を変更する際は必ずテストも更新する運用です。
Skill(週次 Slack ダイジェスト)
ダッシュボードは作っただけでは見に行かなくなるため、週次で Slack に投稿するダイジェスト生成 Skill を用意しています。引数なしで直近 7 日の集計(チーム合計・誰が何のスキルを使ったか・ヘビーユーザー・MCP 利用)を Logs Insights から取得して Slack メッセージを組み立て、引数ありでは自然言語の利用分析(「先週コストが急増した原因を分析して」等)として動作します。
特徴は集計値だけでなく AI 考察を含める点です。考察の前に Claude Code 公式ドキュメント(changelog 等)を WebFetch でライブ取得し、「観測された使い方 → 公式の最新機能 → 次の一手」の流れで提案するため、ダイジェスト自体がチームへの活用促進になります。なお、ダイジェストは活用している人・スキルを称えるポジティブ枠のみとし、非活用者の名指しはしません(監視ではなく横展開が目的のため)。
スケジュール実行は Claude Desktop の routines で検討しましたが、Logs Insights クエリに使う IAM ロールが MFA を要求しヘッドレス実行と相性が悪いため、現時点では週次の手動実行にしています。
デプロイ
1. CloudFormation スタックのデプロイ
LogGroup、IAM User、ダッシュボードを作成します。
cd 70_claude_code_otel_dashboard
aws cloudformation deploy \
--stack-name demo-team-claude-otel-logs \
--template-file cfn/claude-otel-logs.yml \
--parameter-overrides ProjectName=demo-team LogRetentionDays=60 RawLogRetentionDays=1 \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1 \
--profile <your-profile>
2. Sanitizer Lambda のデプロイ
デプロイ前にユニットテストで allowlist の挙動と PII 漏洩がないことを確認します。
cd sam/claude-otel-sanitizer
pytest tests/ -v
sam build
sam deploy --profile <your-profile>
Lambda の実行ログ(/aws/lambda/demo-team-claude-otel-sanitizer)は SAM のデフォルトでは無期限保持になるため、デプロイ後に保持期間を設定しておきます。
aws logs put-retention-policy \
--log-group-name /aws/lambda/demo-team-claude-otel-sanitizer \
--retention-in-days 60 --region us-east-1 --profile <your-profile>
3. Bearer Token 認証の有効化と API Key 発行(手動)
ここは CloudFormation で完結できず、コンソールでの手動作業になります。LogGroup の BearerTokenAuthenticationEnabled プロパティは存在するものの、2026-04 時点では有効化が security token invalid エラーで失敗する事象があったため、本テンプレートではコンソールから有効化する手順にしています。
- CloudWatch コンソール(us-east-1)→ Log groups →
/demo-team/claude-otel→Actions→Enable bearer token authentication - IAM コンソール → Users →
demo-team-claude-otel-writer→Security credentials→API keys→Create API key(表示されたシークレットは再表示不可のため控えておく) - チーム配布用に SSM Parameter Store へ SecureString
/demo-team/claude-otel/bearer-tokenとして保存
4. 各メンバーの Claude Code 設定
各メンバーが ~/.claude/settings.json の env に以下を追記します。Bearer Token を含むため、リポジトリ側の .claude/settings.json には書かないでください。
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://logs.us-east-1.amazonaws.com",
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer <token>,x-aws-log-group=/demo-team/claude-otel,x-aws-log-stream=code",
"OTEL_LOG_TOOL_DETAILS": "1",
"OTEL_RESOURCE_ATTRIBUTES": "user.plan=pro"
}
}
OTEL_LOG_TOOL_DETAILS=1 を設定すると claude_code.skill_activated に実際のスキル名が記録され、ダッシュボードのスキル別集計が可能になります。OTEL_RESOURCE_ATTRIBUTES の user.plan は任意で、設定するとダッシュボードにプラン列が表示されます。
特定のリポジトリだけ送信を止めたい場合は、当該リポジトリの .claude/settings.json で CLAUDE_CODE_ENABLE_TELEMETRY を 0 に上書きすればオプトアウトできます。顧客機密リポジトリではチーム共有のためにリポジトリ側 .claude/settings.json に明示しておくと安全です。
試してみた
検証データの投入
今回は検証用に、自分の直近 7 日分の Claude Code 利用履歴(ローカルのセッションログ)から OTel イベントをRaw LogGroup に投入してパイプライン全体を動かしました。メールアドレスはサンプル値(kasama@example.com)に置換しています。
Sanitizer の動作確認
Raw LogGroup への投入後、数秒で Sanitized LogGroup に allowlist 通過後のイベントが書き込まれました。Sanitized 側のイベントは以下のような形で、ツール詳細やプロンプト関連のフィールドが落ちていることが確認できます。
{
"body": "claude_code.api_request",
"attributes": {
"user.email": "kasama@example.com",
"session.id": "<session-uuid>",
"model": "claude-opus-4-8",
"cost_usd": "0.1000455",
"input_tokens": "10",
"cache_read_tokens": "190383"
},
"resource": {
"attributes": {
"user.plan": "max-5x",
"service.name": "claude-code",
"service.version": "2.1.121"
}
}
}
ダッシュボードの確認
CloudFormation の Outputs に出力される URL からダッシュボードを開きます。先頭のテキストウィジェットに各ウィジェットの見方とカラムの意味(cost_per_msg_usd や cache_pct の解釈)を記載しているため、開いた人がその場で読み方を理解できます。

Team totals はチーム全体の合計です。

Daily cost per user はユーザー別の日次コスト推移です(可読性のためコスト上位 10 名のみ表示)。

Per-user activity では、ユーザーごとのコスト・セッション数・メッセージ数・スキル / サブエージェント / MCP 回数・モデル別呼び出し・キャッシュ率を 1 行で把握できます。検証データでは 7 日分で約 $888(Max プランの API 換算コスト。Max は定額制なので実課金額ではありません)、cache_pct は 99.9% とプロンプトキャッシュが効いた状態です。

Top skills / subagents / MCP servers / models のランキングです。daily-note 系や PR 作成系のスキルが上位に並び、サブエージェントは general-purpose が中心、MCP は Slack と Google Calendar が多いという、自分の使い方の傾向がそのまま出ています。




週次ダイジェストを Slack に投稿
/claude-usage-digest を引数なしで実行すると、Logs Insights で直近 7 日を集計し、公式 changelog を WebFetch した上で Slack メッセージを組み立てます。今回は Slack MCP 経由で自分の DM に送信しました。実際に投稿されたメッセージは以下の通りです。

AI 考察の「もっと活用するなら」が毎週の公式 changelog ベースで更新されるため、ダイジェストがそのままチームへの新機能の周知になります。
コスト
15 名が利用するチームで約 1 ヶ月(31 日間・1,238 セッション・7,121 メッセージ)運用した実測です。インフラ費(取り込み・Lambda・保存)は月 $0.2 弱でした。
| 項目 | 実測(15 名・31 日間) |
|---|---|
| 取り込み(Raw + Sanitized + Lambda ログ) | $0.17 |
| 保存(Sanitized ほか) | $0.001 |
| Lambda(約 4 万実行、ほぼ無料枠内) | $0.008 |
| 小計 | 約 $0.18 |
主な費目は取り込みで、Raw と Sanitized で 2 回課金されます(2 段構成の対価。Lambda は実行回数が多くても無料枠内)。
これとは別に Logs Insights のスキャン料金($0.005/GB)がかかります。ダッシュボードを開くたびに「ウィジェット数 × 時間範囲のデータ量」をスキャンする変動費で、上記 15 名・約 1 ヶ月では月 $0.26 でした。利用規模やダッシュボードの閲覧頻度が増えれば前提次第で数十ドル規模にもなります(例: 100 名が既定の 31 日レンジで週 1 回開く想定で月数十ドル)。時間範囲を短く(既定 31 日 → 7 日など)するのが一番効くコスト対策です。コレクターを常駐させない分、固定費はほぼゼロで従量課金だけで運用できます。
最後に
Claude Code の OTel テレメトリを CloudWatch Logs の OTLP エンドポイントに直接送信し、Sanitizer Lambda で機密を落としたうえでチームの利用状況をダッシュボード化し、週次 Slack ダイジェストの生成まで仕組み化してみました。コレクターレスで固定費がほぼかからないこと、allowlist 方式で機密が残らないこと、そしてダッシュボードを「見に行く」のではなくダイジェストが「届く」形にしたことで、チームの AI 活用状況の共有が運用として回るようになりました。参考になれば幸いです。








