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
$ 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:
- •Metric data: SQLite at
cloudwatch_database_helper.py:30. One database file per process, lock-guarded for writes, opened read-only (URI form) for reads. Separate tables for singleton metrics (rawValue) and aggregated metrics (StatisticValues), each indexed by namespace, metric name, dimensions, and timestamp. - •Alarms, dashboards, history, tags:
CloudWatchStoreatmodels.py:105, accessed via thecloudwatch_storesAccountRegionBundle. Alarm history is a bounded deque (10k entries) so long-running stacks do not grow unboundedly. - •Alarm scheduler: real thread at
alarm_scheduler.py:35-96.on_before_startspawns the scheduler thread;on_before_stopand the registeredSHUTDOWN_HANDLERSentry join it cleanly. Each alarm is a fixed-rate task whose period isEvaluationPeriods * Periodseconds.
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
| Feature | Notes |
|---|---|
| Metric ingest | PutMetricData. Accepts raw Value or pre-aggregated StatisticValues; Values+Counts arrays must be same length (validated at provider_v2.py:118-137). |
| Metric query | GetMetricStatistics (per-period aggregation, multi-statistic), GetMetricData (multi-query batch, NextToken pagination, Label override), ListMetrics. |
| Metric alarms | PutMetricAlarm, DeleteAlarms, DescribeAlarms, DescribeAlarmsForMetric, DescribeAlarmHistory, EnableAlarmActions, DisableAlarmActions, SetAlarmState. |
| Comparison operators | GreaterThanOrEqualToThreshold, GreaterThanThreshold, LessThanThreshold, LessThanOrEqualToThreshold (alarm_scheduler.py:23-28). Anomaly-detection operators not in this set. |
| Alarm actions | SNS Publish and Lambda Invoke dispatched on every state transition with ActionsEnabled=true (provider_v2.py:361-378). OKActions, AlarmActions, InsufficientDataActions all honored. |
| Composite alarms | PutCompositeAlarm + 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. |
| TreatMissingData | missing, breaching, notBreaching, ignore (validated at provider_v2.py:429-437). |
| Dashboards | PutDashboard, GetDashboard, ListDashboards, DeleteDashboards. JSON body stored verbatim; widget rendering is the client's job. |
| Tags | TagResource, UntagResource, ListTagsForResource. Alarm tags are auto-removed on DeleteAlarms (provider_v2.py:205). |
| Persistence | SQLite metric data + alarms + dashboards + history + tags all survive PERSISTENCE=1 restart. Alarms are re-scheduled on load. |
| Internal endpoint | GET /_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:
- •Checks that the alarm has
Period,Statistic,MetricName,Threshold, and a supportedComparisonOperator(_is_alarm_supportedatalarm_scheduler.py:119-134). Alarms missing these (notablyExtendedStatisticand metric-math alarms) are accepted on the API but not evaluated; a warning is logged. - •Registers a fixed-rate task with the scheduler at
period = EvaluationPeriods * Periodseconds. - •On each tick,
calculate_alarm_statequeries the SQLite store for the alarm's metric over the lookback window, applies the statistic + comparison, and produces a newStateValue:OK,ALARM, orINSUFFICIENT_DATA. - •On a state transition,
_update_state(provider_v2.py:298-350) writes the new state + reason + reason-data to the store, appends anAlarmHistoryItem, evaluates any composite alarms that depend on it, and fires the relevant action list. - •Action dispatch is per-action ARN:
arn:aws:sns:...→sns.Publish(provider_v2.py:364-370),arn:aws:lambda:...→lambda.Invoke(371-378). Other AWS action services (Auto Scaling, EC2 reboot, SSM OpsItem) are logged as unsupported and skipped. - •
SetAlarmStatebypasses the scheduler and directly drives a transition, useful for synthetic alarm testing.
$ 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:
$ 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.
$ 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.
$ 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.
# 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:
| Variable | Default | Purpose |
|---|---|---|
SQS_DISABLE_CLOUDWATCH_METRICS | 0 | Set to 1 to stop SQS from reporting queue metrics (ApproximateNumberOfMessagesVisible, ...) to CloudWatch. Defined at config.py:932-935. |
SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL | 60 | Seconds 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
| Service | How it touches CloudWatch |
|---|---|
| CloudWatch Logs | Metric filters call PutMetricData on every matching log event. |
| SQS | Background reporter publishes per-queue metrics; gated by SQS_DISABLE_CLOUDWATCH_METRICS. |
| SNS | Alarm AlarmActions deliver via sns.Publish; standard subscriptions (Lambda, HTTP, email-stub, SQS) fan out from there. |
| Lambda | Alarm action targets invoked directly with the alarm-notification payload. |
| CloudFormation | AWS::CloudWatch::Alarm and AWS::CloudWatch::CompositeAlarm resource providers at services/cloudwatch/resource_providers/. |
Known limitations
- •Metric-math alarms are not evaluated.
PutMetricAlarmwithMetricscontaining anExpressionis accepted at the API but rejected by_is_alarm_supported(alarm_scheduler.py:119-134): the alarm stays inINSUFFICIENT_DATAindefinitely. A warning is logged at create time. - •ExtendedStatistic alarms (
p99,tm99, ...) are not evaluated. Same path: accepted at the API, not scheduled. - •Composite alarm rule grammar is restricted to OR + ALARM-state expressions.
ANDand threshold-comparison rules (e.g.ALARM("a") AND ALARM("b")) are not parsed; this is called out atprovider_v2.py:145-149. - •Anomaly-detection comparison operators are not supported.
LessThanLowerOrGreaterThanUpperThreshold,LessThanLowerThreshold,GreaterThanUpperThresholdare not inCOMPARISON_OPS(alarm_scheduler.py:19-28). - •Alarm actions are limited to SNS and Lambda. Auto Scaling policies, EC2 reboot/stop/terminate, SSM OpsItem, and other action services are logged as unsupported and skipped (
provider_v2.py:380-385). - •Anomaly detection is not implemented.
PutAnomalyDetector,DescribeAnomalyDetectors,DeleteAnomalyDetectorreturnNotImplemented. - •Contributor Insights is not implemented. The 8-op InsightRule surface (
PutInsightRule,GetInsightRuleReport, ...) returnsNotImplemented. - •Metric Streams (to Firehose) is not implemented. The 6-op stream surface (
PutMetricStream,StartMetricStreams, ...) returnsNotImplemented. - •Alarm Mute Rules are not implemented. The 4-op mute-rule surface (
PutAlarmMuteRule,GetAlarmMuteRule, ...) returnsNotImplemented. - •
GetMetricWidgetImage(dashboard PNG render) is not implemented. - •Known limitation: EntityMetricData and
StrictEntityValidationonPutMetricDataare not enforced. The fields are accepted; entity-scoped validation is a no-op. - •
DescribeAlarmsdoes not paginate. All alarms in the region are returned in a single response.