Analyzing Claude Code Usage with CloudWatch and OpenTelemetry
The telemetry environment variables and metric names in this guide follow the official Claude Code monitoring documentation. Claude Code's telemetry is evolving quickly — verify metric names against your installed version (see Verify metrics are flowing).
Bearer token authentication
Bearer tokens (CloudWatch metrics API keys) allow tools running outside AWS (like Claude Code on developer laptops) to send metrics to CloudWatch without requiring the AWS SDK or IAM credential chains. Each token is tied to an AWS IAM user scoped exclusively to the CloudWatchAPIKeyAccess managed policy.
Bearer tokens are long-term credentials. This recipe uses them because AI coding agents run on developer laptops outside of AWS, where SigV4 with short-term credentials would require a central collector or a per-machine collector process. For workloads running inside AWS where SigV4 with short-term credentials is feasible, prefer that approach for a stronger security posture. The CloudWatch OTLP endpoint requires HTTPS; requests over plain HTTP are rejected. For more information, see CloudWatch OTLP Metrics Bearer Token Auth.
Solution overview
The setup has three components:
- A CloudWatch metrics API key — a bearer token tied to a narrowly-scoped IAM user. Created once per developer (or shared per team).
- Claude Code configuration — a handful of environment variables that tell Claude Code's OpenTelemetry SDK to enable telemetry, where to send metrics, and how to attribute them.
- A pre-built dashboard — a CloudWatch dashboard (and a Grafana equivalent) that visualizes token usage, cost, developer productivity, and team-level usage with PromQL queries.
Prerequisites
- An AWS account with permissions to create CloudWatch and IAM resources.
- AWS CLI v2 installed and configured.
- Claude Code installed and able to reach a model. How you authenticate Claude Code to the model depends on your provider and is independent of the CloudWatch metrics setup in this recipe:
- Anthropic API (default) — authenticate with
claude(interactive login) or setANTHROPIC_API_KEY. - Amazon Bedrock — set
export CLAUDE_CODE_USE_BEDROCK=1and authenticate with your AWS credentials (e.g.export AWS_PROFILE=... AWS_REGION=..., or any provider in the AWS credential chain). The bearer token in this recipe authenticates Claude Code to CloudWatch, not to Bedrock.
- Anthropic API (default) — authenticate with
- A CloudWatch metrics API key (created below).
For enterprise rollouts — corporate SSO and IdP federation (Okta, Azure AD, Auth0, Amazon Cognito, AWS IAM Identity Center), OIDC credential federation that eliminates long-lived API keys, per-user attribution (department, team, cost center) from JWT claims, and quota/cost controls — see the Guidance for Claude Code with Amazon Bedrock repository. It provides deployable authentication patterns (External IdP OIDC, IAM Identity Center) for both the Claude Code CLI and Claude Cowork Desktop. That guidance handles how developers authenticate to Bedrock at scale; the CloudWatch metrics setup in this recipe is independent and layers on top of it.
Create a bearer token
You can create the token in the CloudWatch console (Settings > scroll to API keys > Create) or with the CLI:
# Create an IAM user for CloudWatch metrics ingestion
aws iam create-user --user-name claude-code-cloudwatch-metrics-user
# Attach the CloudWatchAPIKeyAccess managed policy
aws iam attach-user-policy \
--user-name claude-code-cloudwatch-metrics-user \
--policy-arn arn:aws:iam::aws:policy/CloudWatchAPIKeyAccess
# Create a service-specific credential (the bearer token), expiring in 90 days
aws iam create-service-specific-credential \
--user-name claude-code-cloudwatch-metrics-user \
--service-name cloudwatch.amazonaws.com \
--credential-age-days 90
The response includes a ServiceCredentialSecret field — this is your bearer token value. Store it securely in AWS Secrets Manager or your organization's vault. Never commit it to version control.
Configure Claude Code
Claude Code reads its telemetry configuration from standard OpenTelemetry environment variables. Set them in the shell that launches claude (or your fleet's profile management). This routes metrics to the CloudWatch OTLP endpoint over a bearer-authenticated HTTPS connection.
# Pull the bearer token from your vault rather than hard-coding it
BEARER_TOKEN=$(aws secretsmanager get-secret-value \
--secret-id cloudwatch-otlp-bearer-token \
--query SecretString --output text)
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_ENDPOINT="https://monitoring.<AWS_REGION>.amazonaws.com"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ${BEARER_TOKEN}"
export OTEL_METRIC_EXPORT_INTERVAL=2000
Replace <AWS_REGION> with your target Region (for example us-east-1). The OpenTelemetry SDK appends the /v1/metrics path to the base endpoint automatically. http/json and http/protobuf are both accepted by CloudWatch.
OTEL_METRIC_EXPORT_INTERVAL=2000 (2 seconds) makes metrics appear quickly while you verify the setup. Claude Code's default is 60000 ms (60 seconds); for steady-state fleet use, raise the interval back toward the default to reduce request volume.
For a fleet-wide rollout, you can set these same variables in the env block of a managed Claude Code settings.json instead of each developer's shell profile.
Add identity and team attribution
Claude Code reads the standard OTEL_RESOURCE_ATTRIBUTES environment variable and attaches the values as resource attributes on every metric. This is how you get per-developer and per-team breakdowns. Set it in the developer's shell profile (or via your fleet's profile management):
export OTEL_RESOURCE_ATTRIBUTES="user.id=$(whoami),user.email=${USER_EMAIL},team.id=${TEAM:-engineering},cost_center=${COST_CENTER:-default},department=${DEPARTMENT:-engineering},environment=${ENV:-dev}"
These dimensions arrive as resource attributes, so in PromQL they are referenced with the @resource. prefix — for example @resource.team.id. The pre-built dashboards already use this prefix. Claude Code's service name is fixed by the CLI (@resource.service.name is claude-code); the dashboards match Claude Code's metrics by their unique claude_code.* prefix.
The attributes you can rely on for grouping and filtering:
| Attribute | PromQL label | Purpose | Example |
|---|---|---|---|
user.id | @resource.user.id | Per-developer attribution | jdoe |
user.email | @resource.user.email | Per-developer attribution | jdoe@example.com |
team.id | @resource.team.id | Team-level aggregation | platform-eng |
cost_center | @resource.cost_center | Finance/chargeback grouping | CC-4200 |
department | @resource.department | Org-level rollup | engineering |
environment | @resource.environment | Distinguish dev/staging/prod usage | production |
Verify metrics are flowing
Run a short Claude Code session so it emits a turn's worth of metrics:
claude -p "Let's conquer the world" --max-turns 1
Then open CloudWatch Query Studio and type claude_ to see the available metrics, or run an instant query such as:
sum({"claude_code.token.usage"})