Skip to content

CloudWatch

CloudWatch is where AWS reports on itself. Every service publishes metrics to it without being asked: a Function’s invocations and errors, a Queue’s depth, a Table’s throttled requests. Every Function writes its logs to it. Alarms watch metrics and tell you when something crosses a line, and dashboards put the metrics you care about on one screen.

A metric is a series of numbers over time, identified by a namespace, a name and dimensions. Lambda reports Errors in the AWS/Lambda namespace, with a FunctionName dimension for each Function. SQS reports ApproximateNumberOfMessagesVisible per QueueName.

You don’t publish these. They exist as soon as the resource does, so an alarm or a dashboard can watch them from the first deploy.

Everything a Function writes to its output lands in a log group named after it, /aws/lambda/<name>. ECS tasks get a log group too. Set how long they’re kept on the runtime, or they’re kept forever:

src/api.ts
{ main: import.meta.url, logging: { retention: "2 weeks" } }

Read a runtime’s logs from your terminal:

Terminal window
alchemy logs Api --tail

An alarm watches one metric. When the metric crosses a threshold for long enough, the alarm moves to the ALARM state:

alchemy.run.ts
const api = yield* Api;
const errors = yield* AWS.CloudWatch.Alarm("ApiErrors", {
Namespace: "AWS/Lambda",
MetricName: "Errors",
Dimensions: [{ Name: "FunctionName", Value: api.functionName }],
Statistic: "Sum",
Period: 60,
EvaluationPeriods: 1,
Threshold: 1,
ComparisonOperator: "GreaterThanOrEqualToThreshold",
TreatMissingData: "notBreaching",
});

The dimension comes from the Function itself, so the alarm follows it across renames and stages. TreatMissingData: "notBreaching" keeps a Function that nobody’s calling out of the INSUFFICIENT_DATA state.

An alarm acts by publishing to an SNS Topic. Subscribe your email, a chat webhook or a pager to the Topic:

INSUFFICIENT_DATAOKALARMpublishes to Alertsdata arrivesErrors ≥ 1 in a minuteback under
An alarm watches one metric and changes state when it crosses the threshold. Moving into ALARM runs its actions, here a publish to the Alerts Topic.
alchemy.run.ts
const alerts = yield* AWS.SNS.Topic("Alerts");
const errors = yield* AWS.CloudWatch.Alarm("ApiErrors", {
Namespace: "AWS/Lambda",
MetricName: "Errors",
Dimensions: [{ Name: "FunctionName", Value: api.functionName }],
Statistic: "Sum",
Period: 60,
EvaluationPeriods: 1,
Threshold: 1,
ComparisonOperator: "GreaterThanOrEqualToThreshold",
AlarmActions: [alerts.topicArn],
});

Because alarms are declared in the Stack, each stage gets its own. Point production’s at a pager and every other stage’s at nothing.

A dashboard is a grid of widgets. Its body is a plain object, built from the Function’s name once it’s known:

alchemy.run.ts
yield* AWS.CloudWatch.Dashboard("Api", {
DashboardBody: api.functionName.pipe(
Output.map((functionName) => ({
widgets: [
{
type: "metric",
width: 12,
height: 6,
properties: {
title: "Invocations and errors",
stat: "Sum",
period: 300,
metrics: [
["AWS/Lambda", "Invocations", "FunctionName", functionName],
["AWS/Lambda", "Errors", "FunctionName", functionName],
],
},
},
],
})),
),
});

Widgets can also show text, a grid of alarm states, or a Logs Insights query. A change to a dashboard is a change in a pull request, reviewed like any other.

One failure often trips several alarms at once. A composite alarm combines their states with a rule, so you’re paged once:

alchemy.run.ts
yield* AWS.CloudWatch.CompositeAlarm("ApiUnhealthy", {
AlarmRule: Output.interpolate`ALARM("${errors.alarmName}") OR ALARM("${latency.alarmName}")`,
AlarmActions: [alerts.topicArn],
});

Alarm names are generated per stage, so build the rule from each alarm’s alarmName.

CloudWatch’s built-in metrics describe your resources from the outside. To see inside your code, its spans, structured logs and custom metrics, export OpenTelemetry from your Effect program. See Observability, and Axiom for a backend declared in the same Stack.