
TeamプランのClaude Code の利用ログを OpenTelemetry で CloudWatch Logs に直接送って貯めてみた
リテールアプリ共創部のるおんです。
先日、Claude Code を Team プラン で使っている組織にて、「メンバーが Claude Code に何を頼み、どんな操作をさせたかを、AWS 上に貯めておいていつでも監査できるようにしておきたい」という相談をいただきました。
Claude Code には OpenTelemetry(OTel)で利用ログを外に出す仕組みがあります。そして CloudWatch Logs には、OTLP(OpenTelemetry の送信形式)のログを API キー(Bearer トークン)で直接受け取れる入口 があります。この2つを組み合わせると、 コレクターも Lambda も立てずに 、各メンバーの Claude Code から CloudWatch Logs へ直接ログを送れます。
この記事では、受け口のロググループを AWS CDK で作り、 claude.ai の管理画面(管理設定) から組織の全員に送信の設定を配って、実際にログが貯まるところまでを紹介します。
先に結論
- Claude Code の OTel のログは、 CloudWatch Logs の OTLP の入口(
https://logs.<リージョン>.amazonaws.com/v1/logs)へ直接送れます 。間にコレクターや ALB、Lambda は要りません - 認証は CloudWatch Logs の API キー を
Authorization: Bearerヘッダーで渡すだけです。送り先のロググループとログストリームもヘッダー(x-aws-log-group/x-aws-log-stream)で指定します - AWS 側に作るのは、 Bearer 認証を有効にしたロググループとログストリームだけ です。CDK なら数十行で書けます
- Team プランなら、 claude.ai の「組織の設定 → Claude Code → 管理設定」 に JSON を1つ貼るだけで、組織の全員の Claude Code に送信の設定が配られます
OTEL_LOG_USER_PROMPTS=1などを付けると、 プロンプトの本文・実行したコマンド・応答の本文 まで記録できます。既定では伏せられています
| 置き場所 | 用意するもの | 誰が作るか |
|---|---|---|
| AWS | ロググループ(Bearer 認証を有効化)・ログストリーム | CDK |
| AWS | CloudWatch Logs の API キー | マネジメントコンソールで発行 |
| claude.ai | 管理設定(送信先と API キーを書いた JSON) | 組織の Owner |
| 各メンバーの PC | なし(次に Claude Code を起動したときから送られる) | — |
構成
各メンバーの PC にも AWS 側にも、送信のための常駐プロセスはありません。Claude Code 自身が OTLP で直接送ります。
Claude Code の OpenTelemetry とは
公式ドキュメントでは、Claude Code の OTel 対応を次のように説明しています。
Track Claude Code usage, costs, and tool activity across your organization by exporting telemetry data through OpenTelemetry (OTel).
Claude Code が出すのは、 メトリクス (利用量やコストの時系列)と イベント (ログ)の2種類です。今回はイベントだけを使います。主なイベントは次のとおりです。
| イベント | いつ出るか |
|---|---|
claude_code.user_prompt |
プロンプトを送ったとき |
claude_code.assistant_response |
モデルから文の返事が来たとき |
claude_code.tool_decision |
ツールの実行を許可・拒否したとき |
claude_code.tool_result |
ツールの実行が終わったとき |
claude_code.api_request |
API を呼ぶたび(モデル名・推定コストつき) |
どのイベントにも user.email・session.id・organization.id などの属性が付くので、 誰の、どのセッションの操作か が分かります。
ただし、中身は既定では伏せられています。本文まで記録したいときは、次の環境変数で1つずつ有効にします。
| 環境変数 | 有効にすると記録されるもの |
|---|---|
OTEL_LOG_USER_PROMPTS=1 |
プロンプトの本文 |
OTEL_LOG_TOOL_DETAILS=1 |
ツールの引数(Bash のコマンド、MCP のサーバー名・ツール名など) |
OTEL_LOG_ASSISTANT_RESPONSES=1 |
応答の本文(未設定なら OTEL_LOG_USER_PROMPTS に従う) |
CloudWatch Logs の OTLP の入口とは
CloudWatch Logs には、HTTP の POST でログを直接送れる入口がいくつか用意されています。そのうちの1つが OpenTelemetry Logs の入口(/v1/logs)で、OTLP の JSON または Protobuf を受け付けます。
リクエストの形は次のとおりです。
POST https://logs.<リージョン>.amazonaws.com/v1/logsContent-Type: application/jsonまたはapplication/x-protobuf- 送り先は
x-aws-log-groupとx-aws-log-streamのヘッダーで指定する(OTLP の入口では、クエリパラメーターでの指定はできない) - 認証は SigV4 か、
Authorization: Bearer <API キー>
Claude Code の OTel の送信は、OTLP の送り先とヘッダーを環境変数で指定できます。SigV4 の署名はできませんが、 Bearer トークンのヘッダーなら付けられる ので、この入口とそのまま噛み合います。
やってみた
1. CDK でロググループを作る
受け口として、ロググループとログストリームを作ります。ポイントは、ロググループで Bearer 認証を有効にする ことです。これを忘れると、API キーで送っても受け付けてもらえません。
import * as cdk from "aws-cdk-lib";
import * as logs from "aws-cdk-lib/aws-logs";
import type { Construct } from "constructs";
export class ClaudeCodeLogsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const logGroup = new logs.LogGroup(this, "ClaudeCodeLogGroup", {
logGroupName: "/claude-code/otel",
retention: logs.RetentionDays.THREE_MONTHS, // 90日
removalPolicy: cdk.RemovalPolicy.RETAIN, // スタックを消してもログは残す
// 秘密鍵などを表示するときに伏せる(メールアドレスは伏せない。誰の操作か分からなくなるため)
dataProtectionPolicy: new logs.DataProtectionPolicy({
name: "claude-code-otel-masking",
identifiers: [
logs.DataIdentifier.AWSSECRETKEY,
logs.DataIdentifier.OPENSSHPRIVATEKEY,
logs.DataIdentifier.PGPPRIVATEKEY,
logs.DataIdentifier.PKCSPRIVATEKEY,
logs.DataIdentifier.PUTTYPRIVATEKEY,
],
}),
});
+ // API キー(Bearer トークン)での受け付けを有効にする。L2 にはまだ無いので L1 のプロパティを使う
+ (logGroup.node.defaultChild as logs.CfnLogGroup).bearerTokenAuthenticationEnabled = true;
const logStream = new logs.LogStream(this, "ClaudeCodeLogStream", {
logGroup,
logStreamName: "claude-code",
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
new cdk.CfnOutput(this, "LogGroupName", { value: logGroup.logGroupName });
new cdk.CfnOutput(this, "LogStreamName", { value: logStream.logStreamName });
new cdk.CfnOutput(this, "OtlpLogsEndpoint", {
value: `https://logs.${this.region}.amazonaws.com/v1/logs`,
});
}
}
BearerTokenAuthenticationEnabled は CloudFormation の AWS::Logs::LogGroup のプロパティで、aws-cdk-lib v2.272.0 では L1(CfnLogGroup)の bearerTokenAuthenticationEnabled として使えます。L2 の LogGroup には同じ名前のプロパティがまだ無いので、 node.defaultChild から L1 を取り出して設定しています。cdk synth すると、テンプレートに "BearerTokenAuthenticationEnabled": true が出ていることを確認できます。
あわせて、 データ保護ポリシー で、うっかりプロンプトに貼られた AWS のシークレットキーや秘密鍵を、ログを見るときに伏せるようにしています(logs:Unmask の権限があれば元の値も見られます)。なお、今回は OTLP で送られたログで実際に伏せられるところまでは確かめていません。既成の識別子にはメールアドレスもありますが、入れると user.email まで伏せられて誰の操作か分からなくなるので、あえて外しています。
デプロイは普段どおりです。
npx cdk deploy
2. CloudWatch Logs の API キーを発行する
次に、送信に使う API キーを発行します。マネジメントコンソールの CloudWatch → 設定 → ログ タブにある「API キー」から作れます。

「作成」を押すとこの画面になります。有効期限は 1・5・30・90・365日、任意の日付、無期限から選べます。

発行すると、コンソールが次の3つをまとめてやってくれます。
- API キー用の IAM ユーザーを作る
- マネージドポリシー
CloudWatchLogsAPIKeyAccess(logs:PutLogEventsとlogs:CallWithBearerTokenを含む)を付ける - その IAM ユーザーのサービス固有の認証情報として、API キーを発行する
API キーの値(シークレット)は 発行したときに一度だけ 表示されます。後から見直せないので、すぐに AWS Secrets Manager などに保存してください。
マネージドポリシーの代わりに自分でポリシーを書く場合は、 logs:PutLogEvents だけをロググループに絞り、 logs:CallWithBearerToken は "Resource": "*" にします。手元では、 logs:CallWithBearerToken までロググループの ARN に絞ると 403 になり、 * に戻したら通りました(AWS のドキュメントの例も * です)。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CallWithBearerToken",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "logs:PutLogEvents",
"Resource": "arn:aws:logs:ap-northeast-1:<アカウント ID>:log-group:/claude-code/otel:*"
}
]
}
3. まず手元で送ってみる
全員に配る前に、手元で送れるかを確かめます。
まずは curl で、OTLP の JSON を1件だけ送ってみます。API キーは Secrets Manager から変数に読み込み、画面には出さないようにしています。
TOKEN=$(aws secretsmanager get-secret-value --secret-id <API キーを保存したシークレット> \
--query SecretString --output text)
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
"https://logs.ap-northeast-1.amazonaws.com/v1/logs" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-H "x-aws-log-group: /claude-code/otel" \
-H "x-aws-log-stream: claude-code" \
-d '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"curl-test"}}]},"scopeLogs":[{"logRecords":[{"timeUnixNano":"'"$(date +%s)"'000000000","body":{"stringValue":"hello"}}]}]}]}'
200
200 が返り、ロググループに1件入りました。続いて、Claude Code 本体から送ってみます。環境変数をその場で付けて、ヘッドレス(claude -p)で1回だけ動かします。
env CLAUDE_CODE_ENABLE_TELEMETRY=1 \
OTEL_LOGS_EXPORTER=otlp \
OTEL_METRICS_EXPORTER=none \
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf \
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.ap-northeast-1.amazonaws.com/v1/logs \
OTEL_EXPORTER_OTLP_LOGS_HEADERS="Authorization=Bearer ${TOKEN},x-aws-log-group=/claude-code/otel,x-aws-log-stream=claude-code" \
OTEL_LOGS_EXPORT_INTERVAL=1000 \
OTEL_LOG_USER_PROMPTS=1 \
OTEL_LOG_TOOL_DETAILS=1 \
claude -p "echo hello-poc を実行して、出力だけを答えてください。" \
--model haiku --allowedTools "Bash(echo:*)" --max-turns 3
ポイントは OTEL_EXPORTER_OTLP_LOGS_HEADERS です。 Authorization と、送り先のロググループ・ログストリームのヘッダーを、カンマ区切りで1つの環境変数に入れます 。メトリクスは送らないので、 OTEL_METRICS_EXPORTER=none にしています。
数秒待つと、ロググループに user_prompt・api_request・tool_decision・tool_result などのイベントが入りました。実行したコマンドは、 tool_result の tool_parameters にこう残ります。
{"bash_command":"echo","full_command":"echo hello-poc"}
4. 管理設定で組織の全員に配る
手元で送れたら、同じ設定を組織の全員に配ります。Team / Enterprise プランでは、claude.ai の 組織の設定 → Claude Code → 管理設定(Managed settings) に JSON を書くと、各メンバーの Claude Code が起動時にそれを取りにいきます。編集できるのは、組織の Owner(または Primary Owner)です。
貼った JSON はこちらです。手順3の環境変数を、そのまま env に移しています。
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "none",
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "https://logs.ap-northeast-1.amazonaws.com/v1/logs",
"OTEL_EXPORTER_OTLP_LOGS_HEADERS": "Authorization=Bearer <API キー>,x-aws-log-group=/claude-code/otel,x-aws-log-stream=claude-code",
"OTEL_LOGS_EXPORT_INTERVAL": "5000",
"OTEL_LOG_USER_PROMPTS": "1",
"OTEL_LOG_TOOL_DETAILS": "1",
"OTEL_LOG_ASSISTANT_RESPONSES": "1"
}
}
管理設定の多くは起動中のセッションにも反映されますが、 OTel の送信の設定は、次に Claude Code を起動したときから効きます 。配ったら、メンバーには一度 Claude Code を起動し直してもらいます。
動作確認
ログはどう貯まるか
メンバーが Claude Code を使うと、ロググループに 1イベント1行 の JSON で貯まっていきます。 user_prompt の1件はこんな形です(値は伏せ、項目を抜粋しています)。
{
"resource": {
"attributes": {
"service.name": "claude-code-desktop",
"os.type": "darwin"
}
},
"scope": { "name": "com.anthropic.claude_code.events", "version": "2.1.299" },
"body": "claude_code.user_prompt",
"attributes": {
"event.name": "user_prompt",
"event.timestamp": "2026-10-21T06:11:23.408Z",
"user.email": "<メールアドレス>",
"session.id": "<セッション ID>",
"organization.id": "<組織 ID>",
"prompt.id": "<プロンプト ID>",
"prompt_length": "12",
"prompt": "<プロンプトの本文>",
"prompt_text": "<プロンプトの本文>"
}
}
resource.attributes.service.name を見ると、どこから使われたかが分かります。上の例はデスクトップアプリの Code タブから使われたときのものです。 prompt.id は、そのプロンプトから続いて出たイベント(API の呼び出し・ツールの実行など)すべてに同じ値が付くので、 1つの依頼で何が起きたか を後から束ねられます。
手元で確かめたイベントの中身は次のとおりです。
| イベント | 中身 |
|---|---|
user_prompt |
prompt と prompt_text の 両方 にプロンプトの本文 |
assistant_response |
応答の本文。 OTEL_LOG_ASSISTANT_RESPONSES=0 のときは <REDACTED> |
tool_decision / tool_result |
ツール名・許可したか・実行したコマンド(tool_parameters) |
api_request |
モデル名・推定コスト(cost_usd) |
Logs Insights で「誰が何を頼んだか」を見る
貯まったログは、CloudWatch Logs Insights で検索できます。例えば、誰がどんなプロンプトを送ったかの一覧はこう出せます。
fields @timestamp, `attributes.user.email` as user, `attributes.prompt` as prompt
| filter `attributes.event.name` = "user_prompt"
| sort @timestamp desc
| limit 100
属性名にドット(.)が入っているので、バッククォートで囲むのがポイントです。同じ要領で、 event.name を tool_result にすれば実行されたコマンドの一覧、 api_request にしてメールアドレスごとに cost_usd を合計すれば、メンバーごとの推定コストも出せます。
どこから使ったときに記録されるか
管理設定は Claude Code のクライアントが取りにいくものなので、記録されるのは Claude Code を使ったときだけ です。
| 使い方 | 記録 |
|---|---|
| ターミナルの Claude Code(CLI) | ✅ |
| VS Code の拡張機能 | ✅ |
| デスクトップアプリの Code タブ | ✅(service.name が claude-code-desktop) |
| claude.ai のクラウドのセッション | 管理設定は届くが、送信先へ出られるかは環境のネットワークの許可しだい(今回は未確認) |
| claude.ai のチャット・Cowork | ❌(Claude Code の OTel の対象外) |
Cowork には、管理画面の「データとプライバシー」に別の OTel の設定があります。チャットや Cowork まで含めて見たい場合は、別の仕組みを考える必要があります。
注意点・Tips
- API キーは組織の全員に配られます 。管理設定の
envは各メンバーの PC に届くので、メンバーなら API キーの値を見られます。権限をlogs:PutLogEvents(このロググループだけ)に絞り、 有効期限を付けて、定期的に入れ替える 運用にしておくのがおすすめです。入れ替えは、新しいキーを発行 → 管理設定を書き換え → 古いキーを無効化、の順で、ログを止めずにできます - 伏せるなら
promptとprompt_textの両方 。プロンプトの本文は2つの項目に同じ値で入ります。データ保護ポリシーや後段の処理で本文を伏せたいときは、両方を対象にしてください - 応答の本文は
OTEL_LOG_ASSISTANT_RESPONSESで決まる 。未設定だとOTEL_LOG_USER_PROMPTSに従います。プロンプトだけ残して応答は残したくない場合は、明示的に0を入れます - 組織の送り先が優先される 。管理設定は設定の中で一番強い階層なので、メンバーが手元で別の OTel の送り先を設定していても、管理設定の送り先が使われます
- ログの取り込みには料金がかかる 。本文まで記録すると1イベントが大きくなります。保存期間(今回は90日)とあわせて、取り込み量の見込みを立てておくと安心です
- ロググループは消えないようにする 。利用ログを貯める目的なので、CDK では
RemovalPolicy.RETAINにして、スタックを消してもログが残るようにしています - クライアント側の仕組みであることを理解しておく 。公式ドキュメントにもあるとおり、管理設定はクライアントが取りにいく設定で、管理されていない端末では利用者が回避できます。監査として厳密さが必要なら、MDM で配る方式(エンドポイントの管理設定)も検討してください
おわりに
Claude Code の OTel と CloudWatch Logs の OTLP の入口を組み合わせると、 ロググループ1つと API キー1つ で、組織の Claude Code の利用ログを自分たちの AWS に貯められました。AWS 側に動くものを置かないので、運用で気にするのは API キーの期限と入れ替えくらいです。
Team プランの管理設定を使えば、メンバーに設定をお願いして回る必要もありません。「AI に何を頼んでいるかを、組織として見えるようにしておきたい」という場面で、最初の一歩としてかなり手軽な構成だと思います。
以上、どなたかの参考になれば幸いです。
参考









