Skip to content

Commit 4545aa0

Browse files
Feature/CCM 12846 expose key metrics to cloudwatch (#389)
* change to create a PR * add my first EMF metrics to upsert letter lambda * correct handler function and lint errors * add cloudwatch:PutMetricData permission * set custom namespace * remove cloudwatch client declaration * remove oddOrEven from dimensions * add oddOrEven from dimensions * remove operationType from messageProcessed * log environment variables * log letterEvent and operation * fix linting errors * emit metrics per supplier and with correct count * correct emitMetrics function * add metrics for rest of Lambda functions * fix failing unit tests * add extra test to check that statuses map is populated * address PR comments * fix npm vulnerabilities * fix typeckeck and lint errors * remove comment * regen lock after merge --------- Co-authored-by: Mark Slowey <mark.slowey1@nhs.net>
1 parent cb1488f commit 4545aa0

22 files changed

Lines changed: 8073 additions & 7813 deletions

File tree

internal/datastore/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"dependencies": {
3-
"@aws-sdk/client-dynamodb": "^3.858.0",
4-
"@aws-sdk/lib-dynamodb": "^3.858.0",
3+
"@aws-sdk/client-dynamodb": "^3.981.0",
4+
"@aws-sdk/lib-dynamodb": "^3.981.0",
55
"@internal/helpers": "*",
66
"pino": "^9.7.0",
77
"zod": "^4.1.11",

lambdas/api-handler/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
{
22
"dependencies": {
3-
"@aws-sdk/client-dynamodb": "^3.925.0",
3+
"@aws-sdk/client-dynamodb": "^3.981.0",
44
"@aws-sdk/client-s3": "^3.925.0",
55
"@aws-sdk/client-sqs": "^3.925.0",
66
"@aws-sdk/lib-dynamodb": "^3.925.0",
77
"@aws-sdk/s3-request-presigner": "^3.925.0",
88
"@internal/datastore": "*",
99
"@internal/helpers": "*",
10-
"aws-lambda": "^1.0.7",
11-
"esbuild": "^0.25.11",
10+
"aws-embedded-metrics": "^4.2.1",
11+
"aws-lambda": "^1.0.6",
12+
"esbuild": "0.27.2",
1213
"pino": "^9.7.0",
1314
"zod": "^4.1.11"
1415
},
Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,66 @@
11
import { APIGatewayProxyHandler } from "aws-lambda";
2+
import { MetricsLogger, metricScope } from "aws-embedded-metrics";
23
import { assertNotEmpty } from "../utils/validation";
34
import { extractCommonIds } from "../utils/common-ids";
45
import { ApiErrorDetail } from "../contracts/errors";
56
import { processError } from "../mappers/error-mapper";
67
import ValidationError from "../errors/validation-error";
78
import { getLetterDataUrl } from "../services/letter-operations";
89
import type { Deps } from "../config/deps";
10+
import { MetricStatus, emitForSingleSupplier } from "../utils/metrics";
911

1012
export default function createGetLetterDataHandler(
1113
deps: Deps,
1214
): APIGatewayProxyHandler {
13-
return async (event) => {
14-
const commonIds = extractCommonIds(
15-
event.headers,
16-
event.requestContext,
17-
deps,
18-
);
19-
20-
if (!commonIds.ok) {
21-
return processError(
22-
commonIds.error,
23-
commonIds.correlationId,
24-
deps.logger,
15+
return metricScope((metrics: MetricsLogger) => {
16+
return async (event) => {
17+
const commonIds = extractCommonIds(
18+
event.headers,
19+
event.requestContext,
20+
deps,
2521
);
26-
}
2722

28-
try {
29-
const letterId = assertNotEmpty(
30-
event.pathParameters?.id,
31-
new ValidationError(
32-
ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter,
33-
),
34-
);
23+
if (!commonIds.ok) {
24+
return processError(
25+
commonIds.error,
26+
commonIds.correlationId,
27+
deps.logger,
28+
);
29+
}
3530

36-
return {
37-
statusCode: 303,
38-
headers: {
39-
Location: await getLetterDataUrl(
40-
commonIds.value.supplierId,
41-
letterId,
42-
deps,
31+
const { supplierId } = commonIds.value;
32+
try {
33+
const letterId = assertNotEmpty(
34+
event.pathParameters?.id,
35+
new ValidationError(
36+
ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter,
4337
),
44-
},
45-
body: "",
46-
};
47-
} catch (error) {
48-
return processError(error, commonIds.value.correlationId, deps.logger);
49-
}
50-
};
38+
);
39+
40+
emitForSingleSupplier(
41+
metrics,
42+
"getLetterData",
43+
supplierId,
44+
1,
45+
MetricStatus.Success,
46+
);
47+
return {
48+
statusCode: 303,
49+
headers: {
50+
Location: await getLetterDataUrl(supplierId, letterId, deps),
51+
},
52+
body: "",
53+
};
54+
} catch (error) {
55+
emitForSingleSupplier(
56+
metrics,
57+
"getLetterData",
58+
supplierId,
59+
1,
60+
MetricStatus.Failure,
61+
);
62+
return processError(error, commonIds.value.correlationId, deps.logger);
63+
}
64+
};
65+
});
5166
}
Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { APIGatewayProxyHandler } from "aws-lambda";
2+
import { MetricsLogger, metricScope } from "aws-embedded-metrics";
23
import { assertNotEmpty } from "../utils/validation";
34
import { extractCommonIds } from "../utils/common-ids";
45
import ValidationError from "../errors/validation-error";
@@ -7,53 +8,72 @@ import { getLetterById } from "../services/letter-operations";
78
import { processError } from "../mappers/error-mapper";
89
import { mapToGetLetterResponse } from "../mappers/letter-mapper";
910
import { Deps } from "../config/deps";
11+
import { MetricStatus, emitForSingleSupplier } from "../utils/metrics";
1012

13+
// Get letter data
1114
export default function createGetLetterHandler(
1215
deps: Deps,
1316
): APIGatewayProxyHandler {
14-
return async (event) => {
15-
const commonIds = extractCommonIds(
16-
event.headers,
17-
event.requestContext,
18-
deps,
19-
);
20-
21-
if (!commonIds.ok) {
22-
return processError(
23-
commonIds.error,
24-
commonIds.correlationId,
25-
deps.logger,
17+
return metricScope((metrics: MetricsLogger) => {
18+
return async (event) => {
19+
const commonIds = extractCommonIds(
20+
event.headers,
21+
event.requestContext,
22+
deps,
2623
);
27-
}
2824

29-
try {
30-
const letterId = assertNotEmpty(
31-
event.pathParameters?.id,
32-
new ValidationError(
33-
ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter,
34-
),
35-
);
25+
if (!commonIds.ok) {
26+
return processError(
27+
commonIds.error,
28+
commonIds.correlationId,
29+
deps.logger,
30+
);
31+
}
3632

37-
const letter = await getLetterById(
38-
commonIds.value.supplierId,
39-
letterId,
40-
deps.letterRepo,
41-
);
33+
const { supplierId } = commonIds.value;
34+
try {
35+
const letterId = assertNotEmpty(
36+
event.pathParameters?.id,
37+
new ValidationError(
38+
ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter,
39+
),
40+
);
41+
42+
const letter = await getLetterById(
43+
supplierId,
44+
letterId,
45+
deps.letterRepo,
46+
);
4247

43-
const response = mapToGetLetterResponse(letter);
48+
const response = mapToGetLetterResponse(letter);
4449

45-
deps.logger.info({
46-
description: "Letter successfully fetched by id",
47-
supplierId: commonIds.value.supplierId,
48-
letterId,
49-
});
50+
deps.logger.info({
51+
description: "Letter successfully fetched by id",
52+
supplierId,
53+
letterId,
54+
});
5055

51-
return {
52-
statusCode: 200,
53-
body: JSON.stringify(response, null, 2),
54-
};
55-
} catch (error) {
56-
return processError(error, commonIds.value.correlationId, deps.logger);
57-
}
58-
};
56+
emitForSingleSupplier(
57+
metrics,
58+
"getLetter",
59+
supplierId,
60+
1,
61+
MetricStatus.Success,
62+
);
63+
return {
64+
statusCode: 200,
65+
body: JSON.stringify(response, null, 2),
66+
};
67+
} catch (error) {
68+
emitForSingleSupplier(
69+
metrics,
70+
"getLetter",
71+
supplierId,
72+
1,
73+
MetricStatus.Failure,
74+
);
75+
return processError(error, commonIds.value.correlationId, deps.logger);
76+
}
77+
};
78+
});
5979
}

lambdas/api-handler/src/handlers/get-letters.ts

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
APIGatewayProxyHandler,
44
} from "aws-lambda";
55
import { Logger } from "pino";
6+
import { MetricsLogger, metricScope } from "aws-embedded-metrics";
67
import { getLettersForSupplier } from "../services/letter-operations";
78
import { extractCommonIds } from "../utils/common-ids";
89
import { requireEnvVar } from "../utils/validation";
@@ -11,7 +12,9 @@ import { processError } from "../mappers/error-mapper";
1112
import ValidationError from "../errors/validation-error";
1213
import { mapToGetLettersResponse } from "../mappers/letter-mapper";
1314
import type { Deps } from "../config/deps";
15+
import { MetricStatus, emitForSingleSupplier } from "../utils/metrics";
1416

17+
// List letters Handlers
1518
// The endpoint should only return pending letters for now
1619
const status = "PENDING";
1720

@@ -82,53 +85,70 @@ function getLimitOrDefault(
8285
export default function createGetLettersHandler(
8386
deps: Deps,
8487
): APIGatewayProxyHandler {
85-
return async (event) => {
86-
const commonIds = extractCommonIds(
87-
event.headers,
88-
event.requestContext,
89-
deps,
90-
);
91-
92-
if (!commonIds.ok) {
93-
return processError(
94-
commonIds.error,
95-
commonIds.correlationId,
96-
deps.logger,
88+
return metricScope((metrics: MetricsLogger) => {
89+
return async (event) => {
90+
const commonIds = extractCommonIds(
91+
event.headers,
92+
event.requestContext,
93+
deps,
9794
);
98-
}
9995

100-
try {
101-
const maxLimit = requireEnvVar(deps.env, "MAX_LIMIT");
96+
if (!commonIds.ok) {
97+
return processError(
98+
commonIds.error,
99+
commonIds.correlationId,
100+
deps.logger,
101+
);
102+
}
102103

103-
const limitNumber = getLimitOrDefault(
104-
event.queryStringParameters,
105-
maxLimit,
106-
deps.logger,
107-
);
104+
const { supplierId } = commonIds.value;
105+
try {
106+
const maxLimit = requireEnvVar(deps.env, "MAX_LIMIT");
108107

109-
const letters = await getLettersForSupplier(
110-
commonIds.value.supplierId,
111-
status,
112-
limitNumber,
113-
deps.letterRepo,
114-
);
108+
const limitNumber = getLimitOrDefault(
109+
event.queryStringParameters,
110+
maxLimit,
111+
deps.logger,
112+
);
113+
114+
const letters = await getLettersForSupplier(
115+
supplierId,
116+
status,
117+
limitNumber,
118+
deps.letterRepo,
119+
);
115120

116-
const response = mapToGetLettersResponse(letters);
121+
const response = mapToGetLettersResponse(letters);
117122

118-
deps.logger.info({
119-
description: "Pending letters successfully fetched",
120-
supplierId: commonIds.value.supplierId,
121-
limitNumber,
122-
status,
123-
lettersCount: letters.length,
124-
});
123+
deps.logger.info({
124+
description: "Pending letters successfully fetched",
125+
supplierId,
126+
limitNumber,
127+
status,
128+
lettersCount: letters.length,
129+
});
125130

126-
return {
127-
statusCode: 200,
128-
body: JSON.stringify(response, null, 2),
129-
};
130-
} catch (error) {
131-
return processError(error, commonIds.value.correlationId, deps.logger);
132-
}
133-
};
131+
emitForSingleSupplier(
132+
metrics,
133+
"getLetters",
134+
supplierId,
135+
letters.length,
136+
MetricStatus.Success,
137+
);
138+
return {
139+
statusCode: 200,
140+
body: JSON.stringify(response, null, 2),
141+
};
142+
} catch (error) {
143+
emitForSingleSupplier(
144+
metrics,
145+
"getLetters",
146+
supplierId,
147+
1,
148+
MetricStatus.Failure,
149+
);
150+
return processError(error, commonIds.value.correlationId, deps.logger);
151+
}
152+
};
153+
});
134154
}

0 commit comments

Comments
 (0)