From 8e845cf063ea026001bf45b9c065b4c48177978d Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Tue, 20 Jan 2026 12:58:06 +0000 Subject: [PATCH 01/17] change to create a PR --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 38df7c65e..7c338e977 100644 --- a/README.md +++ b/README.md @@ -138,3 +138,5 @@ Deployments can be made of any [release](https://github.com/NHSDigital/nhs-notif Unless stated otherwise, the codebase is released under the MIT License. This covers both the codebase and any sample code in the documentation. Any HTML or Markdown documentation is [© Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/) and available under the terms of the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). + +to open a PR From b51d29032e5af594e00e19e3b8b9bfe89ca7758d Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Wed, 21 Jan 2026 14:30:27 +0000 Subject: [PATCH 02/17] add my first EMF metrics to upsert letter lambda --- lambdas/upsert-letter/package.json | 1 + .../src/handler/upsert-handler.ts | 76 ++++++++++++------- package-lock.json | 26 +++++-- 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/lambdas/upsert-letter/package.json b/lambdas/upsert-letter/package.json index e4858da0d..4cf3d7f38 100644 --- a/lambdas/upsert-letter/package.json +++ b/lambdas/upsert-letter/package.json @@ -7,6 +7,7 @@ "@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1": "npm:@nhsdigital/nhs-notify-event-schemas-letter-rendering@^1.1.5", "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.8", "@types/aws-lambda": "^8.10.148", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.27.2", "pino": "^9.7.0", diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index a1b2ea08b..065044cf5 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -19,6 +19,7 @@ import { LetterRequestPreparedEventV2, } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering"; import z from "zod"; +import { metricScope, Unit } from "aws-embedded-metrics"; import { Deps } from "../config/deps"; type SupplierSpec = { supplierId: string; specId: string }; @@ -153,33 +154,50 @@ async function runUpsert( } export default function createUpsertLetterHandler(deps: Deps): SQSHandler { - return async (event: SQSEvent) => { - const batchItemFailures: SQSBatchItemFailure[] = []; - - const tasks = event.Records.map(async (record) => { - try { - const message: string = parseSNSNotification(record); - - const snsEvent = JSON.parse(message); - - const letterEvent: unknown = removeEventBridgeWrapper(snsEvent); - - const type = getType(letterEvent); - - const operation = getOperationFromType(type); - - await runUpsert(operation, letterEvent, deps); - } catch (error) { - deps.logger.error( - { err: error, message: record.body }, - `Error processing upsert of record ${record.messageId}`, - ); - batchItemFailures.push({ itemIdentifier: record.messageId }); - } - }); - - await Promise.all(tasks); - - return { batchItemFailures }; - }; + return metricScope(async (metrics) => { + return async (event: SQSEvent) => { + const batchItemFailures: SQSBatchItemFailure[] = []; + + const tasks = event.Records.map(async (record) => { + try { + const message: string = parseSNSNotification(record); + + const snsEvent = JSON.parse(message); + + const letterEvent: unknown = removeEventBridgeWrapper(snsEvent); + + const type = getType(letterEvent); + + const operation = getOperationFromType(type); + + await runUpsert(operation, letterEvent, deps); + metrics.putDimensions({ + FunctionName: "upsertLambda", + // eslint-disable-next-line sonarjs/pseudo-random + OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, + OperationType: operation.name, + }); + metrics.setProperty("operation", operation.name); + metrics.putMetric("MessagesProcessed", 1, Unit.Count); + } catch (error) { + deps.logger.error( + { err: error, message: record.body }, + `Error processing upsert of record ${record.messageId}`, + ); + metrics.putDimensions({ + FunctionName: "upsertLambda", + // eslint-disable-next-line sonarjs/pseudo-random + OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, + }); + metrics.setProperty("operation", operation.name); + metrics.putMetric("MessageFailed", 1, Unit.Count); + batchItemFailures.push({ itemIdentifier: record.messageId }); + } + }); + + await Promise.all(tasks); + + return { batchItemFailures }; + }; + }); } diff --git a/package-lock.json b/package-lock.json index 64a58b073..f7940dc43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -798,6 +798,7 @@ "@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1": "npm:@nhsdigital/nhs-notify-event-schemas-letter-rendering@^1.1.5", "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.8", "@types/aws-lambda": "^8.10.148", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.27.2", "pino": "^9.7.0", @@ -3338,6 +3339,12 @@ "node": ">=18" } }, + "node_modules/@datastructures-js/heap": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@datastructures-js/heap/-/heap-4.3.7.tgz", + "integrity": "sha512-Dx4un7Uj0dVxkfoq4RkpzsY2OrvNJgQYZ3n3UlGdl88RxxdHd7oTi21/l3zoxUUe0sXFuNUrfmWqlHzqnoN6Ug==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -9107,6 +9114,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-embedded-metrics": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/aws-embedded-metrics/-/aws-embedded-metrics-4.2.1.tgz", + "integrity": "sha512-uzydBXlGQVTB2sZ9ACCQZM3y0u4wdvxxRKFL9LP6RdfI2GcOrCcAsz65UKQvX9iagxFhah322VvvatgP8E7MIg==", + "license": "Apache-2.0", + "dependencies": { + "@datastructures-js/heap": "^4.0.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/aws-lambda": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz", @@ -21309,13 +21328,6 @@ "node": ">=18.17" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "extraneous": true, - "license": "MIT" - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", From 676486692a0c3f0ce70ebdce5cca231472677639 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Wed, 21 Jan 2026 15:01:11 +0000 Subject: [PATCH 03/17] correct handler function and lint errors --- lambdas/upsert-letter/src/handler/upsert-handler.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 065044cf5..71b276314 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -19,7 +19,7 @@ import { LetterRequestPreparedEventV2, } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering"; import z from "zod"; -import { metricScope, Unit } from "aws-embedded-metrics"; +import { Unit, metricScope } from "aws-embedded-metrics"; import { Deps } from "../config/deps"; type SupplierSpec = { supplierId: string; specId: string }; @@ -154,15 +154,18 @@ async function runUpsert( } export default function createUpsertLetterHandler(deps: Deps): SQSHandler { - return metricScope(async (metrics) => { + return metricScope((metrics) => { return async (event: SQSEvent) => { + console.log("The SQSEvent:", event); const batchItemFailures: SQSBatchItemFailure[] = []; const tasks = event.Records.map(async (record) => { try { const message: string = parseSNSNotification(record); + console.log("the message:", message); const snsEvent = JSON.parse(message); + console.log("the snsEvent:", snsEvent); const letterEvent: unknown = removeEventBridgeWrapper(snsEvent); @@ -189,7 +192,6 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { // eslint-disable-next-line sonarjs/pseudo-random OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, }); - metrics.setProperty("operation", operation.name); metrics.putMetric("MessageFailed", 1, Unit.Count); batchItemFailures.push({ itemIdentifier: record.messageId }); } From 9e96b04f2bfdcd8cf065dca2e1467c3a3feb4089 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Wed, 21 Jan 2026 15:26:58 +0000 Subject: [PATCH 04/17] add cloudwatch:PutMetricData permission --- .../components/api/module_lambda_upsert_letter.tf | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf index 201e10186..69974fce9 100644 --- a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf +++ b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf @@ -85,4 +85,16 @@ data "aws_iam_policy_document" "upsert_letter_lambda" { module.sqs_letter_updates.sqs_queue_arn ] } + + statement { + sid = "AllowCloudWatchMetrics" + effect = "Allow" + + actions = [ + "cloudwatch:PutMetricData" + ] + + resources = ["*"] + } + } From 53db977ad0324b9e7a4bc5c6fe2563abf5d1c0f5 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Wed, 21 Jan 2026 15:53:50 +0000 Subject: [PATCH 05/17] set custom namespace --- lambdas/upsert-letter/package.json | 1 + .../src/handler/upsert-handler.ts | 8 + package-lock.json | 542 +++++++++++++++++- 3 files changed, 548 insertions(+), 3 deletions(-) diff --git a/lambdas/upsert-letter/package.json b/lambdas/upsert-letter/package.json index 4cf3d7f38..d0d6eb6ac 100644 --- a/lambdas/upsert-letter/package.json +++ b/lambdas/upsert-letter/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@aws-sdk/client-cloudwatch": "^3.972.0", "@aws-sdk/client-dynamodb": "^3.858.0", "@aws-sdk/lib-dynamodb": "^3.858.0", "@internal/datastore": "*", diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 71b276314..521ebd722 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -20,8 +20,14 @@ import { } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering"; import z from "zod"; import { Unit, metricScope } from "aws-embedded-metrics"; +import { + CloudWatchClient, + PutMetricDataCommand, +} from "@aws-sdk/client-dynamodb"; import { Deps } from "../config/deps"; +const cwClient = new CloudWatchClient({ region: "eu-west-2" }); + type SupplierSpec = { supplierId: string; specId: string }; type PreparedEvents = LetterRequestPreparedEventV2 | LetterRequestPreparedEvent; type UpsertOperation = { @@ -158,6 +164,7 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { return async (event: SQSEvent) => { console.log("The SQSEvent:", event); const batchItemFailures: SQSBatchItemFailure[] = []; + metrics.setNamespace("vlasis_upsertLetter"); const tasks = event.Records.map(async (record) => { try { @@ -181,6 +188,7 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { OperationType: operation.name, }); metrics.setProperty("operation", operation.name); + metrics.putMetric("MessagesProcessed", 1, Unit.Count); } catch (error) { deps.logger.error( diff --git a/package-lock.json b/package-lock.json index f7940dc43..0f8aeb60d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -791,6 +791,7 @@ "name": "nhs-notify-supplier-api-upsert-letter", "version": "0.0.1", "dependencies": { + "@aws-sdk/client-cloudwatch": "^3.972.0", "@aws-sdk/client-dynamodb": "^3.858.0", "@aws-sdk/lib-dynamodb": "^3.858.0", "@internal/datastore": "*", @@ -1192,6 +1193,514 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-cloudwatch": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.972.0.tgz", + "integrity": "sha512-6LiuuuIRQ0faBz94dKTL/zrmIMSljQo0JgZ7pNjIIcxXrsQzMTr+AWhvFPlJPorI6h3XcQ5Vkyz20GODyn6TYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/credential-provider-node": "3.972.0", + "@aws-sdk/middleware-host-header": "3.972.0", + "@aws-sdk/middleware-logger": "3.972.0", + "@aws-sdk/middleware-recursion-detection": "3.972.0", + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/region-config-resolver": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "3.972.0", + "@aws-sdk/util-user-agent-node": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-compression": "^4.3.22", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sso": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.972.0.tgz", + "integrity": "sha512-5qw6qLiRE4SUiz0hWy878dSR13tSVhbTWhsvFT8mGHe37NRRiaobm5MA2sWD0deRAuO98djSiV+dhWXa1xIFNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/middleware-host-header": "3.972.0", + "@aws-sdk/middleware-logger": "3.972.0", + "@aws-sdk/middleware-recursion-detection": "3.972.0", + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/region-config-resolver": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "3.972.0", + "@aws-sdk/util-user-agent-node": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/core": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.972.0.tgz", + "integrity": "sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@aws-sdk/xml-builder": "3.972.0", + "@smithy/core": "^3.20.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.0.tgz", + "integrity": "sha512-kKHoNv+maHlPQOAhYamhap0PObd16SAb3jwaY0KYgNTiSbeXlbGUZPLioo9oA3wU10zItJzx83ClU7d7h40luA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.0.tgz", + "integrity": "sha512-xzEi81L7I5jGUbpmqEHCe7zZr54hCABdj4H+3LzktHYuovV/oqnvoDdvZpGFR0e/KAw1+PL38NbGrpG30j6qlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.0.tgz", + "integrity": "sha512-ruhAMceUIq2aknFd3jhWxmO0P0Efab5efjyIXOkI9i80g+zDY5VekeSxfqRKStEEJSKSCHDLQuOu0BnAn4Rzew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/credential-provider-env": "3.972.0", + "@aws-sdk/credential-provider-http": "3.972.0", + "@aws-sdk/credential-provider-login": "3.972.0", + "@aws-sdk/credential-provider-process": "3.972.0", + "@aws-sdk/credential-provider-sso": "3.972.0", + "@aws-sdk/credential-provider-web-identity": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.0.tgz", + "integrity": "sha512-SsrsFJsEYAJHO4N/r2P0aK6o8si6f1lprR+Ej8J731XJqTckSGs/HFHcbxOyW/iKt+LNUvZa59/VlJmjhF4bEQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.0.tgz", + "integrity": "sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.972.0", + "@aws-sdk/credential-provider-http": "3.972.0", + "@aws-sdk/credential-provider-ini": "3.972.0", + "@aws-sdk/credential-provider-process": "3.972.0", + "@aws-sdk/credential-provider-sso": "3.972.0", + "@aws-sdk/credential-provider-web-identity": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.0.tgz", + "integrity": "sha512-nmzYhamLDJ8K+v3zWck79IaKMc350xZnWsf/GeaXO6E3MewSzd3lYkTiMi7lEp3/UwDm9NHfPguoPm+mhlSWQQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.0.tgz", + "integrity": "sha512-6mYyfk1SrMZ15cH9T53yAF4YSnvq4yU1Xlgm3nqV1gZVQzmF5kr4t/F3BU3ygbvzi4uSwWxG3I3TYYS5eMlAyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.972.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/token-providers": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.0.tgz", + "integrity": "sha512-vsJXBGL8H54kz4T6do3p5elATj5d1izVGUXMluRJntm9/I0be/zUYtdd4oDTM2kSUmd4Zhyw3fMQ9lw7CVhd4A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.0.tgz", + "integrity": "sha512-3eztFI6F9/eHtkIaWKN3nT+PM+eQ6p1MALDuNshFk323ixuCZzOOVT8oUqtZa30Z6dycNXJwhlIq7NhUVFfimw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.0.tgz", + "integrity": "sha512-ZvdyVRwzK+ra31v1pQrgbqR/KsLD+wwJjHgko6JfoKUBIcEfAwJzQKO6HspHxdHWTVUz6MgvwskheR/TTYZl2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.0.tgz", + "integrity": "sha512-F2SmUeO+S6l1h6dydNet3BQIk173uAkcfU1HDkw/bUdRLAnh15D3HP9vCZ7oCPBNcdEICbXYDmx0BR9rRUHGlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.0.tgz", + "integrity": "sha512-kFHQm2OCBJCzGWRafgdWHGFjitUXY/OxXngymcX4l8CiyiNDZB27HDDBg2yLj3OUJc4z4fexLMmP8r9vgag19g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@smithy/core": "^3.20.6", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/nested-clients": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.972.0.tgz", + "integrity": "sha512-QGlbnuGzSQJVG6bR9Qw6G0Blh6abFR4VxNa61ttMbzy9jt28xmk2iGtrYLrQPlCCPhY6enHqjTWm3n3LOb0wAw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/middleware-host-header": "3.972.0", + "@aws-sdk/middleware-logger": "3.972.0", + "@aws-sdk/middleware-recursion-detection": "3.972.0", + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/region-config-resolver": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "3.972.0", + "@aws-sdk/util-user-agent-node": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.0.tgz", + "integrity": "sha512-JyOf+R/6vJW8OEVFCAyzEOn2reri/Q+L0z9zx4JQSKWvTmJ1qeFO25sOm8VIfB8URKhfGRTQF30pfYaH2zxt/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/token-providers": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.972.0.tgz", + "integrity": "sha512-kWlXG+y5nZhgXGEtb72Je+EvqepBPs8E3vZse//1PYLWs2speFqbGE/ywCXmzEJgHgVqSB/u/lqBvs5WlYmSqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/types": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.972.0.tgz", + "integrity": "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.972.0.tgz", + "integrity": "sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.0.tgz", + "integrity": "sha512-eOLdkQyoRbDgioTS3Orr7iVsVEutJyMZxvyZ6WAF95IrF0kfWx5Rd/KXnfbnG/VKa2CvjZiitWfouLzfVEyvJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.0.tgz", + "integrity": "sha512-GOy+AiSrE9kGiojiwlZvVVSXwylu4+fmP0MJfvras/MwP09RB/YtQuOVR1E0fKQc6OMwaTNBjgAbOEhxuWFbAw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.0.tgz", + "integrity": "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.971.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.971.0.tgz", @@ -6166,9 +6675,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.20.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.7.tgz", - "integrity": "sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.21.0.tgz", + "integrity": "sha512-bg2TfzgsERyETAxc/Ims/eJX8eAnIeTi4r4LHpMpfF/2NyO6RsWis0rjKcCPaGksljmOb23BZRiCeT/3NvwkXw==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", @@ -6371,6 +6880,27 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/middleware-compression": { + "version": "4.3.25", + "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.3.25.tgz", + "integrity": "sha512-YWL0Mk2WCjHYbSWuD85oTlB0T7nm+YWTHJWMEac+1Uy1NtnHqnFnX4YvRGG9S6Q++aXaVZdbff8uXl2dC3RLfg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.21.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "fflate": "0.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", @@ -13392,6 +13922,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==", + "license": "MIT" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", From 2f1513d6aeb1b4e71fe7887a93b3667577d7d855 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Wed, 21 Jan 2026 15:59:08 +0000 Subject: [PATCH 06/17] remove cloudwatch client declaration --- lambdas/upsert-letter/src/handler/upsert-handler.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 521ebd722..e4a28ac90 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -20,14 +20,8 @@ import { } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering"; import z from "zod"; import { Unit, metricScope } from "aws-embedded-metrics"; -import { - CloudWatchClient, - PutMetricDataCommand, -} from "@aws-sdk/client-dynamodb"; import { Deps } from "../config/deps"; -const cwClient = new CloudWatchClient({ region: "eu-west-2" }); - type SupplierSpec = { supplierId: string; specId: string }; type PreparedEvents = LetterRequestPreparedEventV2 | LetterRequestPreparedEvent; type UpsertOperation = { From c1a9bc21b5438d448d36fc52f90b1f6173e39490 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Thu, 22 Jan 2026 13:23:02 +0000 Subject: [PATCH 07/17] remove oddOrEven from dimensions --- lambdas/upsert-letter/src/handler/upsert-handler.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index e4a28ac90..8d3c049dc 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -173,14 +173,12 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { const type = getType(letterEvent); const operation = getOperationFromType(type); - - await runUpsert(operation, letterEvent, deps); metrics.putDimensions({ FunctionName: "upsertLambda", - // eslint-disable-next-line sonarjs/pseudo-random - OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, OperationType: operation.name, }); + + await runUpsert(operation, letterEvent, deps); metrics.setProperty("operation", operation.name); metrics.putMetric("MessagesProcessed", 1, Unit.Count); @@ -191,8 +189,6 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { ); metrics.putDimensions({ FunctionName: "upsertLambda", - // eslint-disable-next-line sonarjs/pseudo-random - OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, }); metrics.putMetric("MessageFailed", 1, Unit.Count); batchItemFailures.push({ itemIdentifier: record.messageId }); From 9f9d1cfa0387bc12122b141d0591b3608db06a82 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Thu, 22 Jan 2026 15:27:59 +0000 Subject: [PATCH 08/17] add oddOrEven from dimensions --- lambdas/upsert-letter/src/handler/upsert-handler.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 8d3c049dc..38966136f 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -176,6 +176,8 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { metrics.putDimensions({ FunctionName: "upsertLambda", OperationType: operation.name, + // eslint-disable-next-line sonarjs/pseudo-random + OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, }); await runUpsert(operation, letterEvent, deps); @@ -189,6 +191,8 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { ); metrics.putDimensions({ FunctionName: "upsertLambda", + // eslint-disable-next-line sonarjs/pseudo-random + OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, }); metrics.putMetric("MessageFailed", 1, Unit.Count); batchItemFailures.push({ itemIdentifier: record.messageId }); From b2f012b106ff658e2f6f6028e2f5fd352d1bdd1a Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Fri, 23 Jan 2026 09:18:28 +0000 Subject: [PATCH 09/17] remove operationType from messageProcessed --- lambdas/upsert-letter/src/handler/upsert-handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 38966136f..df8e6f368 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -175,7 +175,6 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { const operation = getOperationFromType(type); metrics.putDimensions({ FunctionName: "upsertLambda", - OperationType: operation.name, // eslint-disable-next-line sonarjs/pseudo-random OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, }); From 32b30a113bb0fd913944ebfc5c8fb335268e4340 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Fri, 23 Jan 2026 16:48:39 +0000 Subject: [PATCH 10/17] log environment variables --- lambdas/upsert-letter/src/handler/upsert-handler.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index df8e6f368..2af591a7f 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -156,6 +156,10 @@ async function runUpsert( export default function createUpsertLetterHandler(deps: Deps): SQSHandler { return metricScope((metrics) => { return async (event: SQSEvent) => { + console.log( + "the environment variables:", + JSON.stringify(process.env, null, 2), + ); console.log("The SQSEvent:", event); const batchItemFailures: SQSBatchItemFailure[] = []; metrics.setNamespace("vlasis_upsertLetter"); From ba37764e3e6082b2cff5cdb004a44ec78f30594e Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Mon, 26 Jan 2026 13:45:10 +0000 Subject: [PATCH 11/17] log letterEvent and operation --- README.md | 2 - .../api/module_lambda_upsert_letter.tf | 12 - lambdas/upsert-letter/package.json | 1 - .../src/handler/upsert-handler.ts | 8 +- package-lock.json | 536 ------------------ 5 files changed, 5 insertions(+), 554 deletions(-) diff --git a/README.md b/README.md index 7c338e977..38df7c65e 100644 --- a/README.md +++ b/README.md @@ -138,5 +138,3 @@ Deployments can be made of any [release](https://github.com/NHSDigital/nhs-notif Unless stated otherwise, the codebase is released under the MIT License. This covers both the codebase and any sample code in the documentation. Any HTML or Markdown documentation is [© Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/) and available under the terms of the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). - -to open a PR diff --git a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf index 69974fce9..201e10186 100644 --- a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf +++ b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf @@ -85,16 +85,4 @@ data "aws_iam_policy_document" "upsert_letter_lambda" { module.sqs_letter_updates.sqs_queue_arn ] } - - statement { - sid = "AllowCloudWatchMetrics" - effect = "Allow" - - actions = [ - "cloudwatch:PutMetricData" - ] - - resources = ["*"] - } - } diff --git a/lambdas/upsert-letter/package.json b/lambdas/upsert-letter/package.json index d0d6eb6ac..4cf3d7f38 100644 --- a/lambdas/upsert-letter/package.json +++ b/lambdas/upsert-letter/package.json @@ -1,6 +1,5 @@ { "dependencies": { - "@aws-sdk/client-cloudwatch": "^3.972.0", "@aws-sdk/client-dynamodb": "^3.858.0", "@aws-sdk/lib-dynamodb": "^3.858.0", "@internal/datastore": "*", diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 2af591a7f..b5b964aa9 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -162,7 +162,9 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { ); console.log("The SQSEvent:", event); const batchItemFailures: SQSBatchItemFailure[] = []; - metrics.setNamespace("vlasis_upsertLetter"); + metrics.setNamespace( + process.env.AWS_LAMBDA_FUNCTION_NAME || "upsertletter", + ); const tasks = event.Records.map(async (record) => { try { @@ -177,8 +179,9 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { const type = getType(letterEvent); const operation = getOperationFromType(type); + console.log("letterEvent: ", letterEvent); + console.log("operation: ", operation); metrics.putDimensions({ - FunctionName: "upsertLambda", // eslint-disable-next-line sonarjs/pseudo-random OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, }); @@ -193,7 +196,6 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { `Error processing upsert of record ${record.messageId}`, ); metrics.putDimensions({ - FunctionName: "upsertLambda", // eslint-disable-next-line sonarjs/pseudo-random OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, }); diff --git a/package-lock.json b/package-lock.json index 0f8aeb60d..221eebc49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -791,7 +791,6 @@ "name": "nhs-notify-supplier-api-upsert-letter", "version": "0.0.1", "dependencies": { - "@aws-sdk/client-cloudwatch": "^3.972.0", "@aws-sdk/client-dynamodb": "^3.858.0", "@aws-sdk/lib-dynamodb": "^3.858.0", "@internal/datastore": "*", @@ -1193,514 +1192,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.972.0.tgz", - "integrity": "sha512-6LiuuuIRQ0faBz94dKTL/zrmIMSljQo0JgZ7pNjIIcxXrsQzMTr+AWhvFPlJPorI6h3XcQ5Vkyz20GODyn6TYw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.972.0", - "@aws-sdk/credential-provider-node": "3.972.0", - "@aws-sdk/middleware-host-header": "3.972.0", - "@aws-sdk/middleware-logger": "3.972.0", - "@aws-sdk/middleware-recursion-detection": "3.972.0", - "@aws-sdk/middleware-user-agent": "3.972.0", - "@aws-sdk/region-config-resolver": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@aws-sdk/util-user-agent-browser": "3.972.0", - "@aws-sdk/util-user-agent-node": "3.972.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.6", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-compression": "^4.3.22", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.7", - "@smithy/middleware-retry": "^4.4.23", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.22", - "@smithy/util-defaults-mode-node": "^4.2.25", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sso": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.972.0.tgz", - "integrity": "sha512-5qw6qLiRE4SUiz0hWy878dSR13tSVhbTWhsvFT8mGHe37NRRiaobm5MA2sWD0deRAuO98djSiV+dhWXa1xIFNw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.972.0", - "@aws-sdk/middleware-host-header": "3.972.0", - "@aws-sdk/middleware-logger": "3.972.0", - "@aws-sdk/middleware-recursion-detection": "3.972.0", - "@aws-sdk/middleware-user-agent": "3.972.0", - "@aws-sdk/region-config-resolver": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@aws-sdk/util-user-agent-browser": "3.972.0", - "@aws-sdk/util-user-agent-node": "3.972.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.6", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.7", - "@smithy/middleware-retry": "^4.4.23", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.22", - "@smithy/util-defaults-mode-node": "^4.2.25", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/core": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.972.0.tgz", - "integrity": "sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@aws-sdk/xml-builder": "3.972.0", - "@smithy/core": "^3.20.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.0.tgz", - "integrity": "sha512-kKHoNv+maHlPQOAhYamhap0PObd16SAb3jwaY0KYgNTiSbeXlbGUZPLioo9oA3wU10zItJzx83ClU7d7h40luA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.0.tgz", - "integrity": "sha512-xzEi81L7I5jGUbpmqEHCe7zZr54hCABdj4H+3LzktHYuovV/oqnvoDdvZpGFR0e/KAw1+PL38NbGrpG30j6qlA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.0.tgz", - "integrity": "sha512-ruhAMceUIq2aknFd3jhWxmO0P0Efab5efjyIXOkI9i80g+zDY5VekeSxfqRKStEEJSKSCHDLQuOu0BnAn4Rzew==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/credential-provider-env": "3.972.0", - "@aws-sdk/credential-provider-http": "3.972.0", - "@aws-sdk/credential-provider-login": "3.972.0", - "@aws-sdk/credential-provider-process": "3.972.0", - "@aws-sdk/credential-provider-sso": "3.972.0", - "@aws-sdk/credential-provider-web-identity": "3.972.0", - "@aws-sdk/nested-clients": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.0.tgz", - "integrity": "sha512-SsrsFJsEYAJHO4N/r2P0aK6o8si6f1lprR+Ej8J731XJqTckSGs/HFHcbxOyW/iKt+LNUvZa59/VlJmjhF4bEQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/nested-clients": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.0.tgz", - "integrity": "sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.972.0", - "@aws-sdk/credential-provider-http": "3.972.0", - "@aws-sdk/credential-provider-ini": "3.972.0", - "@aws-sdk/credential-provider-process": "3.972.0", - "@aws-sdk/credential-provider-sso": "3.972.0", - "@aws-sdk/credential-provider-web-identity": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.0.tgz", - "integrity": "sha512-nmzYhamLDJ8K+v3zWck79IaKMc350xZnWsf/GeaXO6E3MewSzd3lYkTiMi7lEp3/UwDm9NHfPguoPm+mhlSWQQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.0.tgz", - "integrity": "sha512-6mYyfk1SrMZ15cH9T53yAF4YSnvq4yU1Xlgm3nqV1gZVQzmF5kr4t/F3BU3ygbvzi4uSwWxG3I3TYYS5eMlAyg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.972.0", - "@aws-sdk/core": "3.972.0", - "@aws-sdk/token-providers": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.0.tgz", - "integrity": "sha512-vsJXBGL8H54kz4T6do3p5elATj5d1izVGUXMluRJntm9/I0be/zUYtdd4oDTM2kSUmd4Zhyw3fMQ9lw7CVhd4A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/nested-clients": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.0.tgz", - "integrity": "sha512-3eztFI6F9/eHtkIaWKN3nT+PM+eQ6p1MALDuNshFk323ixuCZzOOVT8oUqtZa30Z6dycNXJwhlIq7NhUVFfimw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.0.tgz", - "integrity": "sha512-ZvdyVRwzK+ra31v1pQrgbqR/KsLD+wwJjHgko6JfoKUBIcEfAwJzQKO6HspHxdHWTVUz6MgvwskheR/TTYZl2g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.0.tgz", - "integrity": "sha512-F2SmUeO+S6l1h6dydNet3BQIk173uAkcfU1HDkw/bUdRLAnh15D3HP9vCZ7oCPBNcdEICbXYDmx0BR9rRUHGlQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.0.tgz", - "integrity": "sha512-kFHQm2OCBJCzGWRafgdWHGFjitUXY/OxXngymcX4l8CiyiNDZB27HDDBg2yLj3OUJc4z4fexLMmP8r9vgag19g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@smithy/core": "^3.20.6", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/nested-clients": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.972.0.tgz", - "integrity": "sha512-QGlbnuGzSQJVG6bR9Qw6G0Blh6abFR4VxNa61ttMbzy9jt28xmk2iGtrYLrQPlCCPhY6enHqjTWm3n3LOb0wAw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.972.0", - "@aws-sdk/middleware-host-header": "3.972.0", - "@aws-sdk/middleware-logger": "3.972.0", - "@aws-sdk/middleware-recursion-detection": "3.972.0", - "@aws-sdk/middleware-user-agent": "3.972.0", - "@aws-sdk/region-config-resolver": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@aws-sdk/util-user-agent-browser": "3.972.0", - "@aws-sdk/util-user-agent-node": "3.972.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.6", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.7", - "@smithy/middleware-retry": "^4.4.23", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.22", - "@smithy/util-defaults-mode-node": "^4.2.25", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.0.tgz", - "integrity": "sha512-JyOf+R/6vJW8OEVFCAyzEOn2reri/Q+L0z9zx4JQSKWvTmJ1qeFO25sOm8VIfB8URKhfGRTQF30pfYaH2zxt/A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/token-providers": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.972.0.tgz", - "integrity": "sha512-kWlXG+y5nZhgXGEtb72Je+EvqepBPs8E3vZse//1PYLWs2speFqbGE/ywCXmzEJgHgVqSB/u/lqBvs5WlYmSqQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/nested-clients": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/types": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.972.0.tgz", - "integrity": "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.972.0.tgz", - "integrity": "sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.0.tgz", - "integrity": "sha512-eOLdkQyoRbDgioTS3Orr7iVsVEutJyMZxvyZ6WAF95IrF0kfWx5Rd/KXnfbnG/VKa2CvjZiitWfouLzfVEyvJA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@smithy/types": "^4.12.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.0.tgz", - "integrity": "sha512-GOy+AiSrE9kGiojiwlZvVVSXwylu4+fmP0MJfvras/MwP09RB/YtQuOVR1E0fKQc6OMwaTNBjgAbOEhxuWFbAw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/xml-builder": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.0.tgz", - "integrity": "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.971.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.971.0.tgz", @@ -6880,27 +6371,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/middleware-compression": { - "version": "4.3.25", - "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.3.25.tgz", - "integrity": "sha512-YWL0Mk2WCjHYbSWuD85oTlB0T7nm+YWTHJWMEac+1Uy1NtnHqnFnX4YvRGG9S6Q++aXaVZdbff8uXl2dC3RLfg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.21.0", - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "fflate": "0.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", @@ -13922,12 +13392,6 @@ } } }, - "node_modules/fflate": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", - "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==", - "license": "MIT" - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", From 0233eb8162670de64bb2af59d4cfbc2ffe75600c Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Mon, 26 Jan 2026 13:52:43 +0000 Subject: [PATCH 12/17] fix linting errors --- lambdas/upsert-letter/src/handler/upsert-handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index b5b964aa9..d69d636b4 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -179,8 +179,8 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { const type = getType(letterEvent); const operation = getOperationFromType(type); - console.log("letterEvent: ", letterEvent); - console.log("operation: ", operation); + console.log("letterEvent:", letterEvent); + console.log("operation:", operation); metrics.putDimensions({ // eslint-disable-next-line sonarjs/pseudo-random OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, From 6198984823a40f3feee9d50946c4d8230c198e6e Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Mon, 26 Jan 2026 15:20:58 +0000 Subject: [PATCH 13/17] emit metrics per supplier and with correct count --- .../src/handler/upsert-handler.ts | 62 +++++++++++-------- tests/resources/prepared-letter.json | 3 +- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index d69d636b4..e79ee6bbc 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -19,7 +19,7 @@ import { LetterRequestPreparedEventV2, } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering"; import z from "zod"; -import { Unit, metricScope } from "aws-embedded-metrics"; +import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics"; import { Deps } from "../config/deps"; type SupplierSpec = { supplierId: string; specId: string }; @@ -153,58 +153,70 @@ async function runUpsert( throw new Error("No matching schema for received message"); } +async function emitMetrics( + metrics: MetricsLogger, + successMetrics: Map, + failMetrics: Map, +) { + metrics.setNamespace(process.env.AWS_LAMBDA_FUNCTION_NAME || `upsertLetter`); + // emit success metrics + for (const [supplier, count] of Object.entries(successMetrics)) { + metrics.putDimensions({ + Supplier: supplier, + }); + metrics.putMetric("MessagesProcessed", count, Unit.Count); + } + // emit failure metrics + for (const [supplier, count] of Object.entries(failMetrics)) { + metrics.putDimensions({ + Supplier: supplier, + }); + metrics.putMetric("MessageFailed", count, Unit.Count); + } +} + export default function createUpsertLetterHandler(deps: Deps): SQSHandler { return metricScope((metrics) => { return async (event: SQSEvent) => { - console.log( - "the environment variables:", - JSON.stringify(process.env, null, 2), - ); - console.log("The SQSEvent:", event); const batchItemFailures: SQSBatchItemFailure[] = []; - metrics.setNamespace( - process.env.AWS_LAMBDA_FUNCTION_NAME || "upsertletter", - ); + const perSupplierSuccess: Map = new Map(); + const perSupplierFailure: Map = new Map(); const tasks = event.Records.map(async (record) => { + let supplier = "unknown"; try { const message: string = parseSNSNotification(record); - console.log("the message:", message); - const snsEvent = JSON.parse(message); - console.log("the snsEvent:", snsEvent); - + supplier = snsEvent.data.supplierId || "unknown"; const letterEvent: unknown = removeEventBridgeWrapper(snsEvent); - const type = getType(letterEvent); const operation = getOperationFromType(type); - console.log("letterEvent:", letterEvent); - console.log("operation:", operation); metrics.putDimensions({ - // eslint-disable-next-line sonarjs/pseudo-random - OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, + supplier: snsEvent.data.supplierId || "unknown", }); await runUpsert(operation, letterEvent, deps); - metrics.setProperty("operation", operation.name); - metrics.putMetric("MessagesProcessed", 1, Unit.Count); + perSupplierSuccess.set( + supplier, + (perSupplierSuccess.get(supplier) || 0) + 1, + ); } catch (error) { deps.logger.error( { err: error, message: record.body }, `Error processing upsert of record ${record.messageId}`, ); - metrics.putDimensions({ - // eslint-disable-next-line sonarjs/pseudo-random - OddOrEven: `${Math.floor(Math.random() * 10) % 2}`, - }); - metrics.putMetric("MessageFailed", 1, Unit.Count); + perSupplierFailure.set( + supplier, + (perSupplierFailure.get(supplier) || 0) + 1, + ); batchItemFailures.push({ itemIdentifier: record.messageId }); } }); await Promise.all(tasks); + await emitMetrics(metrics, perSupplierSuccess, perSupplierFailure); return { batchItemFailures }; }; diff --git a/tests/resources/prepared-letter.json b/tests/resources/prepared-letter.json index 50f6eba6c..f9c1fea55 100644 --- a/tests/resources/prepared-letter.json +++ b/tests/resources/prepared-letter.json @@ -4,13 +4,14 @@ "clientId": "testClientId", "createdAt": "2025-08-28T08:45:00.000Z", "domainId": "letter1", - "letterVariantId": "lv1", + "letterVariantId": "notify-standard", "pageCount": 2, "requestId": "0o5Fs0EELR0fUjHjbCnEtdUwQe3", "requestItemId": "0o5Fs0EELR0fUjHjbCnEtdUwQe4", "requestItemPlanId": "0o5Fs0EELR0fUjHjbCnEtdUwQe5", "sha256Hash": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6", "status": "PREPARED", + "supplierId": "testSupplierId", "templateId": "template_123", "url": "s3://nhs-820178564574-eu-west-2-pr280-supapi-test-letters/letter1.png" }, From e965c60277419eb5a336be9c0df31b95805e8c6c Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Mon, 26 Jan 2026 15:58:04 +0000 Subject: [PATCH 14/17] correct emitMetrics function --- .../src/handler/upsert-handler.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index e79ee6bbc..32d66ee6b 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -160,14 +160,14 @@ async function emitMetrics( ) { metrics.setNamespace(process.env.AWS_LAMBDA_FUNCTION_NAME || `upsertLetter`); // emit success metrics - for (const [supplier, count] of Object.entries(successMetrics)) { + for (const [supplier, count] of successMetrics) { metrics.putDimensions({ Supplier: supplier, }); metrics.putMetric("MessagesProcessed", count, Unit.Count); } // emit failure metrics - for (const [supplier, count] of Object.entries(failMetrics)) { + for (const [supplier, count] of failMetrics) { metrics.putDimensions({ Supplier: supplier, }); @@ -175,6 +175,13 @@ async function emitMetrics( } } +function getSupplierId(snsEvent: any): string { + if (snsEvent && snsEvent.data && snsEvent.data.supplierId) { + return snsEvent.data.supplierId; + } + return "unknown"; +} + export default function createUpsertLetterHandler(deps: Deps): SQSHandler { return metricScope((metrics) => { return async (event: SQSEvent) => { @@ -187,14 +194,11 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { try { const message: string = parseSNSNotification(record); const snsEvent = JSON.parse(message); - supplier = snsEvent.data.supplierId || "unknown"; + supplier = getSupplierId(snsEvent); const letterEvent: unknown = removeEventBridgeWrapper(snsEvent); const type = getType(letterEvent); const operation = getOperationFromType(type); - metrics.putDimensions({ - supplier: snsEvent.data.supplierId || "unknown", - }); await runUpsert(operation, letterEvent, deps); @@ -216,8 +220,8 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler { }); await Promise.all(tasks); - await emitMetrics(metrics, perSupplierSuccess, perSupplierFailure); + await emitMetrics(metrics, perSupplierSuccess, perSupplierFailure); return { batchItemFailures }; }; }); From 038d75564d571e3e3378dc3f588b7470c063e36c Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Thu, 29 Jan 2026 16:01:06 +0000 Subject: [PATCH 15/17] add metrics for rest of Lambda functions --- lambdas/api-handler/package.json | 1 + .../src/handlers/get-letter-data.ts | 83 +++++----- .../api-handler/src/handlers/get-letter.ts | 98 +++++++----- .../api-handler/src/handlers/get-letters.ts | 104 ++++++++----- .../api-handler/src/handlers/patch-letter.ts | 130 +++++++++------- .../api-handler/src/handlers/post-letters.ts | 143 +++++++++++------- lambdas/api-handler/src/handlers/post-mi.ts | 116 ++++++++------ .../src/services/letter-operations.ts | 23 ++- lambdas/api-handler/src/utils/metrics.ts | 22 +++ .../letter-updates-transformer/package.json | 1 + .../src/letter-updates-transformer.ts | 86 +++++++---- lambdas/mi-updates-transformer/package.json | 1 + .../src/mi-updates-transformer.ts | 62 +++++--- .../src/handler/upsert-handler.ts | 6 +- package-lock.json | 3 + 15 files changed, 551 insertions(+), 328 deletions(-) create mode 100644 lambdas/api-handler/src/utils/metrics.ts diff --git a/lambdas/api-handler/package.json b/lambdas/api-handler/package.json index 1306f33d3..92fb12998 100644 --- a/lambdas/api-handler/package.json +++ b/lambdas/api-handler/package.json @@ -7,6 +7,7 @@ "@aws-sdk/s3-request-presigner": "^3.925.0", "@internal/datastore": "*", "@internal/helpers": "*", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.25.11", "pino": "^9.7.0", diff --git a/lambdas/api-handler/src/handlers/get-letter-data.ts b/lambdas/api-handler/src/handlers/get-letter-data.ts index 4d516645b..737fdec1c 100644 --- a/lambdas/api-handler/src/handlers/get-letter-data.ts +++ b/lambdas/api-handler/src/handlers/get-letter-data.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyHandler } from "aws-lambda"; +import { MetricsLogger, metricScope } from "aws-embedded-metrics"; import { assertNotEmpty } from "../utils/validation"; import { extractCommonIds } from "../utils/common-ids"; import { ApiErrorDetail } from "../contracts/errors"; @@ -6,46 +7,60 @@ import { processError } from "../mappers/error-mapper"; import ValidationError from "../errors/validation-error"; import { getLetterDataUrl } from "../services/letter-operations"; import type { Deps } from "../config/deps"; +import { MetricStatus, emitForSingleSupplier } from "../utils/metrics"; export default function createGetLetterDataHandler( deps: Deps, ): APIGatewayProxyHandler { - return async (event) => { - const commonIds = extractCommonIds( - event.headers, - event.requestContext, - deps, - ); - - if (!commonIds.ok) { - return processError( - commonIds.error, - commonIds.correlationId, - deps.logger, + return metricScope((metrics: MetricsLogger) => { + return async (event) => { + const commonIds = extractCommonIds( + event.headers, + event.requestContext, + deps, ); - } - try { - const letterId = assertNotEmpty( - event.pathParameters?.id, - new ValidationError( - ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter, - ), - ); + if (!commonIds.ok) { + return processError( + commonIds.error, + commonIds.correlationId, + deps.logger, + ); + } - return { - statusCode: 303, - headers: { - Location: await getLetterDataUrl( - commonIds.value.supplierId, - letterId, - deps, + const { supplierId } = commonIds.value; + try { + const letterId = assertNotEmpty( + event.pathParameters?.id, + new ValidationError( + ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter, ), - }, - body: "", - }; - } catch (error) { - return processError(error, commonIds.value.correlationId, deps.logger); - } - }; + ); + + emitForSingleSupplier( + metrics, + "getLetterData", + supplierId, + 1, + MetricStatus.Success, + ); + return { + statusCode: 303, + headers: { + Location: await getLetterDataUrl(supplierId, letterId, deps), + }, + body: "", + }; + } catch (error) { + emitForSingleSupplier( + metrics, + "getLetterData", + supplierId, + 1, + MetricStatus.Failure, + ); + return processError(error, commonIds.value.correlationId, deps.logger); + } + }; + }); } diff --git a/lambdas/api-handler/src/handlers/get-letter.ts b/lambdas/api-handler/src/handlers/get-letter.ts index a88a48822..fcd3b0990 100644 --- a/lambdas/api-handler/src/handlers/get-letter.ts +++ b/lambdas/api-handler/src/handlers/get-letter.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyHandler } from "aws-lambda"; +import { MetricsLogger, metricScope } from "aws-embedded-metrics"; import { assertNotEmpty } from "../utils/validation"; import { extractCommonIds } from "../utils/common-ids"; import ValidationError from "../errors/validation-error"; @@ -7,53 +8,72 @@ import { getLetterById } from "../services/letter-operations"; import { processError } from "../mappers/error-mapper"; import { mapToGetLetterResponse } from "../mappers/letter-mapper"; import { Deps } from "../config/deps"; +import { MetricStatus, emitForSingleSupplier } from "../utils/metrics"; +// Get letter data export default function createGetLetterHandler( deps: Deps, ): APIGatewayProxyHandler { - return async (event) => { - const commonIds = extractCommonIds( - event.headers, - event.requestContext, - deps, - ); - - if (!commonIds.ok) { - return processError( - commonIds.error, - commonIds.correlationId, - deps.logger, + return metricScope((metrics: MetricsLogger) => { + return async (event) => { + const commonIds = extractCommonIds( + event.headers, + event.requestContext, + deps, ); - } - try { - const letterId = assertNotEmpty( - event.pathParameters?.id, - new ValidationError( - ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter, - ), - ); + if (!commonIds.ok) { + return processError( + commonIds.error, + commonIds.correlationId, + deps.logger, + ); + } - const letter = await getLetterById( - commonIds.value.supplierId, - letterId, - deps.letterRepo, - ); + const { supplierId } = commonIds.value; + try { + const letterId = assertNotEmpty( + event.pathParameters?.id, + new ValidationError( + ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter, + ), + ); + + const letter = await getLetterById( + supplierId, + letterId, + deps.letterRepo, + ); - const response = mapToGetLetterResponse(letter); + const response = mapToGetLetterResponse(letter); - deps.logger.info({ - description: "Letter successfully fetched by id", - supplierId: commonIds.value.supplierId, - letterId, - }); + deps.logger.info({ + description: "Letter successfully fetched by id", + supplierId, + letterId, + }); - return { - statusCode: 200, - body: JSON.stringify(response, null, 2), - }; - } catch (error) { - return processError(error, commonIds.value.correlationId, deps.logger); - } - }; + emitForSingleSupplier( + metrics, + "getLetter", + supplierId, + 1, + MetricStatus.Success, + ); + return { + statusCode: 200, + body: JSON.stringify(response, null, 2), + }; + } catch (error) { + emitForSingleSupplier( + metrics, + "getLetter", + supplierId, + 1, + MetricStatus.Failure, + ); + return processError(error, commonIds.value.correlationId, deps.logger); + } + }; + }); } diff --git a/lambdas/api-handler/src/handlers/get-letters.ts b/lambdas/api-handler/src/handlers/get-letters.ts index adc3febc4..a1d1c47f4 100644 --- a/lambdas/api-handler/src/handlers/get-letters.ts +++ b/lambdas/api-handler/src/handlers/get-letters.ts @@ -3,6 +3,7 @@ import { APIGatewayProxyHandler, } from "aws-lambda"; import { Logger } from "pino"; +import { MetricsLogger, metricScope } from "aws-embedded-metrics"; import { getLettersForSupplier } from "../services/letter-operations"; import { extractCommonIds } from "../utils/common-ids"; import { requireEnvVar } from "../utils/validation"; @@ -11,7 +12,9 @@ import { processError } from "../mappers/error-mapper"; import ValidationError from "../errors/validation-error"; import { mapToGetLettersResponse } from "../mappers/letter-mapper"; import type { Deps } from "../config/deps"; +import { MetricStatus, emitForSingleSupplier } from "../utils/metrics"; +// List letters Handlers // The endpoint should only return pending letters for now const status = "PENDING"; @@ -82,53 +85,70 @@ function getLimitOrDefault( export default function createGetLettersHandler( deps: Deps, ): APIGatewayProxyHandler { - return async (event) => { - const commonIds = extractCommonIds( - event.headers, - event.requestContext, - deps, - ); - - if (!commonIds.ok) { - return processError( - commonIds.error, - commonIds.correlationId, - deps.logger, + return metricScope((metrics: MetricsLogger) => { + return async (event) => { + const commonIds = extractCommonIds( + event.headers, + event.requestContext, + deps, ); - } - try { - const maxLimit = requireEnvVar(deps.env, "MAX_LIMIT"); + if (!commonIds.ok) { + return processError( + commonIds.error, + commonIds.correlationId, + deps.logger, + ); + } - const limitNumber = getLimitOrDefault( - event.queryStringParameters, - maxLimit, - deps.logger, - ); + const { supplierId } = commonIds.value; + try { + const maxLimit = requireEnvVar(deps.env, "MAX_LIMIT"); - const letters = await getLettersForSupplier( - commonIds.value.supplierId, - status, - limitNumber, - deps.letterRepo, - ); + const limitNumber = getLimitOrDefault( + event.queryStringParameters, + maxLimit, + deps.logger, + ); + + const letters = await getLettersForSupplier( + supplierId, + status, + limitNumber, + deps.letterRepo, + ); - const response = mapToGetLettersResponse(letters); + const response = mapToGetLettersResponse(letters); - deps.logger.info({ - description: "Pending letters successfully fetched", - supplierId: commonIds.value.supplierId, - limitNumber, - status, - lettersCount: letters.length, - }); + deps.logger.info({ + description: "Pending letters successfully fetched", + supplierId, + limitNumber, + status, + lettersCount: letters.length, + }); - return { - statusCode: 200, - body: JSON.stringify(response, null, 2), - }; - } catch (error) { - return processError(error, commonIds.value.correlationId, deps.logger); - } - }; + emitForSingleSupplier( + metrics, + "getLetters", + supplierId, + letters.length, + MetricStatus.Success, + ); + return { + statusCode: 200, + body: JSON.stringify(response, null, 2), + }; + } catch (error) { + emitForSingleSupplier( + metrics, + "getLetters", + supplierId, + 1, + MetricStatus.Failure, + ); + return processError(error, commonIds.value.correlationId, deps.logger); + } + }; + }); } diff --git a/lambdas/api-handler/src/handlers/patch-letter.ts b/lambdas/api-handler/src/handlers/patch-letter.ts index 6d0be3c2c..d3c34d956 100644 --- a/lambdas/api-handler/src/handlers/patch-letter.ts +++ b/lambdas/api-handler/src/handlers/patch-letter.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyHandler } from "aws-lambda"; +import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics"; import { enqueueLetterUpdateRequests } from "../services/letter-operations"; import { PatchLetterRequest, @@ -12,74 +13,91 @@ import { assertNotEmpty } from "../utils/validation"; import { extractCommonIds } from "../utils/common-ids"; import { mapToUpdateCommand } from "../mappers/letter-mapper"; import type { Deps } from "../config/deps"; +import { MetricStatus } from "../utils/metrics"; export default function createPatchLetterHandler( deps: Deps, ): APIGatewayProxyHandler { - return async (event) => { - const commonIds = extractCommonIds( - event.headers, - event.requestContext, - deps, - ); - - if (!commonIds.ok) { - return processError( - commonIds.error, - commonIds.correlationId, - deps.logger, + return metricScope((metrics: MetricsLogger) => { + return async (event) => { + const commonIds = extractCommonIds( + event.headers, + event.requestContext, + deps, ); - } - try { - const letterId = assertNotEmpty( - event.pathParameters?.id, - new ValidationError( - ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter, - ), - ); - const body = assertNotEmpty( - event.body, - new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), - ); + if (!commonIds.ok) { + return processError( + commonIds.error, + commonIds.correlationId, + deps.logger, + ); + } - let patchLetterRequest: PatchLetterRequest; + const { supplierId } = commonIds.value; + metrics.setNamespace( + process.env.AWS_LAMBDA_FUNCTION_NAME || "patchLetters", + ); try { - patchLetterRequest = PatchLetterRequestSchema.parse(JSON.parse(body)); - } catch (error) { - const typedError = - error instanceof Error - ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { - cause: error, - }) - : error; - throw typedError; - } + const letterId = assertNotEmpty( + event.pathParameters?.id, + new ValidationError( + ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter, + ), + ); + const body = assertNotEmpty( + event.body, + new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), + ); - const updateLetterCommand: UpdateLetterCommand = mapToUpdateCommand( - patchLetterRequest, - commonIds.value.supplierId, - ); + let patchLetterRequest: PatchLetterRequest; + + try { + patchLetterRequest = PatchLetterRequestSchema.parse(JSON.parse(body)); + } catch (error) { + const typedError = + error instanceof Error + ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { + cause: error, + }) + : error; + throw typedError; + } - if (updateLetterCommand.id !== letterId) { - throw new ValidationError( - ApiErrorDetail.InvalidRequestLetterIdsMismatch, + const updateLetterCommand: UpdateLetterCommand = mapToUpdateCommand( + patchLetterRequest, + supplierId, ); - } - await enqueueLetterUpdateRequests( - [updateLetterCommand], - commonIds.value.correlationId, - deps, - ); + if (updateLetterCommand.id !== letterId) { + throw new ValidationError( + ApiErrorDetail.InvalidRequestLetterIdsMismatch, + ); + } + + await enqueueLetterUpdateRequests( + [updateLetterCommand], + commonIds.value.correlationId, + deps, + ); - return { - statusCode: 202, - body: "", - }; - } catch (error) { - return processError(error, commonIds.value.correlationId, deps.logger); - } - }; + metrics.putDimensions({ + supplier: supplierId, + status: updateLetterCommand.status, + }); + metrics.putMetric(MetricStatus.Success, 1, Unit.Count); + return { + statusCode: 202, + body: "", + }; + } catch (error) { + metrics.putDimensions({ + supplier: supplierId, + }); + metrics.putMetric(MetricStatus.Success, 1, Unit.Count); + return processError(error, commonIds.value.correlationId, deps.logger); + } + }; + }); } diff --git a/lambdas/api-handler/src/handlers/post-letters.ts b/lambdas/api-handler/src/handlers/post-letters.ts index d8f147878..c549ab91c 100644 --- a/lambdas/api-handler/src/handlers/post-letters.ts +++ b/lambdas/api-handler/src/handlers/post-letters.ts @@ -1,4 +1,6 @@ import { APIGatewayProxyHandler } from "aws-lambda"; +import { Metrics$ } from "@aws-sdk/client-s3"; +import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics"; import type { Deps } from "../config/deps"; import { ApiErrorDetail } from "../contracts/errors"; import { @@ -11,78 +13,109 @@ import { mapToUpdateCommands } from "../mappers/letter-mapper"; import { enqueueLetterUpdateRequests } from "../services/letter-operations"; import { extractCommonIds } from "../utils/common-ids"; import { assertNotEmpty, requireEnvVar } from "../utils/validation"; +import { MetricStatus } from "../utils/metrics"; function duplicateIdsExist(postLettersRequest: PostLettersRequest) { const ids = postLettersRequest.data.map((item) => item.id); return new Set(ids).size !== ids.length; } +/** + * emits metrics of successful letter updates, including the supplier and grouped by status + */ +async function emitMetics( + metrics: MetricsLogger, + supplierId: string, + statusesMapping: Map, +) { + for (const [status, count] of statusesMapping) { + metrics.putDimensions({ + supplier: supplierId, + eventType: status, + }); + metrics.putMetric(MetricStatus.Success, count, Unit.Count); + } +} + export default function createPostLettersHandler( deps: Deps, ): APIGatewayProxyHandler { - return async (event) => { - const commonIds = extractCommonIds( - event.headers, - event.requestContext, - deps, - ); - - if (!commonIds.ok) { - return processError( - commonIds.error, - commonIds.correlationId, - deps.logger, + return metricScope((metrics: MetricsLogger) => { + return async (event) => { + const commonIds = extractCommonIds( + event.headers, + event.requestContext, + deps, ); - } - const maxUpdateItems = requireEnvVar(deps.env, "MAX_LIMIT"); - requireEnvVar(deps.env, "QUEUE_URL"); + if (!commonIds.ok) { + return processError( + commonIds.error, + commonIds.correlationId, + deps.logger, + ); + } - try { - const body = assertNotEmpty( - event.body, - new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), + const maxUpdateItems = requireEnvVar(deps.env, "MAX_LIMIT"); + requireEnvVar(deps.env, "QUEUE_URL"); + + const { supplierId } = commonIds.value; + metrics.setNamespace( + process.env.AWS_LAMBDA_FUNCTION_NAME || "postLetters", ); + try { + const body = assertNotEmpty( + event.body, + new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), + ); - let postLettersRequest: PostLettersRequest; + let postLettersRequest: PostLettersRequest; - try { - postLettersRequest = PostLettersRequestSchema.parse(JSON.parse(body)); - } catch (error) { - const typedError = - error instanceof Error - ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { - cause: error, - }) - : error; - throw typedError; - } + try { + postLettersRequest = PostLettersRequestSchema.parse(JSON.parse(body)); + } catch (error) { + const typedError = + error instanceof Error + ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { + cause: error, + }) + : error; + throw typedError; + } - if (postLettersRequest.data.length > maxUpdateItems) { - throw new ValidationError( - ApiErrorDetail.InvalidRequestLettersToUpdate, - { args: [maxUpdateItems] }, - ); - } + if (postLettersRequest.data.length > maxUpdateItems) { + throw new ValidationError( + ApiErrorDetail.InvalidRequestLettersToUpdate, + { args: [maxUpdateItems] }, + ); + } - if (duplicateIdsExist(postLettersRequest)) { - throw new ValidationError( - ApiErrorDetail.InvalidRequestDuplicateLetterId, - ); - } + if (duplicateIdsExist(postLettersRequest)) { + throw new ValidationError( + ApiErrorDetail.InvalidRequestDuplicateLetterId, + ); + } - await enqueueLetterUpdateRequests( - mapToUpdateCommands(postLettersRequest, commonIds.value.supplierId), - commonIds.value.correlationId, - deps, - ); + const statusesMapping = new Map(); + await enqueueLetterUpdateRequests( + mapToUpdateCommands(postLettersRequest, supplierId), + commonIds.value.correlationId, + deps, + statusesMapping, + ); - return { - statusCode: 202, - body: "", - }; - } catch (error) { - return processError(error, commonIds.value.correlationId, deps.logger); - } - }; + await emitMetics(metrics, supplierId, statusesMapping); + return { + statusCode: 202, + body: "", + }; + } catch (error) { + metrics.putDimensions({ + supplier: supplierId, + }); + metrics.putMetric(MetricStatus.Failure, 1, Unit.Count); + return processError(error, commonIds.value.correlationId, deps.logger); + } + }; + }); } diff --git a/lambdas/api-handler/src/handlers/post-mi.ts b/lambdas/api-handler/src/handlers/post-mi.ts index 74fc82e18..4229072aa 100644 --- a/lambdas/api-handler/src/handlers/post-mi.ts +++ b/lambdas/api-handler/src/handlers/post-mi.ts @@ -8,57 +8,87 @@ import { extractCommonIds } from "../utils/common-ids"; import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi"; import { mapToMI } from "../mappers/mi-mapper"; import { Deps } from "../config/deps"; +import { metricScope, MetricsLogger } from "aws-embedded-metrics"; +import { emitForSingleSupplier, MetricStatus } from "../utils/metrics"; export default function createPostMIHandler( deps: Deps, ): APIGatewayProxyHandler { - return async (event) => { - const commonIds = extractCommonIds( - event.headers, - event.requestContext, - deps, - ); - - if (!commonIds.ok) { - return processError( - commonIds.error, - commonIds.correlationId, - deps.logger, - ); - } - - try { - const body = assertNotEmpty( - event.body, - new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), + return metricScope((metrics: MetricsLogger) => { + return async (event) => { + const commonIds = extractCommonIds( + event.headers, + event.requestContext, + deps, ); - let postMIRequest: PostMIRequest; + if (!commonIds.ok) { + return processError( + commonIds.error, + commonIds.correlationId, + deps.logger, + ); + } + const { supplierId } = commonIds.value; try { - postMIRequest = PostMIRequestSchema.parse(JSON.parse(body)); - } catch (error) { - const typedError = - error instanceof Error - ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { - cause: error, - }) - : error; - throw typedError; - } - validateIso8601Timestamp(postMIRequest.data.attributes.timestamp); + const body = assertNotEmpty( + event.body, + new ValidationError(ApiErrorDetail.InvalidRequestMissingBody), + ); - const result = await postMIOperation( - mapToMI(postMIRequest, commonIds.value.supplierId), - deps.miRepo, - ); + let postMIRequest: PostMIRequest; + + try { + postMIRequest = PostMIRequestSchema.parse(JSON.parse(body)); + } catch (error) { + const typedError = + error instanceof Error + ? new ValidationError(ApiErrorDetail.InvalidRequestBody, { + cause: error, + }) + : error; + throw typedError; + } + validateIso8601Timestamp(postMIRequest.data.attributes.timestamp); + + const result = await postMIOperation( + mapToMI(postMIRequest, supplierId), + deps.miRepo, + ); - return { - statusCode: 201, - body: JSON.stringify(result, null, 2), - }; - } catch (error) { - return processError(error, commonIds.value.correlationId, deps.logger); - } - }; + // metric with count 1 specifying the supplier + emitForSingleSupplier( + metrics, + "postMi", + supplierId, + 1, + MetricStatus.Success, + ); + // metric displaying the supplier and the type/number of lineItems posted + emitForSingleSupplier( + metrics, + "postMi", + supplierId, + postMIRequest.data.attributes.quantity, + MetricStatus.Success, + { lineItem: postMIRequest.data.attributes.lineItem }, + ); + + return { + statusCode: 201, + body: JSON.stringify(result, null, 2), + }; + } catch (error) { + emitForSingleSupplier( + metrics, + "postMi", + supplierId, + 1, + MetricStatus.Failure, + ); + return processError(error, commonIds.value.correlationId, deps.logger); + } + }; + }); } diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index 558c2c90c..57b852c6f 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -95,6 +95,7 @@ export async function enqueueLetterUpdateRequests( updateLetterCommands: UpdateLetterCommand[], correlationId: string, deps: Deps, + statusesMapping?: Map, ) { const BATCH_SIZE = 10; // SQS SendMessageBatch max const CONCURRENCY = 5; // number of parallel batch API calls @@ -108,13 +109,21 @@ export async function enqueueLetterUpdateRequests( await Promise.all( window.map(async (batch, batchIdx) => { - const entries = batch.map((request, idx) => ({ - Id: `${i + batchIdx}-${idx}`, // unique per batch entry - MessageBody: JSON.stringify(request), - MessageAttributes: { - CorrelationId: { DataType: "String", StringValue: correlationId }, - }, - })); + const entries = batch.map((request, idx) => { + if (statusesMapping) { + statusesMapping.set( + request.status, + (statusesMapping.get(request.status) || 0) + 1, + ); + } + return { + Id: `${i + batchIdx}-${idx}`, // unique per batch entry + MessageBody: JSON.stringify(request), + MessageAttributes: { + CorrelationId: { DataType: "String", StringValue: correlationId }, + }, + }; + }); const cmd = new SendMessageBatchCommand({ QueueUrl: deps.env.QUEUE_URL, diff --git a/lambdas/api-handler/src/utils/metrics.ts b/lambdas/api-handler/src/utils/metrics.ts new file mode 100644 index 000000000..bd5eef18d --- /dev/null +++ b/lambdas/api-handler/src/utils/metrics.ts @@ -0,0 +1,22 @@ +import { MetricsLogger, Unit } from "aws-embedded-metrics"; + +export function emitForSingleSupplier( + metrics: MetricsLogger, + functionName: string, + supplierId: string, + count: number, + message: string, + dimensions?: Record, +) { + metrics.setNamespace(process.env.AWS_LAMBDA_FUNCTION_NAME || functionName); + metrics.putDimensions({ + ...dimensions, + Supplier: supplierId, + }); + metrics.putMetric(message, count, Unit.Count); +} + +export declare enum MetricStatus { + Success = "success", + Failure = "failure", +} diff --git a/lambdas/letter-updates-transformer/package.json b/lambdas/letter-updates-transformer/package.json index b563d16da..bd80badf5 100644 --- a/lambdas/letter-updates-transformer/package.json +++ b/lambdas/letter-updates-transformer/package.json @@ -5,6 +5,7 @@ "@internal/datastore": "^0.1.0", "@internal/helpers": "^0.1.0", "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.27.2", "pino": "^10.1.0", diff --git a/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts b/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts index c53151c07..5ff0b1705 100644 --- a/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts +++ b/lambdas/letter-updates-transformer/src/letter-updates-transformer.ts @@ -10,6 +10,7 @@ import { PublishBatchRequestEntry, } from "@aws-sdk/client-sns"; import { LetterEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src"; +import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics"; import mapLetterToCloudEvent from "./mappers/letter-mapper"; import { Deps } from "./deps"; import { LetterForEventPub, LetterSchemaForEventPub } from "./types"; @@ -18,39 +19,64 @@ import { LetterForEventPub, LetterSchemaForEventPub } from "./types"; const BATCH_SIZE = 10; export default function createHandler(deps: Deps): Handler { - return async (streamEvent: KinesisStreamEvent) => { - deps.logger.info({ description: "Received event", streamEvent }); - deps.logger.info({ - description: "Number of records", - count: streamEvent.Records?.length || 0, - }); - - // Ensure logging by extracting all records first - const ddbRecords: DynamoDBRecord[] = streamEvent.Records.map((record) => - extractPayload(record, deps), - ); - - const cloudEvents: LetterEvent[] = ddbRecords - .filter((record) => filterRecord(record, deps)) - .map((element) => extractNewLetter(element)) - .map((element) => mapLetterToCloudEvent(element, deps.env.EVENT_SOURCE)); - - for (const batch of generateBatches(cloudEvents)) { + return metricScope((metrics: MetricsLogger) => { + return async (streamEvent: KinesisStreamEvent) => { + deps.logger.info({ description: "Received event", streamEvent }); deps.logger.info({ - description: "Publishing batch", - size: batch.length, - letterEvents: batch, + description: "Number of records", + count: streamEvent.Records?.length || 0, }); - await deps.snsClient.send( - new PublishBatchCommand({ - TopicArn: deps.env.EVENTPUB_SNS_TOPIC_ARN, - PublishBatchRequestEntries: batch.map((element, index) => - buildMessage(element, index), - ), - }), + + // Ensure logging by extracting all records first + const ddbRecords: DynamoDBRecord[] = streamEvent.Records.map((record) => + extractPayload(record, deps), ); - } - }; + + const cloudEvents: LetterEvent[] = ddbRecords + .filter((record) => filterRecord(record, deps)) + .map((element) => extractNewLetter(element)) + .map((element) => + mapLetterToCloudEvent(element, deps.env.EVENT_SOURCE), + ); + + const eventTypeCount = new Map(); + for (const batch of generateBatches(cloudEvents)) { + deps.logger.info({ + description: "Publishing batch", + size: batch.length, + letterEvents: batch, + }); + await deps.snsClient.send( + new PublishBatchCommand({ + TopicArn: deps.env.EVENTPUB_SNS_TOPIC_ARN, + PublishBatchRequestEntries: batch.map((element, index) => { + eventTypeCount.set( + element.type, + (eventTypeCount.get(element.type) || 0) + 1, + ); + return buildMessage(element, index); + }), + }), + ); + } + await emitMetrics(metrics, eventTypeCount); + }; + }); +} + +async function emitMetrics( + metrics: MetricsLogger, + eventTypeCount: Map, +) { + metrics.setNamespace( + process.env.AWS_LAMBDA_FUNCTION_NAME || "letter-updates-transformer", + ); + for (const [type, count] of eventTypeCount) { + metrics.putDimensions({ + eventType: type, + }); + metrics.putMetric("events published", count, Unit.Count); + } } function filterRecord(record: DynamoDBRecord, deps: Deps): boolean { diff --git a/lambdas/mi-updates-transformer/package.json b/lambdas/mi-updates-transformer/package.json index 35dd5567d..826ab2da1 100644 --- a/lambdas/mi-updates-transformer/package.json +++ b/lambdas/mi-updates-transformer/package.json @@ -4,6 +4,7 @@ "@aws-sdk/util-dynamodb": "^3.943.0", "@internal/datastore": "^0.1.0", "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.24.0", "pino": "^10.1.0", diff --git a/lambdas/mi-updates-transformer/src/mi-updates-transformer.ts b/lambdas/mi-updates-transformer/src/mi-updates-transformer.ts index 107b616fa..9c4d2926a 100644 --- a/lambdas/mi-updates-transformer/src/mi-updates-transformer.ts +++ b/lambdas/mi-updates-transformer/src/mi-updates-transformer.ts @@ -11,6 +11,7 @@ import { PublishBatchRequestEntry, } from "@aws-sdk/client-sns"; import { MISubmittedEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src"; +import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics"; import { mapMIToCloudEvent } from "./mappers/mi-mapper"; import { Deps } from "./deps"; @@ -49,26 +50,49 @@ function extractMIData(record: DynamoDBRecord): MI { return MISchema.parse(unmarshall(newImage as any)); } +async function emitMetrics( + metrics: MetricsLogger, + eventTypeCount: Map, +) { + metrics.setNamespace( + process.env.AWS_LAMBDA_FUNCTION_NAME || "letter-updates-transformer", + ); + for (const [type, count] of eventTypeCount) { + metrics.putDimensions({ + eventType: type, + }); + metrics.putMetric("events published", count, Unit.Count); + } +} + export default function createHandler(deps: Deps): Handler { - return async (streamEvent: KinesisStreamEvent) => { - deps.logger.info({ description: "Received event", streamEvent }); + return metricScope((metrics: MetricsLogger) => { + return async (streamEvent: KinesisStreamEvent) => { + deps.logger.info({ description: "Received event", streamEvent }); - const cloudEvents: MISubmittedEvent[] = streamEvent.Records.map((record) => - extractPayload(record, deps), - ) - .filter((record) => record.eventName === "INSERT") - .map((element) => extractMIData(element)) - .map((payload) => mapMIToCloudEvent(payload, deps)); + const cloudEvents: MISubmittedEvent[] = streamEvent.Records.map( + (record) => extractPayload(record, deps), + ) + .filter((record) => record.eventName === "INSERT") + .map((element) => extractMIData(element)) + .map((payload) => mapMIToCloudEvent(payload, deps)); - for (const batch of generateBatches(cloudEvents)) { - await deps.snsClient.send( - new PublishBatchCommand({ - TopicArn: deps.env.EVENTPUB_SNS_TOPIC_ARN, - PublishBatchRequestEntries: batch.map((element) => - buildMessage(element, deps), - ), - }), - ); - } - }; + const eventTypeCount = new Map(); + for (const batch of generateBatches(cloudEvents)) { + await deps.snsClient.send( + new PublishBatchCommand({ + TopicArn: deps.env.EVENTPUB_SNS_TOPIC_ARN, + PublishBatchRequestEntries: batch.map((element) => { + eventTypeCount.set( + element.type, + (eventTypeCount.get(element.type) || 0) + 1, + ); + return buildMessage(element, deps); + }), + }), + ); + } + await emitMetrics(metrics, eventTypeCount); + }; + }); } diff --git a/lambdas/upsert-letter/src/handler/upsert-handler.ts b/lambdas/upsert-letter/src/handler/upsert-handler.ts index 32d66ee6b..c94084610 100644 --- a/lambdas/upsert-letter/src/handler/upsert-handler.ts +++ b/lambdas/upsert-letter/src/handler/upsert-handler.ts @@ -156,7 +156,7 @@ async function runUpsert( async function emitMetrics( metrics: MetricsLogger, successMetrics: Map, - failMetrics: Map, + failedMetrics: Map, ) { metrics.setNamespace(process.env.AWS_LAMBDA_FUNCTION_NAME || `upsertLetter`); // emit success metrics @@ -167,7 +167,7 @@ async function emitMetrics( metrics.putMetric("MessagesProcessed", count, Unit.Count); } // emit failure metrics - for (const [supplier, count] of failMetrics) { + for (const [supplier, count] of failedMetrics) { metrics.putDimensions({ Supplier: supplier, }); @@ -183,7 +183,7 @@ function getSupplierId(snsEvent: any): string { } export default function createUpsertLetterHandler(deps: Deps): SQSHandler { - return metricScope((metrics) => { + return metricScope((metrics: MetricsLogger) => { return async (event: SQSEvent) => { const batchItemFailures: SQSBatchItemFailure[] = []; const perSupplierSuccess: Map = new Map(); diff --git a/package-lock.json b/package-lock.json index 221eebc49..c99dfe8cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,7 @@ "@aws-sdk/s3-request-presigner": "^3.925.0", "@internal/datastore": "*", "@internal/helpers": "*", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.25.11", "pino": "^9.7.0", @@ -269,6 +270,7 @@ "@internal/datastore": "^0.1.0", "@internal/helpers": "^0.1.0", "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.27.2", "pino": "^10.1.0", @@ -333,6 +335,7 @@ "@aws-sdk/util-dynamodb": "^3.943.0", "@internal/datastore": "^0.1.0", "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "aws-embedded-metrics": "^4.2.1", "aws-lambda": "^1.0.7", "esbuild": "^0.24.0", "pino": "^10.1.0", From f022bf90af4399c6626c9c84b65b1b7b229d62e6 Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Fri, 30 Jan 2026 12:43:54 +0000 Subject: [PATCH 16/17] fix failing unit tests --- .../api-handler/src/handlers/post-letters.ts | 4 +- lambdas/api-handler/src/handlers/post-mi.ts | 4 +- .../mappers/__tests__/letter-mapper.test.ts | 41 +++++++++++++++++++ .../api-handler/src/mappers/letter-mapper.ts | 24 +++++++---- .../src/services/letter-operations.ts | 23 ++++------- lambdas/api-handler/src/utils/metrics.ts | 2 +- 6 files changed, 69 insertions(+), 29 deletions(-) diff --git a/lambdas/api-handler/src/handlers/post-letters.ts b/lambdas/api-handler/src/handlers/post-letters.ts index c549ab91c..9bc83cd7f 100644 --- a/lambdas/api-handler/src/handlers/post-letters.ts +++ b/lambdas/api-handler/src/handlers/post-letters.ts @@ -1,5 +1,4 @@ import { APIGatewayProxyHandler } from "aws-lambda"; -import { Metrics$ } from "@aws-sdk/client-s3"; import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics"; import type { Deps } from "../config/deps"; import { ApiErrorDetail } from "../contracts/errors"; @@ -98,10 +97,9 @@ export default function createPostLettersHandler( const statusesMapping = new Map(); await enqueueLetterUpdateRequests( - mapToUpdateCommands(postLettersRequest, supplierId), + mapToUpdateCommands(postLettersRequest, supplierId, statusesMapping), commonIds.value.correlationId, deps, - statusesMapping, ); await emitMetics(metrics, supplierId, statusesMapping); diff --git a/lambdas/api-handler/src/handlers/post-mi.ts b/lambdas/api-handler/src/handlers/post-mi.ts index 4229072aa..bb44b1c70 100644 --- a/lambdas/api-handler/src/handlers/post-mi.ts +++ b/lambdas/api-handler/src/handlers/post-mi.ts @@ -1,4 +1,5 @@ import { APIGatewayProxyHandler } from "aws-lambda"; +import { MetricsLogger, metricScope } from "aws-embedded-metrics"; import postMIOperation from "../services/mi-operations"; import { ApiErrorDetail } from "../contracts/errors"; import ValidationError from "../errors/validation-error"; @@ -8,8 +9,7 @@ import { extractCommonIds } from "../utils/common-ids"; import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi"; import { mapToMI } from "../mappers/mi-mapper"; import { Deps } from "../config/deps"; -import { metricScope, MetricsLogger } from "aws-embedded-metrics"; -import { emitForSingleSupplier, MetricStatus } from "../utils/metrics"; +import { MetricStatus, emitForSingleSupplier } from "../utils/metrics"; export default function createPostMIHandler( deps: Deps, diff --git a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts index fa7f9f81e..683fd7fb8 100644 --- a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts @@ -3,14 +3,55 @@ import { mapToGetLetterResponse, mapToGetLettersResponse, mapToPatchLetterResponse, + mapToUpdateCommands, } from "../letter-mapper"; import { GetLetterResponse, GetLettersResponse, PatchLetterResponse, + PostLettersRequest, } from "../../contracts/letters"; describe("letter-mapper", () => { + it("maps PostLetterRequest to UpdateLetterCommands", () => { + const request: PostLettersRequest = { + data: [ + { + id: "id1", + type: "Letter", + attributes: { + status: "REJECTED", + reasonCode: "123", + reasonText: "Reason text", + }, + }, + { id: "id2", type: "Letter", attributes: { status: "ACCEPTED" } }, + { id: "id3", type: "Letter", attributes: { status: "DELIVERED" } }, + ], + }; + const supplierId = "testSupplierId"; + const updateLetterCommands = mapToUpdateCommands(request, supplierId); + expect(updateLetterCommands).toEqual([ + { + id: "id1", + reasonCode: "123", + reasonText: "Reason text", + supplierId: "testSupplierId", + status: "REJECTED", + }, + { + id: "id2", + supplierId: "testSupplierId", + status: "ACCEPTED", + }, + { + id: "id3", + supplierId: "testSupplierId", + status: "DELIVERED", + }, + ]); + }); + it("maps an internal Letter to a PatchLetterResponse", () => { const date = new Date().toISOString(); const letter: Letter = { diff --git a/lambdas/api-handler/src/mappers/letter-mapper.ts b/lambdas/api-handler/src/mappers/letter-mapper.ts index c11d6d8c0..9117c8282 100644 --- a/lambdas/api-handler/src/mappers/letter-mapper.ts +++ b/lambdas/api-handler/src/mappers/letter-mapper.ts @@ -58,14 +58,24 @@ export function mapToUpdateCommand( export function mapToUpdateCommands( request: PostLettersRequest, supplierId: string, + statusesMapping?: Map, ): UpdateLetterCommand[] { - return request.data.map((letterToUpdate: PostLettersRequestResource) => ({ - id: letterToUpdate.id, - supplierId, - status: LetterStatus.parse(letterToUpdate.attributes.status), - reasonCode: letterToUpdate.attributes.reasonCode, - reasonText: letterToUpdate.attributes.reasonText, - })); + return request.data.map((letterToUpdate: PostLettersRequestResource) => { + const letterCommand = { + id: letterToUpdate.id, + supplierId, + status: LetterStatus.parse(letterToUpdate.attributes.status), + reasonCode: letterToUpdate.attributes.reasonCode, + reasonText: letterToUpdate.attributes.reasonText, + }; + if (statusesMapping) { + statusesMapping.set( + letterCommand.status, + (statusesMapping.get(letterCommand.status) || 0) + 1, + ); + } + return letterCommand; + }); } // --------------------------------------------- diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index 57b852c6f..558c2c90c 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -95,7 +95,6 @@ export async function enqueueLetterUpdateRequests( updateLetterCommands: UpdateLetterCommand[], correlationId: string, deps: Deps, - statusesMapping?: Map, ) { const BATCH_SIZE = 10; // SQS SendMessageBatch max const CONCURRENCY = 5; // number of parallel batch API calls @@ -109,21 +108,13 @@ export async function enqueueLetterUpdateRequests( await Promise.all( window.map(async (batch, batchIdx) => { - const entries = batch.map((request, idx) => { - if (statusesMapping) { - statusesMapping.set( - request.status, - (statusesMapping.get(request.status) || 0) + 1, - ); - } - return { - Id: `${i + batchIdx}-${idx}`, // unique per batch entry - MessageBody: JSON.stringify(request), - MessageAttributes: { - CorrelationId: { DataType: "String", StringValue: correlationId }, - }, - }; - }); + const entries = batch.map((request, idx) => ({ + Id: `${i + batchIdx}-${idx}`, // unique per batch entry + MessageBody: JSON.stringify(request), + MessageAttributes: { + CorrelationId: { DataType: "String", StringValue: correlationId }, + }, + })); const cmd = new SendMessageBatchCommand({ QueueUrl: deps.env.QUEUE_URL, diff --git a/lambdas/api-handler/src/utils/metrics.ts b/lambdas/api-handler/src/utils/metrics.ts index bd5eef18d..83a32c3a3 100644 --- a/lambdas/api-handler/src/utils/metrics.ts +++ b/lambdas/api-handler/src/utils/metrics.ts @@ -16,7 +16,7 @@ export function emitForSingleSupplier( metrics.putMetric(message, count, Unit.Count); } -export declare enum MetricStatus { +export enum MetricStatus { Success = "success", Failure = "failure", } From f413b15f959b616d3aec791c25da58d253302fbf Mon Sep 17 00:00:00 2001 From: vlasis-perdikidis Date: Fri, 30 Jan 2026 12:53:14 +0000 Subject: [PATCH 17/17] add extra test to check that statuses map is populated --- .../mappers/__tests__/letter-mapper.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts index 683fd7fb8..e9a0c58b9 100644 --- a/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts +++ b/lambdas/api-handler/src/mappers/__tests__/letter-mapper.test.ts @@ -52,6 +52,55 @@ describe("letter-mapper", () => { ]); }); + it("maps PostLetterRequest to UpdateLetterCommands and populates statuses map", () => { + const request: PostLettersRequest = { + data: [ + { + id: "id1", + type: "Letter", + attributes: { + status: "REJECTED", + reasonCode: "123", + reasonText: "Reason text", + }, + }, + { id: "id2", type: "Letter", attributes: { status: "ACCEPTED" } }, + { id: "id3", type: "Letter", attributes: { status: "DELIVERED" } }, + ], + }; + const supplierId = "testSupplierId"; + const statusesMap = new Map(); + const updateLetterCommands = mapToUpdateCommands( + request, + supplierId, + statusesMap, + ); + expect(updateLetterCommands).toEqual([ + { + id: "id1", + reasonCode: "123", + reasonText: "Reason text", + supplierId: "testSupplierId", + status: "REJECTED", + }, + { + id: "id2", + supplierId: "testSupplierId", + status: "ACCEPTED", + }, + { + id: "id3", + supplierId: "testSupplierId", + status: "DELIVERED", + }, + ]); + expect(Object.fromEntries(statusesMap)).toEqual({ + REJECTED: 1, + ACCEPTED: 1, + DELIVERED: 1, + }); + }); + it("maps an internal Letter to a PatchLetterResponse", () => { const date = new Date().toISOString(); const letter: Letter = {