# AWS CLI Cheat Sheet — Observability: CloudWatch & Logs

> **Tool:** AWS CLI v2
> **Category:** Cloud CLIs
> **Verified against:** aws-cli/2.33.6, flags verified via `aws <cmd> help`, 2026-08-21
> **Official docs:** https://docs.aws.amazon.com/cli/

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

```bash
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

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

## Alarms

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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.
