Live:CloudOps Webinars & Hands-on Workshops ·Register ↗
Skip to main content

Coding Agents Observability

See all events →

Overview

AI coding agents (Claude Code, OpenAI Codex, GitHub Copilot) run on developer laptops outside AWS, making traditional CloudWatch instrumentation impractical. Each of these agents ships a built-in OpenTelemetry SDK that can export metrics directly to the CloudWatch OTLP endpoint using bearer-token authentication — no collectors, sidecars, or AWS SDK required on developer machines.

This solution configures all three agents to send usage metrics (tokens, sessions, cost, tool calls, latency) to CloudWatch, where PromQL dashboards and alarms provide per-developer, per-team, and organizational-level visibility. The same data is queryable from Amazon Managed Grafana via the CloudWatch PromQL data source.

The pattern is consistent across agents: create a CloudWatch metrics API key (bearer token), configure the agent's OTel environment variables or config file, set resource attributes for team attribution, then deploy a pre-built dashboard.

Cost: CloudWatch OTLP metrics ingestion is billed at $0.50/GB. For a 200-developer organization (~20 sessions/day per developer), the metric volume is on the order of tens of MB/month — well under $5/month for ingestion. Claude Code's worked example (7 metrics × 450 bytes/point × 200 developers × 20 sessions/day × 22 days) yields ~0.27 GB/month, roughly $0.14/month in the base case. PromQL queries in the Console are free. See the Amazon CloudWatch Pricing page for the latest rates.

Prerequisites

  • An AWS account with permissions to create IAM users and CloudWatch resources
  • AWS CLI v2 installed and configured
  • At least one coding agent installed:
    • Claude Code — CLI authenticated to Anthropic API or Amazon Bedrock. For enterprise rollouts with corporate SSO/IdP federation (Okta, Azure AD, Auth0, Amazon Cognito, AWS IAM Identity Center) and OIDC credential federation, see the Claude Apps Gateway.
    • OpenAI Codex — CLI authenticated via codex login or Amazon Bedrock (model_provider = "amazon-bedrock" in ~/.codex/config.toml)
    • GitHub Copilot — VS Code extension signed in, or Copilot CLI authenticated
  • IAM permissions: iam:CreateUser, iam:AttachUserPolicy, iam:CreateServiceSpecificCredential, cloudwatch:PutDashboard

Architecture

┌──────────────────────────────────────────────────────────────────┐
│ Developer Laptops │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Claude Code │ │ OpenAI Codex│ │ GitHub Copilot │ │
│ │ (OTel SDK) │ │ (OTel SDK) │ │ (VS Code / CLI) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
└─────────┼──────────────────┼──────────────────────┼──────────────┘
│ HTTPS + Bearer │ HTTPS + Bearer │ HTTPS + Bearer
│ /v1/metrics │ /v1/metrics │ /v1/metrics
└──────────────────┼──────────────────────┘

┌──────────────────────────────┐
│ CloudWatch OTLP Endpoint │
│ monitoring.<region>.aws.com │
└──────────────┬───────────────┘


┌──────────────────────────────┐
│ CloudWatch metric store │
│ queryable via PromQL API │
└──────┬────────────────┬──────┘
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ CloudWatch │ │ Grafana │
│ Dashboards │ │ (Prometheus data │
│ + Alarms │ │ source, SigV4) │
└────────────────────┘ └────────────────────┘

Deploy

Step 1: Create a bearer token (once per team)

Bearer tokens allow tools running outside AWS to send metrics to CloudWatch without IAM credential chains. Each token is tied to an IAM user scoped to the CloudWatchAPIKeyAccess managed policy.

aws iam create-user --user-name coding-agents-cw-metrics

aws iam attach-user-policy \
--user-name coding-agents-cw-metrics \
--policy-arn arn:aws:iam::aws:policy/CloudWatchAPIKeyAccess

aws iam create-service-specific-credential \
--user-name coding-agents-cw-metrics \
--service-name cloudwatch.amazonaws.com \
--credential-age-days 90

Store the ServiceCredentialSecret value in AWS Secrets Manager or your vault. Never commit it to version control.

Step 2: Configure Claude Code

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.us-east-1.amazonaws.com"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ${BEARER_TOKEN}"
export OTEL_METRIC_EXPORT_INTERVAL=2000
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}"

OTEL_METRIC_EXPORT_INTERVAL=2000 (2 seconds) makes metrics appear quickly during verification. For steady-state fleet use, raise toward the 60000 ms default.

Claude Code can also export events as OpenTelemetry logs. To capture them, additionally set export OTEL_LOGS_EXPORTER=otlp.

Step 3: Configure OpenAI Codex

Add to ~/.codex/config.toml:

[otel]

[otel.metrics_exporter.otlp-http]
endpoint = "https://monitoring.us-east-1.amazonaws.com/v1/metrics"
protocol = "binary"

[otel.metrics_exporter.otlp-http.headers]
"Authorization" = "Bearer <YOUR_BEARER_TOKEN>"

Paste the literal token value — Codex does not expand environment-variable references in TOML headers. Restrict permissions: chmod 600 ~/.codex/config.toml.

Set attribution in your shell:

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}"

Step 4: Configure GitHub Copilot

There are two Copilot products that emit OpenTelemetry with different metric sets:

VS Code Copilot Chat extensionGitHub Copilot CLI
service.namecopilot-chatgithub-copilot
Tool metric prefixcopilot_chat.tool.call.*github.copilot.tool.call.*
Default OTLP protocolhttp/protobufhttp/json

Set common environment variables first (in the shell that launches the client):

export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <YOUR_BEARER_TOKEN>"
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}"

VS Code Copilot Chat extension — enable OTel in settings.json (the auth header must come from the environment variable):

{
"github.copilot.chat.otel.enabled": true,
"github.copilot.chat.otel.otlpEndpoint": "https://monitoring.<AWS_REGION>.amazonaws.com",
"github.copilot.chat.otel.exporterType": "otlp-http"
}

Then launch VS Code from the shell with variables set: code .

GitHub Copilot CLI — configured entirely through environment variables:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://monitoring.<AWS_REGION>.amazonaws.com"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/json"
copilot

To inspect raw metric emissions, set COPILOT_OTEL_FILE_EXPORTER_PATH to a local file path, or use "github.copilot.chat.otel.exporterType": "console" in VS Code.

Step 5: Deploy CloudWatch dashboards

# Claude Code
curl -o claude-code-dashboard.json \
https://raw.githubusercontent.com/aws-observability/aws-observability-accelerator/main/artifacts/cloudwatch-dashboards/claude-code/claude-code.json
aws cloudwatch put-dashboard --dashboard-name ClaudeCodeDashboard \
--dashboard-body file://claude-code-dashboard.json --region us-east-1

# Codex
curl -o codex-dashboard.json \
https://raw.githubusercontent.com/aws-observability/aws-observability-accelerator/main/artifacts/cloudwatch-dashboards/codex/codex.json
aws cloudwatch put-dashboard --dashboard-name CodexDashboard \
--dashboard-body file://codex-dashboard.json --region us-east-1

# Copilot
curl -o copilot-dashboard.json \
https://raw.githubusercontent.com/aws-observability/aws-observability-accelerator/main/artifacts/cloudwatch-dashboards/copilot/copilot.json
aws cloudwatch put-dashboard --dashboard-name CopilotDashboard \
--dashboard-body file://copilot-dashboard.json --region us-east-1

Step 6: Deploy Grafana dashboards

If your organization uses Amazon Managed Grafana (or self-managed Grafana), import the equivalent Grafana JSON for each agent. Each uses the same PromQL, so add a Prometheus data source pointed at the CloudWatch PromQL endpoint with SigV4 authentication and the Service set to monitoring. Select that data source for the dashboard's datasource variable on import.

Note this path does not involve Amazon Managed Service for Prometheus. The metrics stay in CloudWatch; the PromQL API is a CloudWatch query surface over OTLP-ingested metrics, and Grafana reads it using a generic Prometheus data source.

# Claude Code
curl -o claude-code-grafana.json \
https://raw.githubusercontent.com/aws-observability/aws-observability-accelerator/main/artifacts/grafana-dashboards/claude-code/claude-code.json

# Codex
curl -o codex-grafana.json \
https://raw.githubusercontent.com/aws-observability/aws-observability-accelerator/main/artifacts/grafana-dashboards/codex/codex.json

# Copilot
curl -o copilot-grafana.json \
https://raw.githubusercontent.com/aws-observability/aws-observability-accelerator/main/artifacts/grafana-dashboards/copilot/copilot.json

Validate

  1. Claude Code — run a short session and query:

    claude -p "hello" --max-turns 1
    # Then in CloudWatch Query Studio:
    # sum({"claude_code.token.usage"})
  2. Codex — run a one-shot command:

    codex exec "print hello world in python"
    # Query: sum({"codex.turn.token_usage"})
  3. Copilot — open VS Code, send a chat prompt, then query:

    sum(histogram_sum({"gen_ai.client.token.usage", "@resource.service.name"=~"copilot.*"}))

Metrics take 2–5 minutes to appear. Verify resource attributes with cwpromql series '{"claude_code.token.usage"}' -o json or in CloudWatch Query Studio.

Metrics emitted

Claude Code

MetricTypeDescription
claude_code.token.usageCounterTokens consumed; attribute typeinput, output, cacheRead, cacheCreation, plus model
claude_code.cost.usageCounterEstimated cost in USD; attribute model
claude_code.session.countCounterCLI sessions started
claude_code.lines_of_code.countCounterLines of code modified; attribute typeadded, removed
claude_code.commit.countCounterGit commits created by Claude Code
claude_code.pull_request.countCounterPull requests created by Claude Code
claude_code.code_edit_tool.decisionCounterEdit-tool permission decisions; attributes tool, decisionaccept, reject
claude_code.active_time.totalCounterTotal active developer time in seconds

Claude Code is the only agent that emits a cost metric (claude_code.cost.usage), so dashboards chart estimated spend directly.

OpenAI Codex

MetricTypeDescription
codex.turn.token_usageHistogramToken usage; attribute token_typeinput, output, cached_input, reasoning_output, plus model
codex.api_requestCounterModel API request count; attributes model, success, status
codex.api_request.duration_msHistogramAPI request latency
codex.tool.callCounterTool invocation count; attributes tool, success
codex.tool.call.duration_msHistogramTool execution latency
codex.approval.requestedCounterApproval prompts and their decision
codex.conversation.turn.countCounterConversation turns; attribute model
codex.turn.e2e_duration_msHistogramEnd-to-end turn latency
codex.thread.startedCounterThreads/sessions started

Codex does not emit a cost metric. Multiply token counts by model pricing downstream if needed.

GitHub Copilot

There are two clients with different metric sets. The dashboards match both via @resource.service.name=~"copilot.*" and union the two tool-metric names.

MetricTypeSourceNotes
gen_ai.client.token.usageHistogrambothToken counts; gen_ai.token.typeinput, output; gen_ai.request.model. Query totals with sum(histogram_sum(...)).
gen_ai.client.operation.durationHistogrambothLLM call duration (seconds); gen_ai.request.model, error.type
copilot_chat.tool.call.count / github.copilot.tool.call.countCounterVS Code / CLITool invocations; gen_ai.tool.name, success
copilot_chat.tool.call.duration / github.copilot.tool.call.durationHistogramVS Code / CLITool execution latency
copilot_chat.agent.turn.count / github.copilot.agent.turn.countHistogramVS Code / CLILLM round-trips per agent invocation
copilot_chat.time_to_first_tokenHistogramVS CodeTime to first SSE token (seconds)
copilot_chat.agent.invocation.durationHistogramVS CodeAgent end-to-end duration (seconds)
copilot_chat.session.countCounterVS CodeChat sessions started
copilot_chat.lines_of_code.countCounterVS CodeLines added or removed by accepted edits
copilot_chat.edit.acceptance.countCounterVS CodeEdit accept/reject decisions
copilot_chat.user.feedback.countCounterVS CodeThumbs up/down votes
copilot_chat.user.action.countCounterVS CodeEngagement actions (copy, insert, apply, followup)
copilot_chat.pull_request.countCounterVS CodePull requests created

The GitHub Copilot CLI emits only the first five rows. The remaining copilot_chat.* metrics are VS Code-extension-only. Copilot does not emit a dollar-cost metric.

Copilot metrics carry these datapoint attributes, which are what you group and filter by when building panels or alarms: gen_ai.request.model, gen_ai.provider.name, gen_ai.tool.name, copilot_chat.edit.source, and error.type.

Alerting

Every dashboard panel is backed by a PromQL query. Create an alarm from any panel via View in Query Studio > Create alarm. Examples by agent:

Claude Code

Individual spend spike — alert when a developer's hourly spend exceeds twice their 24-hour average:

sum by ("@resource.user.email") (increase({"claude_code.cost.usage"}[1h]))
> 2 * avg_over_time(sum by ("@resource.user.email") (increase({"claude_code.cost.usage"}[1h]))[24h:1h])

Team budget threshold — alert when a team's daily cost exceeds a budget (USD):

sum by ("@resource.team.id") (increase({"claude_code.cost.usage"}[24h])) > 500

Adoption regression — detect when a team's daily sessions drop below half their 7-day average:

sum by ("@resource.team.id") (increase({"claude_code.session.count"}[24h]))
< 0.5 * avg_over_time(sum by ("@resource.team.id") (increase({"claude_code.session.count"}[1h]))[7d:1d])

OpenAI Codex

Team token-usage threshold:

sum by ("@resource.team.id") (increase({"codex.turn.token_usage"}[24h])) > 5000000

Elevated API error rate:

sum(increase({"codex.api_request", success="false"}[1h])) > 50

Latency regression — p90 turn latency exceeds 30s:

histogram_quantile(0.9, sum({"codex.turn.e2e_duration_ms"})) > 30000

Adoption regression:

sum by ("@resource.team.id") (increase({"codex.thread.started"}[24h]))
< 0.5 * avg_over_time(sum by ("@resource.team.id") (increase({"codex.thread.started"}[1h]))[7d:1d])

GitHub Copilot

Team token-usage threshold:

sum by ("@resource.team.id") (increase(histogram_sum({"gen_ai.client.token.usage"})[24h])) > 5000000

LLM latency regression — p90 operation duration exceeds 30s:

histogram_quantile(0.9, sum({"gen_ai.client.operation.duration"})) > 30

Adoption regression:

sum by ("@resource.team.id") (increase({"copilot_chat.session.count"}[24h]))
< 0.5 * avg_over_time(sum by ("@resource.team.id") (increase({"copilot_chat.session.count"}[1h]))[7d:1d])

Troubleshoot

SymptomLikely CauseFix
No metrics after 5 min (any agent)Bearer token invalid or endpoint URL wrongVerify token is the literal ServiceCredentialSecret value; confirm endpoint is https://monitoring.<REGION>.amazonaws.com
Codex config ignored~/.codex/config.toml syntax error or env-var reference in headerPaste the literal token value (Codex does not expand ${VAR} in TOML); run chmod 600 ~/.codex/config.toml
Copilot metrics missing in CloudWatchOTEL_EXPORTER_OTLP_HEADERS not set in the shell that launched VS CodeExport the variable first, then run code . from that shell; VS Code does not pick up headers from settings.json
Resource attributes (@resource.team.id) emptyOTEL_RESOURCE_ATTRIBUTES unset or malformedEnsure the variable is exported in the shell profile that launches the agent; no spaces around = in values
Dashboard shows "No Data" for Grafana panelsData source not pointed at CloudWatch PromQL endpointIn AMG, set the Prometheus data source URL to https://monitoring.<REGION>.amazonaws.com with SigV4 service monitoring
Copilot CLI export errors for traces/logsCLI sends all signals to the same endpoint; non-metrics POSTs are rejectedHarmless — metrics still flow. To suppress, run a local OTel Collector and route each signal separately