Skip to content

Commit 3ac95b1

Browse files
authored
feat(alarm): add MathExpression support for minSampleCountToEvaluateDatapoint (#466)
As discussed outside of Github, this PR adds MathExpression support for minSampleCountToEvaluateDatapoint under the following circumstances: 1. If the MathExpression only has a single metric, convert to a sampleCount metric and use it for the minSampleCount 2. If there are more than one metric, rely on the new optional `sampleCountMetricId` prop to determine which existing metric in the MathExpression should be used for the sampleCount 3. Otherwise, throw an `Error`
1 parent 68e0768 commit 3ac95b1

File tree

3 files changed

+211
-18
lines changed

3 files changed

+211
-18
lines changed

API.md

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/common/alarm/AlarmFactory.ts

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
CompositeAlarm,
88
HorizontalAnnotation,
99
IAlarmRule,
10+
IMetric,
1011
MathExpression,
12+
Metric,
1113
TreatMissingData,
1214
} from "aws-cdk-lib/aws-cloudwatch";
1315
import { Construct } from "constructs";
@@ -199,6 +201,20 @@ export interface AddAlarmProps {
199201
*/
200202
readonly minSampleCountToEvaluateDatapoint?: number;
201203

204+
/**
205+
* This property is required in the following situation:
206+
* <ol>
207+
* <li><code>minSampleCountToEvaluateDatapoint</code> is specified</li>
208+
* <li>the metric used for the alarm is a <code>MathExpression</code></li>
209+
* <li>the <code>MathExpression</code> is composed of more than one metric</li>
210+
* </ol>
211+
*
212+
* In this situation, this property indicates the metric Id in the MathExpression’s <code>usingMetrics</code>
213+
* property that should be used as the sampleCount metric for the new MathExpression as described in the documentation
214+
* for <code>minSampleCountToEvaluateDatapoint</code>.
215+
*/
216+
readonly sampleCountMetricId?: string;
217+
202218
/**
203219
* Specifies how many samples (N) of the metric is needed to trigger the alarm.
204220
* If this property is specified, an artificial composite alarm is created of the following:
@@ -571,27 +587,55 @@ export class AlarmFactory {
571587
// apply metric math for minimum metric samples
572588

573589
if (props.minSampleCountToEvaluateDatapoint) {
590+
let label: string = `${adjustedMetric}`;
591+
let metricExpression: string;
592+
let metricSampleCountId: string = "sampleCount";
593+
let usingMetrics: Record<string, IMetric>;
594+
574595
if (adjustedMetric instanceof MathExpression) {
575-
throw new Error(
576-
"minSampleCountToEvaluateDatapoint is not supported for MathExpressions. " +
577-
"If you already use MathExpression, you can extend your expression to evaluate " +
578-
"the sample count using IF statement, e.g. IF(sampleCount > X, mathExpression)."
579-
);
596+
label = adjustedMetric.label ?? label;
597+
metricExpression = `(${adjustedMetric.expression})`;
598+
599+
if (Object.keys(adjustedMetric.usingMetrics).length === 1) {
600+
const sampleCountMetric = (
601+
adjustedMetric.usingMetrics[
602+
Object.keys(adjustedMetric.usingMetrics)[0]
603+
] as Metric
604+
).with({
605+
statistic: MetricStatistic.N,
606+
label: "Sample count",
607+
});
608+
609+
usingMetrics = {
610+
...adjustedMetric.usingMetrics,
611+
[metricSampleCountId]: sampleCountMetric,
612+
};
613+
} else if (props.sampleCountMetricId) {
614+
usingMetrics = adjustedMetric.usingMetrics;
615+
metricSampleCountId = props.sampleCountMetricId;
616+
} else {
617+
throw new Error(
618+
"sampleCountMetricId must be specified when using minSampleCountToEvaluateDatapoint with a multiple-metric MathExpression"
619+
);
620+
}
621+
} else {
622+
const metricId: string = "metric";
623+
624+
metricExpression = metricId;
625+
usingMetrics = {
626+
[metricId]: adjustedMetric,
627+
[metricSampleCountId]: adjustedMetric.with({
628+
statistic: MetricStatistic.N,
629+
label: "Sample count",
630+
}),
631+
};
580632
}
581633

582-
const metricSampleCount = adjustedMetric.with({
583-
statistic: MetricStatistic.N,
584-
label: "Sample count",
585-
});
586-
587634
alarmMetric = new MathExpression({
588-
label: `${adjustedMetric}`,
589-
expression: `IF(sampleCount > ${props.minSampleCountToEvaluateDatapoint}, metric)`,
635+
label,
636+
expression: `IF(${metricSampleCountId} > ${props.minSampleCountToEvaluateDatapoint}, ${metricExpression})`,
590637
period: adjustedMetric.period,
591-
usingMetrics: {
592-
metric: adjustedMetric,
593-
sampleCount: metricSampleCount,
594-
},
638+
usingMetrics,
595639
});
596640
}
597641

test/common/alarm/AlarmFactory.test.ts

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
CompositeAlarmOperator,
2121
IAlarmNamingStrategy,
2222
MetricFactoryDefaults,
23+
MetricStatistic,
2324
multipleActions,
2425
noopAction,
2526
SnsAlarmActionStrategy,
@@ -389,7 +390,7 @@ test("addAlarm: check created alarms when minSampleCountToEvaluateDatapoint is u
389390
});
390391
});
391392

392-
test("addAlarm: minSampleCountToEvaluateDatapoint used with Math Expression throws error", () => {
393+
test("addAlarm: check created alarms when minSampleCountToEvaluateDatapoint is used with single-metric MathExpression", () => {
393394
const stack = new Stack();
394395
const factory = new AlarmFactory(stack, {
395396
globalMetricDefaults,
@@ -398,21 +399,152 @@ test("addAlarm: minSampleCountToEvaluateDatapoint used with Math Expression thro
398399
});
399400
const mathExpression = new MathExpression({
400401
expression: "MAX(metric)",
402+
label: "max",
401403
usingMetrics: {
402404
metric,
403405
},
404406
});
405407

408+
factory.addAlarm(mathExpression, {
409+
...props,
410+
alarmNameSuffix: "none",
411+
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
412+
minSampleCountToEvaluateDatapoint: 42,
413+
period: Duration.minutes(15),
414+
});
415+
416+
const template = Template.fromStack(stack);
417+
template.hasResourceProperties("AWS::CloudWatch::Alarm", {
418+
AlarmName: "DummyServiceAlarms-prefix-none",
419+
AlarmDescription: "Description",
420+
ComparisonOperator: "LessThanThreshold",
421+
DatapointsToAlarm: 10,
422+
EvaluationPeriods: 10,
423+
TreatMissingData: "notBreaching",
424+
Metrics: [
425+
Match.objectLike({
426+
Expression: "IF(sampleCount > 42, (MAX(metric)))",
427+
Label: "max",
428+
}),
429+
{
430+
Id: "metric",
431+
MetricStat: {
432+
Metric: Match.objectLike({
433+
MetricName: "DummyMetric1",
434+
}),
435+
Period: 900,
436+
Stat: "Average",
437+
},
438+
ReturnData: false,
439+
},
440+
{
441+
Id: "sampleCount",
442+
MetricStat: {
443+
Metric: Match.objectLike({
444+
MetricName: "DummyMetric1",
445+
}),
446+
Period: 900,
447+
Stat: "SampleCount",
448+
},
449+
ReturnData: false,
450+
},
451+
],
452+
});
453+
});
454+
455+
test("addAlarm: should throw Error when minSampleCountToEvaluateDatapoint is used with multiple-metric MathExpression and sampleCountMetricId is not specified", () => {
456+
const stack = new Stack();
457+
const factory = new AlarmFactory(stack, {
458+
globalMetricDefaults,
459+
globalAlarmDefaults,
460+
localAlarmNamePrefix: "prefix",
461+
});
462+
const mathExpression = new MathExpression({
463+
expression: "MAX(metric)",
464+
label: "max",
465+
usingMetrics: {
466+
m1: metric,
467+
m2: metric.with({ statistic: MetricStatistic.N }),
468+
},
469+
});
470+
406471
expect(() =>
407472
factory.addAlarm(mathExpression, {
408473
...props,
474+
alarmNameSuffix: "none",
475+
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
409476
minSampleCountToEvaluateDatapoint: 42,
477+
period: Duration.minutes(15),
410478
})
411479
).toThrow(
412-
"minSampleCountToEvaluateDatapoint is not supported for MathExpressions"
480+
"sampleCountMetricId must be specified when using minSampleCountToEvaluateDatapoint with a multiple-metric MathExpression"
413481
);
414482
});
415483

484+
test("addAlarm: check created alarms when minSampleCountToEvaluateDatapoint is used with multiple-metric MathExpression and sampleCountMetricId is specified", () => {
485+
const stack = new Stack();
486+
const factory = new AlarmFactory(stack, {
487+
globalMetricDefaults,
488+
globalAlarmDefaults,
489+
localAlarmNamePrefix: "prefix",
490+
});
491+
const mathExpression = new MathExpression({
492+
expression: "MAX(m1)",
493+
label: "max",
494+
usingMetrics: {
495+
m1: metric,
496+
m2: metric.with({ statistic: MetricStatistic.N }),
497+
},
498+
});
499+
500+
factory.addAlarm(mathExpression, {
501+
...props,
502+
alarmNameSuffix: "none",
503+
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
504+
minSampleCountToEvaluateDatapoint: 42,
505+
sampleCountMetricId: "m2",
506+
period: Duration.minutes(15),
507+
});
508+
509+
const template = Template.fromStack(stack);
510+
template.hasResourceProperties("AWS::CloudWatch::Alarm", {
511+
AlarmName: "DummyServiceAlarms-prefix-none",
512+
AlarmDescription: "Description",
513+
ComparisonOperator: "LessThanThreshold",
514+
DatapointsToAlarm: 10,
515+
EvaluationPeriods: 10,
516+
TreatMissingData: "notBreaching",
517+
Metrics: [
518+
Match.objectLike({
519+
Expression: "IF(m2 > 42, (MAX(m1)))",
520+
Label: "max",
521+
}),
522+
{
523+
Id: "m1",
524+
MetricStat: {
525+
Metric: Match.objectLike({
526+
MetricName: "DummyMetric1",
527+
}),
528+
Period: 900,
529+
Stat: "Average",
530+
},
531+
ReturnData: false,
532+
},
533+
{
534+
Id: "m2",
535+
MetricStat: {
536+
Metric: Match.objectLike({
537+
MetricName: "DummyMetric1",
538+
}),
539+
Period: 900,
540+
Stat: "SampleCount",
541+
},
542+
ReturnData: false,
543+
},
544+
],
545+
});
546+
});
547+
416548
test("addCompositeAlarm: snapshot for operator", () => {
417549
const stack = new Stack();
418550
const factory = new AlarmFactory(stack, {

0 commit comments

Comments
 (0)