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.
Metrics come from every service
Section titled “Metrics come from every service”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.
Logs go to log groups
Section titled “Logs go to log groups”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:
{ main: import.meta.url, logging: { retention: "2 weeks" } }Read a runtime’s logs from your terminal:
alchemy logs Api --tailAlarm on a metric
Section titled “Alarm on a metric”An alarm watches one metric. When the metric crosses a threshold for long enough, the alarm moves to the ALARM state:
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.
Be told when it fires
Section titled “Be told when it fires”An alarm acts by publishing to an SNS Topic. Subscribe your email, a chat webhook or a pager to the Topic:
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.
Put metrics on a dashboard
Section titled “Put metrics on a dashboard”A dashboard is a grid of widgets. Its body is a plain object, built from the Function’s name once it’s known:
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.
Combine alarms
Section titled “Combine alarms”One failure often trips several alarms at once. A composite alarm combines their states with a rule, so you’re paged once:
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.
Your own telemetry
Section titled “Your own telemetry”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.