Skip to content

Commit 218893b

Browse files
Merge pull request #623 from govuk-one-login/OJ-3220
OJ-3220 - Handle ResourceAlreadyExistsException in PIIRedactFunction createLogStream()
2 parents e5e58e2 + 1d042fe commit 218893b

3 files changed

Lines changed: 113 additions & 32 deletions

File tree

lambdas/pii-redact/src/pii-redact-handler.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
PutLogEventsCommand,
99
PutLogEventsCommandOutput,
1010
CreateLogStreamCommand,
11+
ResourceAlreadyExistsException,
1112
} from "@aws-sdk/client-cloudwatch-logs";
1213
import { redactPII } from "./pii-redactor";
1314
import {
@@ -19,24 +20,22 @@ import { initOpenTelemetry } from "../../open-telemetry/src/otel-setup";
1920

2021
initOpenTelemetry();
2122

22-
const logger = new Logger();
2323
const cloudwatch = new CloudWatchLogsClient();
2424

2525
const logStreamTrackingTable = process.env.RedactionLogStreamTrackingTable;
2626

2727
export class PiiRedactHandler implements LambdaInterface {
28-
private readonly dynamodb: DynamoDBClient;
29-
30-
constructor(dynamodb = new DynamoDBClient()) {
31-
this.dynamodb = dynamodb;
32-
}
28+
constructor(
29+
private readonly dynamodb = new DynamoDBClient(),
30+
private readonly logger = new Logger()
31+
) {}
3332

3433
public async handler(
3534
event: CloudWatchLogsEvent,
3635
_context: unknown
3736
): Promise<object> {
3837
try {
39-
logger.info("Received " + JSON.stringify(event));
38+
this.logger.info("Received " + JSON.stringify(event));
4039

4140
const logDataBase64 = event.awslogs.data;
4241
const logDataBuffer = Buffer.from(logDataBase64, "base64");
@@ -56,7 +55,7 @@ export class PiiRedactHandler implements LambdaInterface {
5655
return {};
5756
} catch (error: unknown) {
5857
const message = error instanceof Error ? error.message : String(error);
59-
logger.error(`Error in PiiRedactHandler: ${message}`);
58+
this.logger.error(`Error in PiiRedactHandler: ${message}`);
6059
throw error;
6160
}
6261
}
@@ -66,7 +65,7 @@ export class PiiRedactHandler implements LambdaInterface {
6665
logStream: string,
6766
logEvents: CloudWatchLogsDecodedData
6867
) {
69-
logger.info("Putting redacted logs into " + redactLogGroup);
68+
this.logger.info("Putting redacted logs into " + redactLogGroup);
7069

7170
try {
7271
const response: PutLogEventsCommandOutput = await cloudwatch.send(
@@ -81,26 +80,38 @@ export class PiiRedactHandler implements LambdaInterface {
8180
})),
8281
})
8382
);
84-
logger.info(JSON.stringify(response));
83+
this.logger.info(JSON.stringify(response));
8584
} catch (error) {
86-
logger.error(`Error putting log events into ${redactLogGroup}: ${error}`);
85+
this.logger.error(
86+
`Error putting log events into ${redactLogGroup}: ${error}`
87+
);
8788
throw error;
8889
}
8990
}
9091

9192
private async createLogStream(logStreamName: string, logGroupName: string) {
9293
if (!(await this.logStreamExists(logStreamName))) {
93-
logger.info("Creating log stream " + logStreamName);
94-
95-
await cloudwatch.send(
96-
new CreateLogStreamCommand({
97-
logGroupName: logGroupName,
98-
logStreamName: logStreamName,
99-
})
100-
);
94+
this.logger.info("Creating log stream " + logStreamName);
95+
96+
try {
97+
await cloudwatch.send(
98+
new CreateLogStreamCommand({
99+
logStreamName: logStreamName,
100+
logGroupName: logGroupName,
101+
})
102+
);
103+
} catch (error: unknown) {
104+
if (error instanceof ResourceAlreadyExistsException) {
105+
this.logger.info(logStreamName + " already exists");
106+
} else {
107+
throw error;
108+
}
109+
}
101110

102111
await this.saveLogStreamRecordInDB(logStreamName);
103-
logger.info("Added " + logStreamName + " to " + logStreamTrackingTable);
112+
this.logger.info(
113+
"Added " + logStreamName + " to " + logStreamTrackingTable
114+
);
104115
}
105116
}
106117

lambdas/pii-redact/tests/pii-redact-handler.test.ts

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,31 @@ import { PiiRedactHandler } from "../src/pii-redact-handler";
22
import * as zlib from "node:zlib";
33
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
44
import mocked = jest.mocked;
5+
import { ResourceAlreadyExistsException } from "@aws-sdk/client-cloudwatch-logs";
6+
import { mockLogger } from "./utils";
57

6-
let shouldMockThrowError = false;
8+
let throwMockError: null | "Error" | "ResourceAlreadyExistsException" = null;
79

810
jest.mock("@aws-sdk/client-cloudwatch-logs", () => {
911
const actual = jest.requireActual("@aws-sdk/client-cloudwatch-logs");
1012
return {
1113
...actual,
1214
CloudWatchLogsClient: jest.fn(() => ({
1315
send: jest.fn().mockImplementation((command) => {
14-
if (
15-
shouldMockThrowError &&
16-
command instanceof actual.CreateLogStreamCommand
17-
) {
18-
throw new Error("Error creating log stream");
16+
if (command instanceof actual.CreateLogStreamCommand) {
17+
switch (throwMockError) {
18+
case "ResourceAlreadyExistsException": {
19+
throw new ResourceAlreadyExistsException({
20+
$metadata: {},
21+
message: "Resource already exists!",
22+
});
23+
}
24+
case "Error": {
25+
throw new Error("Error creating log stream");
26+
}
27+
}
1928
}
29+
2030
return {
2131
message: "",
2232
};
@@ -44,6 +54,10 @@ describe("pii-redact-handler", () => {
4454
});
4555
}
4656

57+
beforeEach(() => {
58+
throwMockError = null;
59+
});
60+
4761
it("should not fail when running the handler", async () => {
4862
const mockDynamoDbClient = mocked(DynamoDBClient);
4963
mockDynamoDbClient.prototype.send = jest.fn().mockReturnValue({
@@ -71,7 +85,10 @@ describe("pii-redact-handler", () => {
7185
data: compressedData,
7286
},
7387
};
74-
const handler = new PiiRedactHandler(mockDynamoDbClient.prototype);
88+
const handler = new PiiRedactHandler(
89+
mockDynamoDbClient.prototype,
90+
mockLogger
91+
);
7592
const result = await handler.handler(event, {});
7693
expect(result).toStrictEqual({});
7794
});
@@ -81,7 +98,7 @@ describe("pii-redact-handler", () => {
8198
mockDynamoDbClient.prototype.send = jest.fn().mockReturnValue({
8299
Count: 0,
83100
});
84-
shouldMockThrowError = true;
101+
throwMockError = "Error";
85102
const piiData = {
86103
owner: undefined,
87104
logGroup: "dummy-log-group",
@@ -103,12 +120,50 @@ describe("pii-redact-handler", () => {
103120
data: compressedData,
104121
},
105122
};
106-
const handler = new PiiRedactHandler(mockDynamoDbClient.prototype);
123+
const handler = new PiiRedactHandler(
124+
mockDynamoDbClient.prototype,
125+
mockLogger
126+
);
107127

108128
await expect(handler.handler(event, {})).rejects.toThrow(
109129
new Error("Error creating log stream")
110130
);
111-
shouldMockThrowError = false;
131+
});
132+
133+
it("should not throw when a ResourceAlreadyExistsException error occurs creating a log stream", async () => {
134+
const mockDynamoDbClient = mocked(DynamoDBClient);
135+
mockDynamoDbClient.prototype.send = jest.fn().mockReturnValue({
136+
Count: 0,
137+
});
138+
throwMockError = "ResourceAlreadyExistsException";
139+
const piiData = {
140+
owner: undefined,
141+
logGroup: "dummy-log-group",
142+
logStream: "dummy-log-stream",
143+
subscriptionFilters: undefined,
144+
messageType: undefined,
145+
logEvents: [
146+
{
147+
id: undefined,
148+
timestamp: undefined,
149+
message: "{}",
150+
extractedFields: undefined,
151+
},
152+
],
153+
};
154+
const compressedData = await encode(JSON.stringify(piiData));
155+
const event = {
156+
awslogs: {
157+
data: compressedData,
158+
},
159+
};
160+
const handler = new PiiRedactHandler(
161+
mockDynamoDbClient.prototype,
162+
mockLogger
163+
);
164+
165+
const result = await handler.handler(event, {});
166+
expect(result).toStrictEqual({});
112167
});
113168

114169
it("should create log stream when does not exist", async () => {
@@ -138,7 +193,10 @@ describe("pii-redact-handler", () => {
138193
data: compressedData,
139194
},
140195
};
141-
const handler = new PiiRedactHandler(mockDynamoDbClient.prototype);
196+
const handler = new PiiRedactHandler(
197+
mockDynamoDbClient.prototype,
198+
mockLogger
199+
);
142200
const result = await handler.handler(event, {});
143201

144202
expect(result).toStrictEqual({});
@@ -171,7 +229,10 @@ describe("pii-redact-handler", () => {
171229
data: compressedData,
172230
},
173231
};
174-
const handler = new PiiRedactHandler(mockDynamoDbClient.prototype);
232+
const handler = new PiiRedactHandler(
233+
mockDynamoDbClient.prototype,
234+
mockLogger
235+
);
175236
const result = await handler.handler(event, {});
176237

177238
expect(result).toStrictEqual({});

lambdas/pii-redact/tests/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Logger } from "@aws-lambda-powertools/logger";
2+
3+
export const mockLogger = {
4+
info: jest.fn(),
5+
critical: jest.fn(),
6+
debug: jest.fn(),
7+
error: jest.fn(),
8+
warn: jest.fn(),
9+
} as unknown as Logger;

0 commit comments

Comments
 (0)