Skip to content

Commit 53cd97b

Browse files
authored
Improve logging on Lambda function errors (#81)
1 parent aeebe82 commit 53cd97b

File tree

6 files changed

+193
-12
lines changed

6 files changed

+193
-12
lines changed

.changeset/weak-owls-crash.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@seek/aws-codedeploy-infra': patch
3+
---
4+
5+
HookStack: Improve logging on Lambda function errors
6+
7+
When the `BeforeAllowTraffic` hook invokes your Lambda function and receives a `FunctionError` back, it now logs additional information from the response payload to aid troubleshooting:
8+
9+
```diff
10+
{
11+
"err": {
12+
"message": "Lambda function responded with error: Unhandled",
13+
+ "payload": {
14+
+ "errorMessage": "RequestId: 00000000-0000-0000-0000-000000000000 Error: Task timed out after 1.00 seconds",
15+
+ "errorType": "Sandbox.Timedout"
16+
+ },
17+
"stack": "Error: Lambda function responded with error: Unhandled...",
18+
"type": "Error"
19+
},
20+
"level": 50,
21+
"msg": "Failed to process lifecycle event"
22+
}
23+
```

packages/infra/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@aws-sdk/client-cloudformation": "3.668.0",
3939
"@aws-sdk/client-codedeploy": "3.668.0",
4040
"@aws-sdk/client-lambda": "3.668.0",
41+
"@smithy/util-stream": "3.2.1",
4142
"aws-sdk-client-mock": "4.1.0",
4243
"aws-sdk-client-mock-jest": "4.1.0",
4344
"isomorphic-git": "1.27.1",

packages/infra/src/handlers/index.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,16 @@ describe('handler', () => {
6969
});
7070

7171
it('reports a failure back to CodeDeploy', async () => {
72-
const err = new Error('mock-error');
72+
const err = Object.assign(
73+
new Error('Lambda function responded with error: Unhandled'),
74+
{
75+
payload: {
76+
errorMessage:
77+
'RequestId: 00000000-0000-0000-0000-000000000000 Error: Task timed out after 1.00 seconds',
78+
errorType: 'Sandbox.Timedout',
79+
},
80+
},
81+
);
7382

7483
processEventMock.mockRejectedValue(err);
7584

@@ -92,7 +101,12 @@ describe('handler', () => {
92101
expect(testLogs).toStrictEqual([
93102
{
94103
awsRequestId: context.awsRequestId,
95-
err: expect.objectContaining({ message: err.message }),
104+
err: {
105+
message: err.message,
106+
payload: err.payload,
107+
stack: expect.any(String),
108+
type: 'Error',
109+
},
96110
level: 50,
97111
msg: 'Failed to process lifecycle event',
98112
timestamp: expect.any(String),

packages/infra/src/handlers/process/lambda/smokeTest.test.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'aws-sdk-client-mock-jest';
22

33
import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
4+
import { Uint8ArrayBlobAdapter } from '@smithy/util-stream';
45
import { mockClient } from 'aws-sdk-client-mock';
56

67
import { storage } from '../../framework/context';
@@ -136,13 +137,60 @@ describe('smokeTest', () => {
136137
});
137138

138139
it('propagates an error from the Lambda function', async () => {
139-
lambda
140-
.on(InvokeCommand)
141-
.resolves({ FunctionError: 'mock-function-error', StatusCode: 200 });
140+
const payload = {
141+
errorMessage:
142+
'RequestId: 00000000-0000-0000-0000-000000000000 Error: Task timed out after 1.00 seconds',
143+
errorType: 'Sandbox.Timedout',
144+
};
145+
146+
lambda.on(InvokeCommand).resolves({
147+
FunctionError: 'Unhandled',
148+
Payload: Uint8ArrayBlobAdapter.fromString(JSON.stringify(payload)),
149+
// Yes, this is OK when there's a function error.
150+
StatusCode: 200,
151+
});
152+
153+
const err = await smokeTest(oneFn).catch((reason) => reason);
154+
155+
expect(err).toMatchInlineSnapshot(
156+
`[Error: Lambda function responded with error: Unhandled]`,
157+
);
158+
expect(err).toHaveProperty('payload', payload);
159+
});
142160

143-
await expect(smokeTest(oneFn)).rejects.toThrowErrorMatchingInlineSnapshot(
144-
`"Lambda function responded with error: mock-function-error"`,
161+
it('handles a non-JSON payload on Lambda function error', async () => {
162+
// This isn't known to happen but we're being defensive.
163+
const payload = 'Something happened';
164+
165+
lambda.on(InvokeCommand).resolves({
166+
FunctionError: 'Unhandled',
167+
Payload: Uint8ArrayBlobAdapter.fromString(payload),
168+
// Yes, this is OK when there's a function error.
169+
StatusCode: 200,
170+
});
171+
172+
const err = await smokeTest(oneFn).catch((reason) => reason);
173+
174+
expect(err).toMatchInlineSnapshot(
175+
`[Error: Lambda function responded with error: Unhandled]`,
176+
);
177+
expect(err).toHaveProperty('payload', payload);
178+
});
179+
180+
it('handles an empty payload on the Lambda function error', async () => {
181+
// This isn't known to happen but we're being defensive.
182+
lambda.on(InvokeCommand).resolves({
183+
FunctionError: 'Unhandled',
184+
// Yes, this is OK when there's a function error.
185+
StatusCode: 200,
186+
});
187+
188+
const err = await smokeTest(oneFn).catch((reason) => reason);
189+
190+
expect(err).toMatchInlineSnapshot(
191+
`[Error: Lambda function responded with error: Unhandled]`,
145192
);
193+
expect(err).not.toHaveProperty('payload');
146194
});
147195

148196
it('throws on an unexpected status code from the Lambda function', async () => {

packages/infra/src/handlers/process/lambda/smokeTest.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1-
import { InvokeCommand } from '@aws-sdk/client-lambda';
1+
import {
2+
InvokeCommand,
3+
type InvokeCommandOutput,
4+
} from '@aws-sdk/client-lambda';
25

36
import { config } from '../../config';
47
import { lambdaClient } from '../../framework/aws';
58
import { getContext } from '../../framework/context';
69

710
import type { LambdaFunction } from './types';
811

12+
const tryParsePayload = (response: InvokeCommandOutput) => {
13+
let payload: unknown = response.Payload?.transformToString();
14+
15+
if (typeof payload === 'string') {
16+
try {
17+
payload = JSON.parse(payload);
18+
} catch {}
19+
}
20+
21+
return payload ? { payload } : undefined;
22+
};
23+
924
export const smokeTest = async (fns: LambdaFunction[]): Promise<void> => {
1025
await Promise.all(fns.map((fn) => smokeTestFunction(fn)));
1126
};
@@ -46,8 +61,11 @@ export const smokeTestFunction = async ({
4661
}
4762

4863
if (response.FunctionError) {
49-
throw new Error(
50-
`Lambda function responded with error: ${response.FunctionError}`,
64+
throw Object.assign(
65+
new Error(
66+
`Lambda function responded with error: ${response.FunctionError}`,
67+
),
68+
tryParsePayload(response),
5169
);
5270
}
5371
};

pnpm-lock.yaml

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

0 commit comments

Comments
 (0)