| 1 | --- |
| 2 | title: CloudWatch |
| 3 | description: 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 | |
| 6 | CloudWatch is AWS's built-in metrics store: every Lambda function, |
| 7 | SQS queue, and DynamoDB table publishes metrics into it |
| 8 | automatically. In alchemy the consuming side is code too — a |
| 9 | `Dashboard` is a structured widget document and an `Alarm` is a |
| 10 | threshold on a metric, both declared in the same Stack as the |
| 11 | resources they watch. A tweaked threshold or a new graph is a diff in |
| 12 | the PR, not a console click. |
| 13 | |
| 14 | ## Dashboards as code |
| 15 | |
| 16 | `DashboardBody` is a plain object, not a JSON string — alchemy |
| 17 | serializes it to the document the CloudWatch API expects: |
| 18 | |
| 19 | ```typescript |
| 20 | import * as AWS from "alchemy/AWS"; |
| 21 | |
| 22 | const 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 | |
| 45 | Top-level props are PascalCase because they mirror the |
| 46 | `PutDashboard` API input; the widget document inside follows |
| 47 | CloudWatch'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 |
| 50 | Insights queries). Each takes optional `x`/`y`/`width`/`height` grid |
| 51 | coordinates. |
| 52 | |
| 53 | If you omit `name`, alchemy generates a unique dashboard name from |
| 54 | the app, stage, and logical ID — so `prod` and `pr-42` each get |
| 55 | their own copy. Renaming a dashboard replaces it. |
| 56 | |
| 57 | ## Wire in resource names |
| 58 | |
| 59 | Hard-coding `"api"` works until the function is renamed. Metric rows |
| 60 | reference metrics by dimension value, so build the body from the |
| 61 | function's [Output](/infrastructure-as-code/outputs) instead: |
| 62 | |
| 63 | ```typescript |
| 64 | import * as Output from "alchemy/Output"; |
| 65 | |
| 66 | const func = yield* Api; // an AWS.Lambda.Function |
| 67 | |
| 68 | const 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 |
| 92 | until the function's physical name is known, so the dashboard always |
| 93 | tracks the deployed function — including across replacements. |
| 94 | |
| 95 | ## Alarms |
| 96 | |
| 97 | An `Alarm` watches one metric and changes state when it crosses a |
| 98 | threshold. Props are PascalCase, extending the `PutMetricAlarm` API |
| 99 | input directly; `AlarmActions` is a list of ARN strings — most |
| 100 | commonly an [SNS](/aws/messaging/sns) topic that fans out to email, Slack, or |
| 101 | PagerDuty: |
| 102 | |
| 103 | ```typescript |
| 104 | const alerts = yield* AWS.SNS.Topic("Alerts"); |
| 105 | |
| 106 | const 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 |
| 121 | like `func.functionName` resolve in place. `TreatMissingData: |
| 122 | "notBreaching"` keeps a quiet function out of the `INSUFFICIENT_DATA` |
| 123 | state. The resolved alarm exposes `alarmName`, `alarmArn`, and its |
| 124 | current `stateValue` as outputs. |
| 125 | |
| 126 | Alarms show up on dashboards too — an `alarm` widget renders a |
| 127 | status 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 | |
| 140 | To aggregate several alarms into one page-worthy signal, |
| 141 | `CompositeAlarm` combines their states with a boolean rule: |
| 142 | |
| 143 | ```typescript |
| 144 | const composite = yield* AWS.CloudWatch.CompositeAlarm("HighSeverity", { |
| 145 | AlarmRule: 'ALARM("HighErrors") OR ALARM("HighLatency")', |
| 146 | }); |
| 147 | ``` |
| 148 | |
| 149 | Like dashboards, alarms get generated per-stage names when `name` is |
| 150 | omitted, and renaming one replaces it. |
| 151 | |
| 152 | ## Where metrics come from |
| 153 | |
| 154 | You rarely have to publish anything: AWS services emit metrics on |
| 155 | their own. Every Lambda function reports `Invocations`, `Errors`, |
| 156 | `Duration`, and `Throttles` under the `AWS/Lambda` namespace, keyed |
| 157 | by the `FunctionName` dimension — which is why the snippets above |
| 158 | work against a stock [Lambda](/aws/compute/lambda) function with zero |
| 159 | instrumentation. Other services follow suit (`AWS/SQS`, |
| 160 | `AWS/DynamoDB`, ...). |
| 161 | |
| 162 | For application-level signals — spans, structured logs, custom |
| 163 | metrics from your Effect code — see |
| 164 | [Observability](/testing/observability): Effect emits OpenTelemetry |
| 165 | natively, and the receiving end (datasets, monitors, notifiers) is |
| 166 | provisioned as resources in the same Stack, exactly like the alarms |
| 167 | here. |
| 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 | |
| 178 | Reference: |
| 179 | |
| 180 | - [`Alarm` reference](/providers/aws/cloudwatch/alarm) |
| 181 | - [`Dashboard` reference](/providers/aws/cloudwatch/dashboard) |
| 182 | - [`CompositeAlarm` reference](/providers/aws/cloudwatch/compositealarm) |