Docs / CloudWatch

CloudWatch

LocalEmu CloudWatch implements 20 of 43 operations, covering the metrics, alarms, and dashboards core. The provider is fully LocalEmu-side (no moto delegation): metric data lands in a SQLite database, alarms are evaluated by a real scheduler thread on their configured period, and ALARM-state actions fire to SNS and Lambda for real. The 23 not-implemented operations are concentrated in four feature surfaces that LocalEmu does not currently emulate: anomaly detection, Contributor Insights, Metric Streams (to Firehose), and Alarm Mute Rules.

Operation-level coverage: see the CloudWatch coverage matrix.

Quick start

Terminal
$ awsemu cloudwatch put-metric-data --namespace App/Api \
    --metric-data '[
      {"MetricName":"Latency","Value":42.0,"Unit":"Milliseconds","Dimensions":[{"Name":"Route","Value":"GET /users"}]},
      {"MetricName":"Latency","Value":58.0,"Unit":"Milliseconds","Dimensions":[{"Name":"Route","Value":"GET /users"}]},
      {"MetricName":"Latency","Value":113.0,"Unit":"Milliseconds","Dimensions":[{"Name":"Route","Value":"GET /users"}]}
    ]'

$ awsemu cloudwatch list-metrics --namespace App/Api \
    --query 'Metrics[].{Name:MetricName,Dim:Dimensions[0].Value}'
[{"Name": "Latency", "Dim": "GET /users"}]

$ awsemu cloudwatch get-metric-statistics --namespace App/Api \
    --metric-name Latency --statistics Sum Average Maximum \
    --start-time $(date -u -v-5M +%FT%TZ) --end-time $(date -u +%FT%TZ) \
    --period 60 --query 'Datapoints[].{Sum:Sum,Avg:Average,Max:Maximum}'
[{"Sum": 213.0, "Avg": 71.0, "Max": 113.0}]

Both raw Value and pre-aggregated StatisticValues (Sum, Minimum, Maximum, SampleCount) are accepted. Timestamps default to current UTC if absent. GetMetricStatistics aggregates per requested period across all matching dimensions.

Architecture

Code lives at services/cloudwatch/. The default provider is CloudwatchProvider in provider_v2.py:140, wired in services/providers.py:68-73. It uses two backing stores:

Persistence: accept_state_visitor (provider_v2.py:160-162) registers both cloudwatch_stores and the SQLite data directory as an AssetDirectory. With PERSISTENCE=1, metric data, alarms, dashboards, alarm history, and tags all survive restart. On load, restart_existing_alarms (alarm_scheduler.py:108-117) re-registers every alarm with the scheduler.

Legacy v1 provider: PROVIDER_OVERRIDE_CLOUDWATCH=v1 selects the older moto-backed provider at provider.py. The v2 SQLite path is the default and is the one this page documents.

Features supported

FeatureNotes
Metric ingestPutMetricData. Accepts raw Value or pre-aggregated StatisticValues; Values+Counts arrays must be same length (validated at provider_v2.py:118-137).
Metric queryGetMetricStatistics (per-period aggregation, multi-statistic), GetMetricData (multi-query batch, NextToken pagination, Label override), ListMetrics.
Metric alarmsPutMetricAlarm, DeleteAlarms, DescribeAlarms, DescribeAlarmsForMetric, DescribeAlarmHistory, EnableAlarmActions, DisableAlarmActions, SetAlarmState.
Comparison operatorsGreaterThanOrEqualToThreshold, GreaterThanThreshold, LessThanThreshold, LessThanOrEqualToThreshold (alarm_scheduler.py:23-28). Anomaly-detection operators not in this set.
Alarm actionsSNS Publish and Lambda Invoke dispatched on every state transition with ActionsEnabled=true (provider_v2.py:361-378). OKActions, AlarmActions, InsufficientDataActions all honored.
Composite alarmsPutCompositeAlarm + evaluation at provider_v2.py:868-934. Rule grammar: ALARM("name") OR ALARM("name"). Child alarms referenced by ARN; state changes propagate to the composite, which fires its own actions.
TreatMissingDatamissing, breaching, notBreaching, ignore (validated at provider_v2.py:429-437).
DashboardsPutDashboard, GetDashboard, ListDashboards, DeleteDashboards. JSON body stored verbatim; widget rendering is the client's job.
TagsTagResource, UntagResource, ListTagsForResource. Alarm tags are auto-removed on DeleteAlarms (provider_v2.py:205).
PersistenceSQLite metric data + alarms + dashboards + history + tags all survive PERSISTENCE=1 restart. Alarms are re-scheduled on load.
Internal endpointGET /_aws/cloudwatch/metrics/raw dumps every metric data point in the SQLite store as JSON. Useful for dashboards and test assertions.

Alarms: real-time evaluation

When you create or update a metric alarm, the provider hands the ARN to AlarmScheduler.schedule_metric_alarm (alarm_scheduler.py:63-96). The scheduler:

Terminal
$ awsemu sns create-topic --name on-call --query TopicArn --output text
arn:aws:sns:us-east-1:000000000000:on-call

$ awsemu cloudwatch put-metric-alarm \
    --alarm-name high-latency \
    --namespace App/Api --metric-name Latency --statistic Average \
    --period 60 --evaluation-periods 1 \
    --threshold 100 --comparison-operator GreaterThanThreshold \
    --alarm-actions arn:aws:sns:us-east-1:000000000000:on-call

# Push a few data points above the threshold. The alarm scheduler
# thread will pick them up at the next period boundary and fire SNS.
$ for v in 120 140 160; do
>   awsemu cloudwatch put-metric-data --namespace App/Api \
>     --metric-data '[{"MetricName":"Latency","Value":'$v',"Unit":"Milliseconds"}]'
> done

$ awsemu cloudwatch describe-alarms --alarm-names high-latency \
    --query 'MetricAlarms[].{State:StateValue,Reason:StateReason}'
[{"State": "ALARM", "Reason": "Threshold Crossed: ..."}]

The SNS message payload matches the AWS-standard alarm notification shape. For Lambda targets, the same payload is delivered as the function invocation body:

Terminal
$ awsemu lambda create-function --function-name alarm-handler \
    --runtime python3.12 --handler index.handler \
    --role arn:aws:iam::000000000000:role/lambda-role \
    --zip-file fileb://handler.zip

$ awsemu cloudwatch put-metric-alarm \
    --alarm-name lambda-alarm \
    --namespace App/Api --metric-name ErrorCount --statistic Sum \
    --period 60 --evaluation-periods 1 \
    --threshold 1 --comparison-operator GreaterThanOrEqualToThreshold \
    --alarm-actions arn:aws:lambda:us-east-1:000000000000:function:alarm-handler

# Drive the alarm into ALARM state manually for testing
$ awsemu cloudwatch set-alarm-state \
    --alarm-name lambda-alarm \
    --state-value ALARM --state-reason "manual trigger for testing"

# The lambda was invoked with the SNS-style alarm payload
$ awsemu logs filter-log-events \
    --log-group-name /aws/lambda/alarm-handler --limit 1 --query 'events[0].message'
"{\"AlarmName\":\"lambda-alarm\",\"NewStateValue\":\"ALARM\",\"OldStateValue\":\"INSUFFICIENT_DATA\",...}"

Composite alarms

PutCompositeAlarm accepts an AlarmRule built from child alarm references. The evaluator at provider_v2.py:868-934 tokenises the rule, resolves each ALARM("name-or-arn") to a child alarm, and computes the composite state. Whenever a child alarm transitions, every composite alarm that mentions it is re-evaluated.

Terminal
$ awsemu cloudwatch put-composite-alarm \
    --alarm-name api-degraded \
    --alarm-rule 'ALARM("high-latency") OR ALARM("high-errors")' \
    --alarm-actions arn:aws:sns:us-east-1:000000000000:on-call

$ awsemu cloudwatch describe-alarms \
    --alarm-types CompositeAlarm \
    --query 'CompositeAlarms[].{Name:AlarmName,Rule:AlarmRule,State:StateValue}'
[{
  "Name":  "api-degraded",
  "Rule":  "ALARM(\"high-latency\") OR ALARM(\"high-errors\")",
  "State": "INSUFFICIENT_DATA"
}]

Dashboards

Dashboards are stored as opaque JSON bodies (provider_v2.py:584-650). The CRUD surface is complete, so any AWS Console or Terraform workflow that creates dashboards round-trips faithfully. Widget rendering is not performed locally; GetMetricWidgetImage (PNG generation) is not implemented.

Terminal
$ awsemu cloudwatch put-dashboard --dashboard-name api-overview \
    --dashboard-body '{
      "widgets":[{
        "type":"metric","x":0,"y":0,"width":12,"height":6,
        "properties":{
          "metrics":[["App/Api","Latency"]],
          "period":60,"stat":"Average","region":"us-east-1","title":"API latency"
        }
      }]
    }'

$ awsemu cloudwatch list-dashboards \
    --query 'DashboardEntries[].DashboardName'
["api-overview"]

$ awsemu cloudwatch get-dashboard --dashboard-name api-overview \
    --query 'DashboardBody' --output text | python3 -m json.tool
{"widgets": [{"type": "metric", ...}]}

Logs metric filters: cross-publish

The CloudWatch Logs provider calls this CloudWatch backend on every PutLogEvents that matches a metric filter (services/logs/provider.py:81-104). The matched events drive PutMetricData with the configured namespace and metric name, landing in the same SQLite store. From there, alarms and dashboards work over log-derived metrics as they would in AWS.

Terminal
# Logs metric filters cross-publish to this CloudWatch backend.
$ awsemu logs create-log-group --log-group-name app/api
$ awsemu logs put-metric-filter --log-group-name app/api \
    --filter-name errors --filter-pattern 'ERROR' \
    --metric-transformations '[{
      "metricName":      "ApiErrorCount",
      "metricNamespace": "App/Api",
      "metricValue":     "1"
    }]'

$ awsemu logs put-log-events --log-group-name app/api --log-stream-name s1 \
    --log-events '[
      {"timestamp":'$(date +%s000)',"message":"ERROR boom"},
      {"timestamp":'$(date +%s000)',"message":"ERROR kaboom"}
    ]' >/dev/null

$ awsemu cloudwatch get-metric-statistics --namespace App/Api \
    --metric-name ApiErrorCount --statistics Sum \
    --start-time $(date -u -v-5M +%FT%TZ) --end-time $(date -u +%FT%TZ) \
    --period 60 --query 'Datapoints[].Sum'
[2.0]

Configuration

CloudWatch has no service-specific environment variables for metric or alarm behavior. Two related knobs control how other services emit metrics into CloudWatch:

VariableDefaultPurpose
SQS_DISABLE_CLOUDWATCH_METRICS0Set to 1 to stop SQS from reporting queue metrics (ApproximateNumberOfMessagesVisible, ...) to CloudWatch. Defined at config.py:932-935.
SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL60Seconds between SQS metric report cycles. Lower for tighter alarm feedback in tests. Defined at config.py:936.

Provider selection: PROVIDER_OVERRIDE_CLOUDWATCH=v1 falls back to the legacy moto-backed provider. The default and v2 both resolve to the SQLite-backed provider documented here.

Integration points

ServiceHow it touches CloudWatch
CloudWatch LogsMetric filters call PutMetricData on every matching log event.
SQSBackground reporter publishes per-queue metrics; gated by SQS_DISABLE_CLOUDWATCH_METRICS.
SNSAlarm AlarmActions deliver via sns.Publish; standard subscriptions (Lambda, HTTP, email-stub, SQS) fan out from there.
LambdaAlarm action targets invoked directly with the alarm-notification payload.
CloudFormationAWS::CloudWatch::Alarm and AWS::CloudWatch::CompositeAlarm resource providers at services/cloudwatch/resource_providers/.

Known limitations