Skip to content

Commit 8e77c25

Browse files
connorkochConnor Koch
andauthored
feat(lambda): add isOffsetLag prop for alarms/dashboards for OffsetLag metric (#574)
Lambda has added support for connecting to Kafka streams via Event Source Mappings (see: https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html). This comes with the addition of a new CW metric under the AWS/Lambda namespace, `OffsetLag` (see: https://aws.amazon.com/blogs/compute/offset-lag-metric-for-amazon-msk-as-an-event-source-for-lambda/). This metric is different than the existing IteratorAge metric used for Kinesis streams. OffsetLag represents the difference between the last record written to a Kafka topic and the last record that the function's consumer group has processed (note the metric is in # of records, it's not a time metric). See here: https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics.html. Defaulted the monitor to `false` since it's a new prop and is not widely used. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_ Co-authored-by: Connor Koch <[email protected]>
1 parent 5e5b18b commit 8e77c25

File tree

8 files changed

+695
-242
lines changed

8 files changed

+695
-242
lines changed

API.md

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

lib/common/monitoring/alarms/AgeAlarmFactory.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export interface MaxAgeThreshold extends CustomAlarmThreshold {
1414
readonly maxAgeInMillis: number;
1515
}
1616

17+
export interface MaxOffsetLagThreshold extends CustomAlarmThreshold {
18+
readonly maxOffsetLag: number;
19+
}
20+
1721
export interface DaysSinceUpdateThreshold extends CustomAlarmThreshold {
1822
readonly maxDaysSinceUpdate: number;
1923
}
@@ -65,6 +69,27 @@ export class AgeAlarmFactory {
6569
});
6670
}
6771

72+
addMaxOffsetLagAlarm(
73+
metric: MetricWithAlarmSupport,
74+
props: MaxOffsetLagThreshold,
75+
disambiguator?: string,
76+
) {
77+
return this.alarmFactory.addAlarm(metric, {
78+
treatMissingData:
79+
props.treatMissingDataOverride ?? TreatMissingData.MISSING,
80+
comparisonOperator:
81+
props.comparisonOperatorOverride ??
82+
ComparisonOperator.GREATER_THAN_THRESHOLD,
83+
...props,
84+
disambiguator,
85+
threshold: props.maxOffsetLag,
86+
alarmNameSuffix: "Offset-Lag-Max",
87+
alarmDescription: "Max Offset Lag is too high.",
88+
// Dedupe all iterator max age to the same ticket
89+
alarmDedupeStringSuffix: "AnyMaxOffsetLag",
90+
});
91+
}
92+
6893
addDaysSinceUpdateAlarm(
6994
metric: MetricWithAlarmSupport,
7095
props: DaysSinceUpdateThreshold,

lib/monitoring/aws-lambda/LambdaFunctionMetricFactory.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,15 @@ export class LambdaFunctionMetricFactory extends BaseMetricFactory<LambdaFunctio
194194
}),
195195
);
196196
}
197+
198+
metricMaxOffsetLagInNumberOfRecords() {
199+
return this.metricFactory.adaptMetric(
200+
this.lambdaFunction.metric("OffsetLag", {
201+
statistic: MetricStatistic.MAX,
202+
label: "Offset Lag",
203+
region: this.region,
204+
account: this.account,
205+
}),
206+
);
207+
}
197208
}

lib/monitoring/aws-lambda/LambdaFunctionMonitoring.ts

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
LatencyType,
3131
LowTpsThreshold,
3232
MaxAgeThreshold,
33+
MaxOffsetLagThreshold,
3334
MegabyteMillisecondAxisFromZero,
3435
MetricWithAlarmSupport,
3536
MinUsageCountThreshold,
@@ -61,6 +62,13 @@ export interface LambdaFunctionMonitoringOptions extends BaseMonitoringProps {
6162
* @default - true
6263
*/
6364
readonly isIterator?: boolean;
65+
/**
66+
* Indicates that the Lambda function handles an event source which uses offsets for records (e.g. Kafka streams).
67+
* This impacts what widgets are shown, as well as validates the ability to use addMaxOffsetLagAlarm.
68+
*
69+
* @default - false
70+
*/
71+
readonly isOffsetLag?: boolean;
6472

6573
readonly addLatencyP50Alarm?: Record<string, LatencyThreshold>;
6674
readonly addLatencyP90Alarm?: Record<string, LatencyThreshold>;
@@ -92,6 +100,8 @@ export interface LambdaFunctionMonitoringOptions extends BaseMonitoringProps {
92100
>;
93101
readonly addMaxIteratorAgeAlarm?: Record<string, MaxAgeThreshold>;
94102

103+
readonly addMaxOffsetLagAlarm?: Record<string, MaxOffsetLagThreshold>;
104+
95105
// Enhanced CPU metrics that are all time-based and not percent based
96106
readonly addEnhancedMonitoringMaxCpuTotalTimeAlarm?: Record<
97107
string,
@@ -148,6 +158,7 @@ export class LambdaFunctionMonitoring extends Monitoring {
148158
readonly cpuTotalTimeAnnotations: HorizontalAnnotation[];
149159
readonly memoryUsageAnnotations: HorizontalAnnotation[];
150160
readonly maxIteratorAgeAnnotations: HorizontalAnnotation[];
161+
readonly maxOffsetLagAnnotations: HorizontalAnnotation[];
151162

152163
readonly tpsMetric: MetricWithAlarmSupport;
153164
readonly p50LatencyMetric: MetricWithAlarmSupport;
@@ -165,6 +176,8 @@ export class LambdaFunctionMonitoring extends Monitoring {
165176

166177
readonly isIterator: boolean;
167178
readonly maxIteratorAgeMetric: MetricWithAlarmSupport;
179+
readonly isOffsetLag: boolean;
180+
readonly maxOffsetLagMetric: MetricWithAlarmSupport;
168181

169182
readonly lambdaInsightsEnabled: boolean;
170183
readonly enhancedMetricFactory?: LambdaFunctionEnhancedMetricFactory;
@@ -209,6 +222,7 @@ export class LambdaFunctionMonitoring extends Monitoring {
209222
this.cpuTotalTimeAnnotations = [];
210223
this.memoryUsageAnnotations = [];
211224
this.maxIteratorAgeAnnotations = [];
225+
this.maxOffsetLagAnnotations = [];
212226

213227
this.metricFactory = new LambdaFunctionMetricFactory(
214228
scope.createMetricFactory(),
@@ -242,6 +256,9 @@ export class LambdaFunctionMonitoring extends Monitoring {
242256
this.isIterator = props.isIterator ?? true;
243257
this.maxIteratorAgeMetric =
244258
this.metricFactory.metricMaxIteratorAgeInMillis();
259+
this.isOffsetLag = props.isOffsetLag ?? false;
260+
this.maxOffsetLagMetric =
261+
this.metricFactory.metricMaxOffsetLagInNumberOfRecords();
245262

246263
this.lambdaInsightsEnabled = props.lambdaInsightsEnabled ?? false;
247264
if (props.lambdaInsightsEnabled) {
@@ -521,6 +538,22 @@ export class LambdaFunctionMonitoring extends Monitoring {
521538
this.maxIteratorAgeAnnotations.push(createdAlarm.annotation);
522539
this.addAlarm(createdAlarm);
523540
}
541+
for (const disambiguator in props.addMaxOffsetLagAlarm) {
542+
if (!this.isOffsetLag) {
543+
throw new Error(
544+
"addMaxOffsetLagAlarm is not applicable if isOffsetLag is not true",
545+
);
546+
}
547+
548+
const alarmProps = props.addMaxOffsetLagAlarm[disambiguator];
549+
const createdAlarm = this.ageAlarmFactory.addMaxOffsetLagAlarm(
550+
this.maxOffsetLagMetric,
551+
alarmProps,
552+
disambiguator,
553+
);
554+
this.maxOffsetLagAnnotations.push(createdAlarm.annotation);
555+
this.addAlarm(createdAlarm);
556+
}
524557

525558
props.useCreatedAlarms?.consume(this.createdAlarms());
526559
}
@@ -545,19 +578,37 @@ export class LambdaFunctionMonitoring extends Monitoring {
545578
),
546579
];
547580

581+
let secondRowWidgetWidth: number;
582+
if (this.isIterator && this.isOffsetLag) {
583+
secondRowWidgetWidth = QuarterWidth;
584+
} else if (this.isIterator || this.isOffsetLag) {
585+
secondRowWidgetWidth = ThirdWidth;
586+
} else {
587+
secondRowWidgetWidth = HalfWidth;
588+
}
589+
const secondRow: Row = new Row(
590+
this.createInvocationWidget(
591+
secondRowWidgetWidth,
592+
DefaultGraphWidgetHeight,
593+
),
594+
this.createErrorCountWidget(
595+
secondRowWidgetWidth,
596+
DefaultGraphWidgetHeight,
597+
),
598+
);
548599
if (this.isIterator) {
549-
widgets.push(
550-
new Row(
551-
this.createInvocationWidget(ThirdWidth, DefaultGraphWidgetHeight),
552-
this.createIteratorAgeWidget(ThirdWidth, DefaultGraphWidgetHeight),
553-
this.createErrorCountWidget(ThirdWidth, DefaultGraphWidgetHeight),
600+
secondRow.addWidget(
601+
this.createIteratorAgeWidget(
602+
secondRowWidgetWidth,
603+
DefaultGraphWidgetHeight,
554604
),
555605
);
556-
} else {
557-
widgets.push(
558-
new Row(
559-
this.createInvocationWidget(HalfWidth, DefaultGraphWidgetHeight),
560-
this.createErrorCountWidget(HalfWidth, DefaultGraphWidgetHeight),
606+
}
607+
if (this.isOffsetLag) {
608+
secondRow.addWidget(
609+
this.createOffsetLagWidget(
610+
secondRowWidgetWidth,
611+
DefaultGraphWidgetHeight,
561612
),
562613
);
563614
}
@@ -681,6 +732,17 @@ export class LambdaFunctionMonitoring extends Monitoring {
681732
});
682733
}
683734

735+
createOffsetLagWidget(width: number, height: number) {
736+
return new GraphWidget({
737+
width,
738+
height,
739+
title: "OffsetLag",
740+
left: [this.maxOffsetLagMetric],
741+
leftYAxis: CountAxisFromZero,
742+
leftAnnotations: this.maxOffsetLagAnnotations,
743+
});
744+
}
745+
684746
createLambdaInsightsCpuWidget(width: number, height: number) {
685747
return new GraphWidget({
686748
width,

test/facade/__snapshots__/MonitoringAspect.test.ts.snap

Lines changed: 1 addition & 37 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/facade/__snapshots__/MonitoringFacade.test.ts.snap

Lines changed: 1 addition & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/monitoring/aws-lambda/LambdaFunctionMonitoring.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ test("snapshot test: all alarms", () => {
8383
lambdaFunction,
8484
humanReadableName: "Dummy Lambda for testing",
8585
alarmFriendlyName: "DummyLambda",
86+
isOffsetLag: true,
8687
addFaultRateAlarm: {
8788
Warning: {
8889
maxErrorRate: 1,
@@ -165,6 +166,11 @@ test("snapshot test: all alarms", () => {
165166
maxAgeInMillis: 1_000_000,
166167
},
167168
},
169+
addMaxOffsetLagAlarm: {
170+
Warning: {
171+
maxOffsetLag: 100,
172+
},
173+
},
168174
useCreatedAlarms: {
169175
consume(alarms: AlarmWithAnnotation[]) {
170176
numAlarmsCreated = alarms.length;
@@ -173,7 +179,7 @@ test("snapshot test: all alarms", () => {
173179
});
174180

175181
addMonitoringDashboardsToStack(stack, monitoring);
176-
expect(numAlarmsCreated).toStrictEqual(14);
182+
expect(numAlarmsCreated).toStrictEqual(15);
177183
expect(Template.fromStack(stack)).toMatchSnapshot();
178184
});
179185

@@ -536,6 +542,35 @@ test("throws error if attempting to create iterator age alarm if not an iterator
536542
);
537543
});
538544

545+
test("throws error if attempting to create offsetLag alarm if not an offsetLag Lambda", () => {
546+
const stack = new Stack();
547+
548+
const scope = new TestMonitoringScope(stack, "Scope");
549+
550+
const lambdaFunction = new Function(stack, "Function", {
551+
functionName: "DummyLambda",
552+
runtime: Runtime.NODEJS_18_X,
553+
code: InlineCode.fromInline("{}"),
554+
handler: "Dummy::handler",
555+
});
556+
557+
expect(
558+
() =>
559+
new LambdaFunctionMonitoring(scope, {
560+
lambdaFunction,
561+
humanReadableName: "Dummy Lambda for testing",
562+
alarmFriendlyName: "DummyLambda",
563+
addMaxOffsetLagAlarm: {
564+
Warning: {
565+
maxOffsetLag: 100,
566+
},
567+
},
568+
}),
569+
).toThrow(
570+
"addMaxOffsetLagAlarm is not applicable if isOffsetLag is not true",
571+
);
572+
});
573+
539574
test("doesn't create alarms for enhanced Lambda Insights metrics if not enabled", () => {
540575
const stack = new Stack();
541576

0 commit comments

Comments
 (0)