Skip to content

CloudWatch

CloudWatch is AWS’s built-in metrics store: every Lambda function, SQS queue, and DynamoDB table publishes metrics into it automatically. In alchemy the consuming side is code too — a Dashboard is a structured widget document and an Alarm is a threshold on a metric, both declared in the same Stack as the resources they watch. A tweaked threshold or a new graph is a diff in the PR, not a console click.

DashboardBody is a plain object, not a JSON string — alchemy serializes it to the document the CloudWatch API expects:

import * as AWS from "alchemy/AWS";
const dashboard = yield* AWS.CloudWatch.Dashboard("ApiHealth", {
DashboardBody: {
widgets: [
{
type: "metric",
width: 12,
height: 6,
properties: {
title: "Lambda invocations and errors",
view: "timeSeries",
stat: "Sum",
period: 300,
metrics: [
["AWS/Lambda", "Invocations", "FunctionName", "api"],
["AWS/Lambda", "Errors", "FunctionName", "api"],
],
},
},
],
},
});

Top-level props are PascalCase because they mirror the PutDashboard API input; the widget document inside follows CloudWatch’s own dashboard-body schema (type, properties, metrics, …). Four widget types are supported: metric (graphs), text (markdown), alarm (alarm status grids), and log (Logs Insights queries). Each takes optional x/y/width/height grid coordinates.

If you omit name, alchemy generates a unique dashboard name from the app, stage, and logical ID — so prod and pr-42 each get their own copy. Renaming a dashboard replaces it.

Hard-coding "api" works until the function is renamed. Metric rows reference metrics by dimension value, so build the body from the function’s Output instead:

import * as Output from "alchemy/Output";
const func = yield* Api; // an AWS.Lambda.Function
const dashboard = yield* AWS.CloudWatch.Dashboard("ApiDashboard", {
DashboardBody: func.functionName.pipe(
Output.map((functionName) => ({
widgets: [
{
type: "metric",
width: 12,
height: 6,
properties: {
title: "Lambda Duration",
stat: "Average",
period: 300,
metrics: [
["AWS/Lambda", "Duration", "FunctionName", functionName],
],
},
},
],
})),
),
});

Output.map defers building the document until the function’s physical name is known, so the dashboard always tracks the deployed function — including across replacements.

An Alarm watches one metric and changes state when it crosses a threshold. Props are PascalCase, extending the PutMetricAlarm API input directly; AlarmActions is a list of ARN strings — most commonly an SNS topic that fans out to email, Slack, or PagerDuty:

const alerts = yield* AWS.SNS.Topic("Alerts");
const errors = yield* AWS.CloudWatch.Alarm("HighErrors", {
MetricName: "Errors",
Namespace: "AWS/Lambda",
Statistic: "Sum",
Period: 60,
EvaluationPeriods: 1,
Threshold: 1,
ComparisonOperator: "GreaterThanOrEqualToThreshold",
TreatMissingData: "notBreaching",
Dimensions: [{ Name: "FunctionName", Value: func.functionName }],
AlarmActions: [alerts.topicArn],
});

Dimensions scopes the alarm to one function, and Output references like func.functionName resolve in place. TreatMissingData: "notBreaching" keeps a quiet function out of the INSUFFICIENT_DATA state. The resolved alarm exposes alarmName, alarmArn, and its current stateValue as outputs.

Alarms show up on dashboards too — an alarm widget renders a status grid from ARNs:

{
type: "alarm",
width: 12,
properties: {
title: "Alarms",
alarms: [errors.alarmArn],
},
},

To aggregate several alarms into one page-worthy signal, CompositeAlarm combines their states with a boolean rule:

const composite = yield* AWS.CloudWatch.CompositeAlarm("HighSeverity", {
AlarmRule: 'ALARM("HighErrors") OR ALARM("HighLatency")',
});

Like dashboards, alarms get generated per-stage names when name is omitted, and renaming one replaces it.

You rarely have to publish anything: AWS services emit metrics on their own. Every Lambda function reports Invocations, Errors, Duration, and Throttles under the AWS/Lambda namespace, keyed by the FunctionName dimension — which is why the snippets above work against a stock Lambda function with zero instrumentation. Other services follow suit (AWS/SQS, AWS/DynamoDB, …).

For application-level signals — spans, structured logs, custom metrics from your Effect code — see Observability: Effect emits OpenTelemetry natively, and the receiving end (datasets, monitors, notifiers) is provisioned as resources in the same Stack, exactly like the alarms here.

  • Observability — OTel exporters as Layers, plus the cross-cloud version of alerts-in-code.
  • Lambda — the functions these dashboards and alarms watch.
  • Axiom — datasets, monitors, and notifiers when you want telemetry outside CloudWatch.

Reference: