以下の内容はhttps://techblog.zozo.com/entry/claudecode-otelより取得しました。


社員に何もさせずにClaude Code利用ログを集める ── 数百名規模のOpenTelemetry収集基盤の構築

社員に何もさせずにClaude Code利用ログを集める ── 数百名規模のOpenTelemetry収集基盤の構築

こんにちは、技術戦略部CTOブロックの塩崎です。

当社ZOZOには1人あたり月額200ドルの基準のもと、Claude CodeやGemini CLIをはじめとした各種AI開発ツールを利用可能にする制度を2025年7月にスタートさせました。

corp.zozo.com

現在ではこの制度を用いて数百名という非常に多くの社員がClaude Codeを利用しています。このような中で組織全体のAI活用を推進するためには、それぞれの社員や部署のClaude Codeの利用状況をモニタリングすることが重要です。そのためにClaude CodeのOpenTelemetry機能を利用して、全社員のClaude Code利用状況を収集したので、本記事ではその手法を紹介します。

ccusageを使った利用情報の収集の課題

Claude Codeの利用情報を収集する方法と言いますと、まずccusageを思い浮かべる人が多いかと思います。

ccusage.com

当社でも最初はこのccusageを利用しようとしましたが、課題に遭遇しました。まず利用者にccusageを実行してもらうという点が課題でした。ccusageはコマンド一発で利用状況を出力でき、プログラムから扱いやすい構造化されたJSON出力もサポートしています。そういう意味で非常に便利なツールではあるものの、数百名の社員から漏れなくccusageの出力結果を回収しようとすると手間がかかります。さらにこの作業は1回だけ実施すればOKというものではなく、継続的なモニタリングのためには都度ccusageを回収する必要もあります。

実際に全社員からccusageを集めるということを1回実施してみましたが、これを定期的に実施することは運用負荷が高いという結論になりました。数名から十数名の組織であれば定期的なccusageの収集が十分現実的に実施できるかもしれませんが、ZOZOの規模感では厳しい結果になりました。

Claude CodeのOTel機能の紹介

ccusageの代わりに注目した機能が、Claude CodeのOpenTelemetry出力機能です。

code.claude.com

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と呼ばれ、優先順位が最も高い設定ファイルとして認識されます。

code.claude.com

そのため、以下のような内容のファイルを配布し、全社員の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の上で似たような仕組みを構築する場合は、以下のドキュメントなどが参考になるかと思います。

github.com

(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が動いています。

github.com

以下のような設定で動いており、受け取った情報を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.google.com

そのため、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 フィールドに格納されています。

Claude Code利用情報へのログ結果

ここに対していちいち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をよく利用しているのかを分析できます。

techblog.zozo.com

利用情報の活用事例

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では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com




以上の内容はhttps://techblog.zozo.com/entry/claudecode-otelより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14