
こんにちは、技術戦略部CTOブロックの塩崎です。
当社ZOZOには1人あたり月額200ドルの基準のもと、Claude CodeやGemini CLIをはじめとした各種AI開発ツールを利用可能にする制度を2025年7月にスタートさせました。
現在ではこの制度を用いて数百名という非常に多くの社員がClaude Codeを利用しています。このような中で組織全体のAI活用を推進するためには、それぞれの社員や部署のClaude Codeの利用状況をモニタリングすることが重要です。そのためにClaude CodeのOpenTelemetry機能を利用して、全社員のClaude Code利用状況を収集したので、本記事ではその手法を紹介します。
ccusageを使った利用情報の収集の課題
Claude Codeの利用情報を収集する方法と言いますと、まずccusageを思い浮かべる人が多いかと思います。
当社でも最初はこのccusageを利用しようとしましたが、課題に遭遇しました。まず利用者にccusageを実行してもらうという点が課題でした。ccusageはコマンド一発で利用状況を出力でき、プログラムから扱いやすい構造化されたJSON出力もサポートしています。そういう意味で非常に便利なツールではあるものの、数百名の社員から漏れなくccusageの出力結果を回収しようとすると手間がかかります。さらにこの作業は1回だけ実施すればOKというものではなく、継続的なモニタリングのためには都度ccusageを回収する必要もあります。
実際に全社員からccusageを集めるということを1回実施してみましたが、これを定期的に実施することは運用負荷が高いという結論になりました。数名から十数名の組織であれば定期的なccusageの収集が十分現実的に実施できるかもしれませんが、ZOZOの規模感では厳しい結果になりました。
Claude CodeのOTel機能の紹介
ccusageの代わりに注目した機能が、Claude CodeのOpenTelemetry出力機能です。
LLM APIのコールやユーザーのプロンプト入力などのイベントを設定したエンドポイントに対してOpenTelemetry仕様で送信する機能です。なお、入力したプロンプトは、プライバシーを考慮して文字数のみを取得して本文は取得していません。
この機能を用いてClaude Codeの利用情報を収集すれば、前述した課題が解決できると考えました。以降では収集するための仕組みを解説します。
作ったものの全体像紹介
まずは構築した仕組みの概要を紹介します。

Claude Codeから送信された利用情報はGoogle Cloudで動作しているCloud Runに送られ、最終的にBigQueryに格納されます。上の図からも分かるように利用情報を送信する部分・受け取る部分・分析する部分という3つのコンポーネントからなっているため、順番に解説していきます。
利用情報を送信する部分
まずは、利用情報を送信する部分を解説します。
各自の環境で動いているClaude CodeにOpenTelemetryの設定を入れています。全社員に対して設定を入れるように依頼をしたとしても、どうしても漏れが生じてしまうため、そのような依頼ベースの手法に頼らず、ファイルを配布することを考えます。ZOZOはMDMツールとしてIntuneを利用しているため、Intuneの仕組みを使って以下のパスにJSONファイルを配置しました。
Windows: C:\Program Files\ClaudeCode\managed-settings.json macOS: /Library/Application Support/ClaudeCode/managed-settings.json
この場所に配置したJSON設定ファイルはManaged settingsと呼ばれ、優先順位が最も高い設定ファイルとして認識されます。
そのため、以下のような内容のファイルを配布し、全社員のClaude CodeにOpenTelemetryの設定を追加しています。基本的には公式ドキュメントの通りの設定なので詳細な解説は省略しますが、Resource Attributeだけは少々工夫をしました。AWS Bedrockをモデルプロバイダーとして利用している時に利用者のメールアドレスが取得できなかったため、Resource Attributeにメールアドレスを入れるような設定を追加しています。また、OpenTelemetry情報を受け取るサーバーに認証を設定しているため、そのための認証トークンも埋め込んでいます。
{ "env": { "CLAUDE_CODE_ENABLE_TELEMETRY": "1", "OTEL_METRICS_EXPORTER": "otlp", "OTEL_LOGS_EXPORTER": "otlp", "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://<OpenTelemetry エンドポイント>", "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer <認証トークン>", "OTEL_RESOURCE_ATTRIBUTES": "user.email=<会社メールアドレス>", "OTEL_METRICS_INCLUDE_VERSION": "true" } }
利用情報を受け取る部分
次にOpenTelemetry情報を受け取る部分を説明します。Cloud Runの周りのアーキテクチャ図をより詳細に書くとこのようになります。

図からGoogle Cloudをメインにした構成であることが分かります。ZOZOは分析基盤としてBigQueryを活用しており、最終的にBigQueryに情報を格納すると便利なため、Google Cloudをメインとしています。AWSやSnowflakeなどに分析基盤を持っている方は、それらの中にClaude Codeの利用情報も入れると既存のアセットをうまく活用できます。AWSの上で似たような仕組みを構築する場合は、以下のドキュメントなどが参考になるかと思います。
(2026-03-16 追記)
また、DatadogもOpenTelemetry情報を受け取ってダッシュボード化する機能を提供しているので、Datadogを導入している方はこちらも参考になるかと思います。
www.datadoghq.com (2026-03-16 追記ここまで)
Claude Codeから送信されたOpenTelemetry情報はCloud Load Balancingで受け取ってからCloud Runに転送しています。Cloud Runで直接受け取る構成にもできますが、独自ドメインの対応やCloud Armorとの統合などを考慮してCloud Load Balancingを挟む構成にしています。この部分のTerraformのコードを以下に貼ります。
resource "google_dns_record_set" "otel_collector" { name = "<Domain of OTel Collector>" type = "A" ttl = 300 managed_zone = google_dns_managed_zone.coding_ai.name rrdatas = [google_compute_global_address.otel_collector.address] } resource "google_compute_global_address" "otel_collector" { name = "otel-collector-ip" } resource "google_compute_global_forwarding_rule" "otel_collector" { name = "otel-collector-forwarding-rule" target = google_compute_target_https_proxy.otel_collector.id port_range = "443" ip_address = google_compute_global_address.otel_collector.id load_balancing_scheme = "EXTERNAL_MANAGED" } resource "google_compute_managed_ssl_certificate" "otel_collector" { name = "otel-collector-cert" managed { domains = ["<Domain of Otel Collector>"] } } resource "google_compute_ssl_policy" "otel_collector" { name = "otel-collector-ssl-policy" profile = "MODERN" min_tls_version = "TLS_1_2" } resource "google_compute_target_https_proxy" "otel_collector" { name = "otel-collector-https-proxy" url_map = google_compute_url_map.otel_collector.id ssl_certificates = [google_compute_managed_ssl_certificate.otel_collector.id] ssl_policy = google_compute_ssl_policy.otel_collector.id } resource "google_compute_url_map" "otel_collector" { name = "otel-collector-url-map" default_service = google_compute_backend_service.otel_collector.id } resource "google_compute_backend_service" "otel_collector" { name = "otel-collector-backend" protocol = "HTTPS" load_balancing_scheme = "EXTERNAL_MANAGED" backend { group = google_compute_region_network_endpoint_group.otel_collector.id } log_config { enable = true sample_rate = 1.0 } } resource "google_compute_region_network_endpoint_group" "otel_collector" { name = "otel-collector-neg" region = "asia-northeast1" network_endpoint_type = "SERVERLESS" cloud_run { service = google_cloud_run_v2_service.otel_collector.name } } resource "google_artifact_registry_repository" "otel_collector" { location = "asia-northeast1" repository_id = "otel-collector" description = "OpenTelemetry Collector images" format = "DOCKER" } resource "google_secret_manager_secret" "otel_auth_token" { secret_id = "otel-collector-auth-token" replication { auto {} } } resource "google_secret_manager_secret_iam_member" "otel_collector_secret_accessor" { secret_id = google_secret_manager_secret.otel_auth_token.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${google_service_account.otel_collector.email}" } resource "google_cloud_run_v2_service" "otel_collector" { name = "otel-collector" location = "asia-northeast1" ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" invoker_iam_disabled = true template { scaling { min_instance_count = 1 max_instance_count = 10 } service_account = google_service_account.otel_collector.email containers { image = "${google_artifact_registry_repository.otel_collector.location}-docker.pkg.dev/${local.project_id}/${google_artifact_registry_repository.otel_collector.repository_id}/otel-collector:latest" ports { container_port = 4318 } resources { limits = { cpu = "1" memory = "1Gi" } } env { name = "GCP_PROJECT_ID" value = local.project_id } env { name = "OTEL_AUTH_TOKEN" value_source { secret_key_ref { secret = google_secret_manager_secret.otel_auth_token.secret_id version = "latest" } } } } timeout = "300s" } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 } lifecycle { ignore_changes = [scaling] } depends_on = [google_secret_manager_secret_iam_member.otel_collector_secret_accessor] }
Cloud Runの中にはOSSのOpenTelemetry Collectorが動いています。
以下のような設定で動いており、受け取った情報をCloud LoggingとCloud Metricsに転送していることが分かります。
extensions: bearertokenauth: token: ${env:OTEL_AUTH_TOKEN} receivers: otlp: protocols: http: endpoint: 0.0.0.0:${env:PORT} auth: authenticator: bearertokenauth processors: batch: timeout: 10s send_batch_size: 1024 transform: error_mode: ignore log_statements: - context: log statements: - 'set(body, {"message": body}) where IsString(body)' - 'merge_maps(attributes, resource.attributes, "upsert")' - 'merge_maps(body, attributes, "upsert")' exporters: googlecloud: project: ${env:GCP_PROJECT_ID} metric: prefix: "custom.googleapis.com/claude_code" log: default_log_name: "claude-code-telemetry" service: extensions: [bearertokenauth] pipelines: metrics: receivers: [otlp] processors: [batch] exporters: [googlecloud] logs: receivers: [otlp] processors: [batch, transform] exporters: [googlecloud] traces: receivers: [otlp] processors: [batch] exporters: [googlecloud] telemetry: logs: level: info
YAMLには基本的な設定しか書いていませんが、transformの部分がやや特殊なので解説をします。Claude Codeが送信するログに含まれるResource AttributeをそのままCloud Loggingに送信したところ、その情報がCloud Loggingに保存されませんでした。そのため、Resource Attributeの情報を全て抜き出してLog Attributeにコピーしています。
また、Cloud Loggingの標準的なログの保持期限は30日ですので、保持期限を伸ばしています。
_Default ログバケットの保持期限を伸ばすと影響範囲が大きいため、Claude Code用のログバケットを新規に作成し、そちらに流れるようにLog Routerを設定しています。該当箇所のTerraformコードを以下に示します。
resource "google_logging_project_bucket_config" "claude_code_logs" { project = local.project_id location = "global" bucket_id = "claude_code_logs" retention_days = 3650 enable_analytics = true } resource "google_logging_project_sink" "claude_code_logs" { project = local.project_id name = "claude-code-logs-sink" destination = "logging.googleapis.com/projects/${local.project_id}/locations/global/buckets/${google_logging_project_bucket_config.claude_code_logs.bucket_id}" filter = "logName=\"projects/${local.project_id}/logs/claude-code-telemetry\"" unique_writer_identity = true }
利用情報を分析する部分
最後はCloud Loggingに格納されているClaude Codeの利用情報をBigQueryから参照できるようにする部分を解説します。
ここ数年でCloud LoggingとBigQueryはかなり高度に統合されています。特に以下の機能を使うとCloud Loggingに保存されたデータに対して直接BigQueryからクエリを実行できます。Cloud Loggingの中身はBigQueryそのものかと思えるほど統合されています。
そのため、Cloud Loggingに情報を入れることとBigQueryに情報を入れることはほぼ等しくなっています。以下のようにLinked Datasetを作成すれば2つの世界がシームレスにつながり、BigQueryからのクエリを実行できます。
resource "google_logging_linked_dataset" "claude_code_logs" { bucket = google_logging_project_bucket_config.claude_code_logs.id link_id = "claude_code_logs_bq_link" description = "Linked dataset for querying Claude Code logs from BigQuery" }
Claude Codeの利用情報は以下のようにJSON形式で半構造化されたデータが json_payload フィールドに格納されています。

ここに対していちいちJSONパースをするのは手間なので、パース後のVIEWをイベントに応じて作成しています。
SELECT -- Standard attributes JSON_VALUE(json_payload, '$."session.id"') AS session_id, CAST(JSON_VALUE(json_payload, '$."event.sequence"') AS INT64) AS event_sequence, JSON_VALUE(json_payload, '$."service.name"') AS service_name, JSON_VALUE(json_payload, '$."service.version"') AS service_version, JSON_VALUE(json_payload, '$."app.version"') AS app_version, JSON_VALUE(json_payload, '$."organization.id"') AS organization_id, JSON_VALUE(json_payload, '$."user.account_uuid"') AS user_account_uuid, JSON_VALUE(json_payload, '$."user.id"') AS user_id, JSON_VALUE(json_payload, '$."user.email"') AS user_email, JSON_VALUE(json_payload, '$."host.arch"') AS host_arch, JSON_VALUE(json_payload, '$."os.type"') AS os_type, JSON_VALUE(json_payload, '$."os.version"') AS os_version, JSON_VALUE(json_payload, '$."terminal.type"') AS terminal_type, -- Attributes JSON_VALUE(json_payload, '$."event.name"') AS event_name, TIMESTAMP(JSON_VALUE(json_payload, '$."event.timestamp"')) AS event_timestamp, JSON_VALUE(json_payload, '$.model') AS model, CAST(JSON_VALUE(json_payload, '$.cost_usd') AS FLOAT64) AS cost_usd, CAST(JSON_VALUE(json_payload, '$.duration_ms') AS INT64) AS duration_ms, CAST(JSON_VALUE(json_payload, '$.input_tokens') AS INT64) AS input_tokens, CAST(JSON_VALUE(json_payload, '$.output_tokens') AS INT64) AS output_tokens, CAST(JSON_VALUE(json_payload, '$.cache_read_tokens') AS INT64) AS cache_read_tokens, CAST(JSON_VALUE(json_payload, '$.cache_creation_tokens') AS INT64) AS cache_creation_tokens FROM <Cloud LoggingとLinkされたデータセット> WHERE JSON_VALUE(json_payload, '$."event.name"') = 'api_request'
ZOZOのBigQueryは以下の仕組みでkintoneの情報をリアルタイムで取得できるようにしてあります。そのため、kintoneに格納されている組織図情報などとも組み合わせて、どの組織がClaude Codeをよく利用しているのかを分析できます。
利用情報の活用事例
OpenTelemetry機能を使って収集した利用情報の活用事例を1つ紹介します。
Claude Codeを利用するための課金体系はいくつかあります。Pro / Max / Teamプランのような費用が固定されるものもあれば、Anthropic API / AWS Bedrockなどのような従量課金のものもあります。Claude Codeの利用量が少ない人には、前者の方法はコストパフォーマンスが悪いため、後者の従量課金制の仕組みに移行してもらっています。この移行のために、api_request イベントの cost_usd フィールドを集計して、各自に最も適したプランをアナウンスしています。
SELECT DATE(event_timestamp, "Asia/Tokyo") AS DATE, user_email, SUM(cost_usd) AS cost_usd, COUNT(*) AS api_call_count, FROM <APIリクエストログのVIEW> GROUP BY ALL
まとめ
Claude Codeの利用状況をOpenTelemetryで収集する仕組みを紹介しました。組織のAI活用を推進するためにはClaude CodeなどのAIツールの利用状況を集計・分析することが肝心です。同じような課題に直面している人の助けになると嬉しいです。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。