Verified8 commandsAI-assisted

Observability: CloudWatch & Logs

.md

Verified against aws-cli/2.33.6, flags verified via `aws <cmd> help`, 2026-08-21 · official docs

CloudWatch metrics and alarms, and CloudWatch Logs — the commands for reading what's actually happening in a running system and reacting to it.

Reading metrics#

aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --start-time 2026-08-19T00:00:00Z --end-time 2026-08-20T00:00:00Z \
  --period 300 --statistics Average

aws cloudwatch get-metric-data \
  --metric-data-queries '[{"Id":"cpu","MetricStat":{"Metric":{"Namespace":"AWS/EC2","MetricName":"CPUUtilization","Dimensions":[{"Name":"InstanceId","Value":"i-0123456789abcdef0"}]},"Period":300,"Stat":"Average"},"ReturnData":true}]' \
  --start-time 2026-08-19T00:00:00Z --end-time 2026-08-20T00:00:00Z

get-metric-statistics is the simple single-metric query; get-metric-data is the newer, batched form (query up to 500 metrics in one call, supports metric math) — reach for get-metric-data for anything beyond a quick one-off check.

Publishing custom metrics#

aws cloudwatch put-metric-data \
  --namespace MyApp \
  --metric-name QueueDepth \
  --value 42 --unit Count \
  --dimensions Environment=production

Alarms#

aws cloudwatch put-metric-alarm \
  --alarm-name high-cpu \
  --namespace AWS/EC2 --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --statistic Average --period 300 --evaluation-periods 3 \
  --threshold 80 --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:sns:us-east-1:111122223333:my-alerts-topic

aws cloudwatch describe-alarms --state-value ALARM       # only alarms currently firing
aws cloudwatch describe-alarms --alarm-name-prefix high- # filter by name prefix

Metric math#

aws cloudwatch get-metric-data \
  --start-time 2026-08-20T00:00:00Z --end-time 2026-08-21T00:00:00Z \
  --metric-data-queries '[
    {"Id":"m1","MetricStat":{"Metric":{"Namespace":"AWS/EBS","MetricName":"VolumeReadOps","Dimensions":[{"Name":"VolumeId","Value":"vol-0123456789abcdef0"}]},"Period":300,"Stat":"Sum"},"ReturnData":false},
    {"Id":"m2","MetricStat":{"Metric":{"Namespace":"AWS/EBS","MetricName":"VolumeWriteOps","Dimensions":[{"Name":"VolumeId","Value":"vol-0123456789abcdef0"}]},"Period":300,"Stat":"Sum"},"ReturnData":false},
    {"Id":"total_iops","Expression":"(m1+m2)/300","Label":"Avg Total IOPS","ReturnData":true}
  ]'

Metric math lets you combine raw metrics with arithmetic/statistical functions server-side instead of pulling raw series and computing client-side. ReturnData: false on the input metrics (m1, m2) hides them from the response and returns only the computed expression — set it true on any series you also want returned alongside the math result.

Dashboards#

aws cloudwatch put-dashboard --dashboard-name my-service --dashboard-body file://dashboard.json
aws cloudwatch get-dashboard --dashboard-name my-service
aws cloudwatch list-dashboards --dashboard-name-prefix my-

--dashboard-body is a JSON document of widget definitions (each widget references a metric or a log-insights query) — there's no imperative "add a widget" command, put-dashboard always replaces the entire dashboard body. Fetch the current body with get-dashboard, edit it, and put-dashboard it back rather than hand-authoring the whole thing from scratch each time.

Composite alarms#

aws cloudwatch put-composite-alarm \
  --alarm-name service-degraded \
  --alarm-rule "ALARM(high-cpu) AND ALARM(high-error-rate)" \
  --alarm-actions arn:aws:sns:us-east-1:111122223333:my-alerts-topic \
  --actions-enabled

A composite alarm doesn't watch a metric — its --alarm-rule is a boolean expression (AND/OR/NOT, parenthesized) over the ALARM/OK/INSUFFICIENT_DATA state of other alarms. Use it to cut noise: page only when several individually-noisy alarms are in ALARM together, instead of firing one page per underlying alarm.

Log groups and streams#

aws logs describe-log-groups --log-group-name-prefix /aws/lambda/
aws logs describe-log-streams --log-group-name /aws/lambda/my-function --order-by LastEventTime --descending
aws logs put-retention-policy --log-group-name /aws/lambda/my-function --retention-in-days 30

Log groups default to never expiring unless you set a retention policy — a common, quietly expensive default left over from first-time Lambda/ECS setups. Worth auditing with describe-log-groups periodically.

Reading log events#

aws logs get-log-events --log-group-name /aws/lambda/my-function --log-stream-name <stream-name> --start-from-head
aws logs filter-log-events --log-group-name /aws/lambda/my-function --filter-pattern "ERROR" --start-time 1755648000000
aws logs tail /aws/lambda/my-function --follow --since 1h --filter-pattern "ERROR"

aws logs tail is the closest thing to kubectl logs -f for CloudWatch — it's a CLI-only convenience command (not a direct API wrapper), it accepts human-readable --since values like 1h/30m, and --follow streams new events live instead of returning a fixed page.

filter-log-events searches across all streams in a log group at once; get-log-events reads one specific stream. Use filter-log-events when you don't already know which stream/task/container instance produced the log line you're looking for.