1---
2title: CloudWatch
3description: Declare CloudWatch dashboards as structured widget documents and metric alarms that fire SNS topics — versioned next to the Lambda functions and queues they observe.
4---
5
6CloudWatch is AWS's built-in metrics store: every Lambda function,
7SQS queue, and DynamoDB table publishes metrics into it
8automatically. In alchemy the consuming side is code too — a
9`Dashboard` is a structured widget document and an `Alarm` is a
10threshold on a metric, both declared in the same Stack as the
11resources they watch. A tweaked threshold or a new graph is a diff in
12the PR, not a console click.
13
14## Dashboards as code
15
16`DashboardBody` is a plain object, not a JSON string — alchemy
17serializes it to the document the CloudWatch API expects:
18
19```typescript
20import * as AWS from "alchemy/AWS";
21
22const dashboard = yield* AWS.CloudWatch.Dashboard("ApiHealth", {
23 DashboardBody: {
24 widgets: [
25 {
26 type: "metric",
27 width: 12,
28 height: 6,
29 properties: {
30 title: "Lambda invocations and errors",
31 view: "timeSeries",
32 stat: "Sum",
33 period: 300,
34 metrics: [
35 ["AWS/Lambda", "Invocations", "FunctionName", "api"],
36 ["AWS/Lambda", "Errors", "FunctionName", "api"],
37 ],
38 },
39 },
40 ],
41 },
42});
43```
44
45Top-level props are PascalCase because they mirror the
46`PutDashboard` API input; the widget document inside follows
47CloudWatch's own dashboard-body schema (`type`, `properties`,
48`metrics`, ...). Four widget types are supported: `metric` (graphs),
49`text` (markdown), `alarm` (alarm status grids), and `log` (Logs
50Insights queries). Each takes optional `x`/`y`/`width`/`height` grid
51coordinates.
52
53If you omit `name`, alchemy generates a unique dashboard name from
54the app, stage, and logical ID — so `prod` and `pr-42` each get
55their own copy. Renaming a dashboard replaces it.
56
57## Wire in resource names
58
59Hard-coding `"api"` works until the function is renamed. Metric rows
60reference metrics by dimension value, so build the body from the
61function's [Output](/infrastructure-as-code/outputs) instead:
62
63```typescript
64import * as Output from "alchemy/Output";
65
66const func = yield* Api; // an AWS.Lambda.Function
67
68const dashboard = yield* AWS.CloudWatch.Dashboard("ApiDashboard", {
69 DashboardBody: func.functionName.pipe(
70 Output.map((functionName) => ({
71 widgets: [
72 {
73 type: "metric",
74 width: 12,
75 height: 6,
76 properties: {
77 title: "Lambda Duration",
78 stat: "Average",
79 period: 300,
80 metrics: [
81 ["AWS/Lambda", "Duration", "FunctionName", functionName],
82 ],
83 },
84 },
85 ],
86 })),
87 ),
88});
89```
90
91`Output.map` defers building the document
92until the function's physical name is known, so the dashboard always
93tracks the deployed function — including across replacements.
94
95## Alarms
96
97An `Alarm` watches one metric and changes state when it crosses a
98threshold. Props are PascalCase, extending the `PutMetricAlarm` API
99input directly; `AlarmActions` is a list of ARN strings — most
100commonly an [SNS](/aws/messaging/sns) topic that fans out to email, Slack, or
101PagerDuty:
102
103```typescript
104const alerts = yield* AWS.SNS.Topic("Alerts");
105
106const errors = yield* AWS.CloudWatch.Alarm("HighErrors", {
107 MetricName: "Errors",
108 Namespace: "AWS/Lambda",
109 Statistic: "Sum",
110 Period: 60,
111 EvaluationPeriods: 1,
112 Threshold: 1,
113 ComparisonOperator: "GreaterThanOrEqualToThreshold",
114 TreatMissingData: "notBreaching",
115 Dimensions: [{ Name: "FunctionName", Value: func.functionName }],
116 AlarmActions: [alerts.topicArn],
117});
118```
119
120`Dimensions` scopes the alarm to one function, and Output references
121like `func.functionName` resolve in place. `TreatMissingData:
122"notBreaching"` keeps a quiet function out of the `INSUFFICIENT_DATA`
123state. The resolved alarm exposes `alarmName`, `alarmArn`, and its
124current `stateValue` as outputs.
125
126Alarms show up on dashboards too — an `alarm` widget renders a
127status grid from ARNs:
128
129```typescript
130{
131 type: "alarm",
132 width: 12,
133 properties: {
134 title: "Alarms",
135 alarms: [errors.alarmArn],
136 },
137},
138```
139
140To aggregate several alarms into one page-worthy signal,
141`CompositeAlarm` combines their states with a boolean rule:
142
143```typescript
144const composite = yield* AWS.CloudWatch.CompositeAlarm("HighSeverity", {
145 AlarmRule: 'ALARM("HighErrors") OR ALARM("HighLatency")',
146});
147```
148
149Like dashboards, alarms get generated per-stage names when `name` is
150omitted, and renaming one replaces it.
151
152## Where metrics come from
153
154You rarely have to publish anything: AWS services emit metrics on
155their own. Every Lambda function reports `Invocations`, `Errors`,
156`Duration`, and `Throttles` under the `AWS/Lambda` namespace, keyed
157by the `FunctionName` dimension — which is why the snippets above
158work against a stock [Lambda](/aws/compute/lambda) function with zero
159instrumentation. Other services follow suit (`AWS/SQS`,
160`AWS/DynamoDB`, ...).
161
162For application-level signals — spans, structured logs, custom
163metrics from your Effect code — see
164[Observability](/testing/observability): Effect emits OpenTelemetry
165natively, and the receiving end (datasets, monitors, notifiers) is
166provisioned as resources in the same Stack, exactly like the alarms
167here.
168
169## Where next
170
171- [Observability](/testing/observability) — OTel exporters as
172 Layers, plus the cross-cloud version of alerts-in-code.
173- [Lambda](/aws/compute/lambda) — the functions these dashboards and alarms
174 watch.
175- [Axiom](/axiom) — datasets, monitors, and notifiers when you want
176 telemetry outside CloudWatch.
177
178Reference:
179
180- [`Alarm` reference](/providers/aws/cloudwatch/alarm)
181- [`Dashboard` reference](/providers/aws/cloudwatch/dashboard)
182- [`CompositeAlarm` reference](/providers/aws/cloudwatch/compositealarm)