Skip to content

Commit 0fcf157

Browse files
author
Rodrigo Tuna
committed
RFC: Log Alarm L2 Construct (#960)
1 parent 338ca44 commit 0fcf157

1 file changed

Lines changed: 328 additions & 0 deletions

File tree

text/0960-logalarm-l2.md

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
# RFC: Log Alarm L2 Construct
2+
3+
* **Original Author(s):** @rtuna
4+
* **Tracking Issue:** https://github.com/aws/aws-cdk-rfcs/issues/960
5+
* **API Bar Raiser:** TBD
6+
7+
Add a new `LogAlarm` L2 construct to `aws-cdk-lib/aws-cloudwatch` so customers can define CloudWatch log alarms — alarms
8+
evaluated against the results of a scheduled CloudWatch Logs query — in CDK, without hand-writing the
9+
`AWS::CloudWatch::LogAlarm` L1 resource.
10+
11+
## Summary
12+
13+
`LogAlarm` lets customers alarm directly on logs. Instead of a metric + threshold + evaluation periods, a log alarm runs
14+
a scheduled Logs query (a query string + aggregation over one or more log groups) and compares the aggregated result
15+
against a threshold using an M-out-of-N evaluation. It also can surface matching log lines in the alarm notification.
16+
The construct wraps the `AWS::CloudWatch::LogAlarm` CloudFormation resource, reuses the existing
17+
`ComparisonOperator`/`TreatMissingData` enums, and — like `Alarm` and `PromQLAlarm` — extends `AlarmBase` so it is
18+
interchangeable anywhere an `IAlarm` is accepted.
19+
20+
---
21+
22+
## Working Backwards
23+
24+
`CHANGELOG: feat(cloudwatch): add Log Alarm L2 construct`
25+
26+
### README
27+
28+
#### Log Alarm
29+
30+
Create a CloudWatch alarm that evaluates a scheduled CloudWatch Logs query against one or more log groups and alarms on the aggregated results.
31+
32+
```ts
33+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
34+
import * as iam from 'aws-cdk-lib/aws-iam';
35+
import * as logs from 'aws-cdk-lib/aws-logs';
36+
import { Duration } from 'aws-cdk-lib';
37+
38+
declare const logGroup: logs.LogGroup;
39+
declare const queryRole: iam.IRole;
40+
41+
new cloudwatch.LogAlarm(this, 'ErrorRateAlarm', {
42+
threshold: 5,
43+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
44+
queryResultsToEvaluate: 3,
45+
queryResultsToAlarm: 2,
46+
scheduledQueryConfiguration: {
47+
queryString: 'fields @message | filter @message like /ERROR/',
48+
aggregationExpression: 'count(*)',
49+
logGroupIdentifiers: [logGroup.logGroupName],
50+
scheduledQueryRole: queryRole,
51+
schedule: {
52+
frequency: Duration.minutes(5),
53+
startTimeOffset: Duration.minutes(5),
54+
},
55+
},
56+
});
57+
```
58+
59+
##### Including Log Lines in Notifications
60+
61+
Set `actionLogLineCount` to include matching log lines in alarm notifications. When it is greater than 0, an
62+
`actionLogLineRole` is required so CloudWatch can read the log lines.
63+
64+
```ts
65+
new cloudwatch.LogAlarm(this, 'ErrorRateAlarm', {
66+
threshold: 5,
67+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
68+
queryResultsToEvaluate: 3,
69+
queryResultsToAlarm: 2,
70+
actionLogLineCount: 10,
71+
actionLogLineRole: logLineRole,
72+
scheduledQueryConfiguration: { /* ... */ },
73+
});
74+
```
75+
76+
##### Adding Actions
77+
78+
`LogAlarm` extends `AlarmBase`, so you add alarm, OK, and insufficient-data actions the same way as existing alarms:
79+
80+
```ts
81+
import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
82+
83+
declare const logAlarm: cloudwatch.LogAlarm;
84+
declare const topic: sns.Topic;
85+
86+
logAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(topic));
87+
```
88+
89+
##### Importing an Existing Log Alarm
90+
91+
```ts
92+
const imported = cloudwatch.LogAlarm.fromLogAlarmArn(
93+
this, 'ImportedAlarm',
94+
'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyLogAlarm',
95+
);
96+
97+
const importedByName = cloudwatch.LogAlarm.fromLogAlarmName(this, 'ImportedByName', 'MyLogAlarm');
98+
```
99+
100+
---
101+
102+
Ticking the box below indicates that the public API of this RFC has been
103+
signed-off by the API bar raiser (the `status/api-approved` label was applied to
104+
the RFC pull request):
105+
106+
```
107+
[ ] Signed-off by API Bar Raiser @xxxxx
108+
```
109+
110+
## Public FAQ
111+
112+
### What are we launching today?
113+
114+
A new `LogAlarm` L2 construct in the existing `aws-cdk-lib/aws-cloudwatch` module, wrapping the `AWS::CloudWatch::LogAlarm` CloudFormation resource.
115+
116+
### What is a log alarm?
117+
118+
A log alarm evaluates a scheduled CloudWatch Logs query against one or more log groups on a recurring schedule and
119+
alarms on an aggregation of the results (for example `count(*)`). It removes the old two-step pattern (metric filter →
120+
metric alarm) and can attach the matching log lines directly to the alarm notification.
121+
122+
### Why should I use this feature?
123+
124+
Customers monitoring logs previously had to create a metric filter and then a metric alarm, and then manually go back to
125+
the log group to find the offending log lines. `LogAlarm` collapses that into a single construct and can surface the
126+
triggering log lines in the notification, reducing MTTR.
127+
128+
### Why do I need a new construct instead of the existing `Alarm`?
129+
130+
The `Alarm` construct is built around a metric, comparison operator, threshold, and evaluation periods. A log alarm has
131+
a different model: instead of a metric it runs a scheduled query (`scheduledQueryConfiguration`), and instead of
132+
`evaluationPeriods`/`datapointsToAlarm` it uses `queryResultsToEvaluate` (N) and `queryResultsToAlarm` (M). A dedicated
133+
construct gives a clean, type-safe API without overloading `Alarm` with mutually exclusive property groups.
134+
135+
---
136+
137+
## Internal FAQ
138+
139+
### Why are we doing this?
140+
141+
Customers who want to alarm on logs currently must create a metric filter and then a metric alarm, and then manually
142+
return to the log group to find the log lines that triggered the alarm. CloudWatch launched `AWS::CloudWatch::LogAlarm`
143+
to collapse this into a single resource that runs a scheduled Logs query and can surface the triggering log lines in the
144+
notification. Without an L2, CDK users must hand-write the L1 `CfnLogAlarm`, manually render the `rate(...)` schedule
145+
expression, wire the IAM role ARN, and discover the service's validation rules by trial and error. An L2 provides a
146+
type-safe, validated, idiomatic API consistent with the existing `Alarm` and `PromQLAlarm` constructs.
147+
148+
### Why should we _not_ do this?
149+
150+
The only reason not to is that the `ScheduledQueryConfiguration` field constraints are still being finalised upstream
151+
(see open issues) — the schema could shift. This is mitigated by tracking the published `@aws-cdk/aws-service-spec` and
152+
adjusting the L2 as the schema settles; the core shape (query + aggregation + schedule + M-of-N) is stable.
153+
154+
### What is the technical solution (design) of this feature?
155+
156+
`LogAlarm` is a new class extending `AlarmBase` (mirroring `Alarm`, `CompositeAlarm`, and `PromQLAlarm`), so it is
157+
interchangeable anywhere an `IAlarm` is accepted:
158+
159+
```
160+
IAlarm (extends IResource)
161+
└── AlarmBase (abstract)
162+
├── Alarm
163+
├── CompositeAlarm
164+
├── PromQLAlarm
165+
└── LogAlarm ← NEW
166+
```
167+
168+
It maps to the `AWS::CloudWatch::LogAlarm` resource (a distinct resource type, unlike `PromQLAlarm` which reuses
169+
`AWS::CloudWatch::Alarm`). The full API is in the Proposed API Design section below. Key design decisions:
170+
171+
- **`Duration` for the schedule** (not raw `number` seconds as `PromQLAlarm` used) — per the CDK Design Guidelines'
172+
preference for strong types. `frequency` is rendered to a `rate(...)` schedule expression and must be a whole number of
173+
minutes ≥ 1 (validated at synth); `startTimeOffset`/`endTimeOffset` convert to the integer seconds the L1 expects.
174+
- **`startTimeOffset` is required** — the scheduled-query service rejects a null start-time offset, so requiring it fails
175+
fast at synth instead of at deploy (per the guideline on synth-time validation of always-fail-at-deploy input).
176+
- **`logGroupIdentifiers: string[]` (not `ILogGroup[]`)**`aws-logs` already depends on `aws-cloudwatch`, so importing
177+
`ILogGroup` into `aws-cloudwatch` would create a circular module dependency and break the jsii build (which is why no
178+
`aws-cloudwatch` construct references `aws-logs`). `string[]` also matches how CWLI `SOURCE`-prefix queries identify log
179+
groups.
180+
181+
A proof-of-concept construct with unit tests and an integration test has been implemented and deployed against a real
182+
account, confirming it creates a valid `AWS::CloudWatch::LogAlarm`.
183+
184+
### Is this a breaking change?
185+
186+
No. This is a new construct; it does not alter any existing API.
187+
188+
### What alternative solutions did you consider?
189+
190+
1. **Extend `Alarm` with optional log-alarm props** — rejected. Would require making most `AlarmProps` optional and adding
191+
mutual-exclusivity validation, degrading the type system for both standard and log alarms.
192+
2. **A static factory on `Alarm`** (e.g. `Alarm.fromLogQuery(...)`) — rejected. Hides the different configuration model and
193+
still needs to return a distinct type.
194+
3. **New `LogAlarm extends AlarmBase`** — chosen. Clean dedicated surface, interchangeable via `IAlarm`, consistent with
195+
the accepted `PromQLAlarm` precedent.
196+
197+
### What are the drawbacks of this solution?
198+
199+
The `ScheduledQueryConfiguration` field constraints are still being finalised upstream (notably whether
200+
`LogGroupIdentifiers` is required and the exact `StartTimeOffset` range). The construct follows the published
201+
`@aws-cdk/aws-service-spec` and will be adjusted as the schema settles.
202+
203+
### What is the high-level project plan?
204+
205+
- [x] Implement the `LogAlarm` construct, unit tests, integration test, and README
206+
- [x] Validate against `aws-cdk-lib` built from `@aws-cdk/aws-service-spec` 0.1.190
207+
- [x] Deploy the integration test to a real account and confirm resource creation
208+
- [ ] RFC review and API bar-raiser sign-off
209+
- [ ] Open the public `aws/aws-cdk` PR
210+
- [ ] Address review feedback, merge, and release
211+
212+
### Are there any open issues that need to be addressed later?
213+
214+
- Alignment of `ScheduledQueryConfiguration` field constraints across the Coral model, CFN schema, and Hermes validation
215+
(tracked in EVENTS-4979): `LogGroupIdentifiers` required→optional, `StartTimeOffset` range `[1, 2592000]`,
216+
`AggregationExpression` maxLength 256→2048. The L2 will follow the published spec.
217+
218+
---
219+
220+
## Proposed API Design
221+
222+
### `LogAlarmProps`
223+
224+
```ts
225+
export interface LogAlarmProps {
226+
readonly threshold: number;
227+
readonly comparisonOperator: ComparisonOperator; // static-threshold operators only
228+
readonly queryResultsToEvaluate: number; // N (positive integer)
229+
readonly queryResultsToAlarm: number; // M (positive integer, ≤ N)
230+
readonly scheduledQueryConfiguration: ScheduledQueryConfiguration;
231+
232+
readonly alarmName?: string; // @default - generated
233+
readonly alarmDescription?: string; // @default - none
234+
readonly actionsEnabled?: boolean; // @default true
235+
readonly treatMissingData?: TreatMissingData; // @default - service default
236+
readonly actionLogLineCount?: number; // 0–50; requires role when > 0
237+
readonly actionLogLineRole?: IRole;
238+
readonly alarmActions?: IAlarmAction[];
239+
readonly okActions?: IAlarmAction[];
240+
readonly insufficientDataActions?: IAlarmAction[];
241+
}
242+
243+
export interface ScheduledQueryConfiguration {
244+
readonly queryString: string;
245+
readonly aggregationExpression: string; // e.g. count(*)
246+
readonly logGroupIdentifiers: string[]; // names, ARNs, or CWLI SOURCE patterns
247+
readonly scheduledQueryRole: IRole;
248+
readonly schedule: ScheduledQuerySchedule;
249+
}
250+
251+
export interface ScheduledQuerySchedule {
252+
readonly frequency: Duration; // rendered to rate(...); whole minutes
253+
readonly startTimeOffset: Duration; // required by the service
254+
readonly endTimeOffset?: Duration; // @default - none
255+
}
256+
```
257+
258+
### `LogAlarm`
259+
260+
```ts
261+
@propertyInjectable
262+
export class LogAlarm extends AlarmBase {
263+
public static readonly PROPERTY_INJECTION_ID: string;
264+
public static fromLogAlarmArn(scope: Construct, id: string, alarmArn: string): IAlarm;
265+
public static fromLogAlarmName(scope: Construct, id: string, alarmName: string): IAlarm;
266+
public get alarmArn(): string;
267+
public get alarmName(): string;
268+
constructor(scope: Construct, id: string, props: LogAlarmProps);
269+
}
270+
```
271+
272+
### Validation performed at synth
273+
274+
- `comparisonOperator` must be a static-threshold operator (anomaly-detection operators rejected).
275+
- `queryResultsToEvaluate` / `queryResultsToAlarm` positive integers; `queryResultsToAlarm ≤ queryResultsToEvaluate`.
276+
- `actionLogLineCount` integer in `[0, 50]`; requires `actionLogLineRole` when `> 0`.
277+
- `logGroupIdentifiers` length in `[1, 50]`.
278+
- `schedule.frequency` a whole number of minutes ≥ 1.
279+
280+
---
281+
282+
## CloudFormation Mapping
283+
284+
```ts
285+
new cloudwatch.LogAlarm(this, 'ErrorRateAlarm', {
286+
threshold: 5,
287+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
288+
queryResultsToEvaluate: 3,
289+
queryResultsToAlarm: 2,
290+
scheduledQueryConfiguration: {
291+
queryString: 'fields @message | filter @message like /ERROR/',
292+
aggregationExpression: 'count(*)',
293+
logGroupIdentifiers: [logGroup.logGroupName],
294+
scheduledQueryRole: queryRole,
295+
schedule: { frequency: Duration.minutes(5), startTimeOffset: Duration.minutes(5) },
296+
},
297+
});
298+
```
299+
300+
Synthesizes to:
301+
302+
```yaml
303+
Type: AWS::CloudWatch::LogAlarm
304+
Properties:
305+
ComparisonOperator: GreaterThanThreshold
306+
Threshold: 5
307+
QueryResultsToEvaluate: 3
308+
QueryResultsToAlarm: 2
309+
ScheduledQueryConfiguration:
310+
QueryString: "fields @message | filter @message like /ERROR/"
311+
AggregationExpression: "count(*)"
312+
LogGroupIdentifiers: [ { "Ref": "LogGroup..." } ]
313+
ScheduledQueryRoleARN: { "Fn::GetAtt": ["ScheduledQueryRole...", "Arn"] }
314+
ScheduleConfiguration:
315+
ScheduleExpression: "rate(5 minutes)"
316+
StartTimeOffset: 300
317+
```
318+
319+
---
320+
321+
## Appendix
322+
323+
### References
324+
325+
- [AWS::CloudWatch::LogAlarm — CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-logalarm.html)
326+
- [CDK Design Guidelines](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)
327+
- [Existing Alarm L2 source](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-cloudwatch/lib/alarm.ts)
328+
- [AlarmBase source](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-cloudwatch/lib/alarm-base.ts)

0 commit comments

Comments
 (0)