Observability: CloudWatch & Logs
.mdVerified 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:00Zget-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=productionAlarms#
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 prefixMetric 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-enabledA 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 30Log 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.