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..32d66ee6b 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 { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics"; import { Deps } from "../config/deps"; type SupplierSpec = { supplierId: string; specId: string }; @@ -152,34 +153,76 @@ async function runUpsert( throw new Error("No matching schema for received message"); } -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 }); - } +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 successMetrics) { + metrics.putDimensions({ + Supplier: supplier, }); + metrics.putMetric("MessagesProcessed", count, Unit.Count); + } + // emit failure metrics + for (const [supplier, count] of failMetrics) { + metrics.putDimensions({ + Supplier: supplier, + }); + metrics.putMetric("MessageFailed", count, Unit.Count); + } +} - await Promise.all(tasks); +function getSupplierId(snsEvent: any): string { + if (snsEvent && snsEvent.data && snsEvent.data.supplierId) { + return snsEvent.data.supplierId; + } + return "unknown"; +} - return { batchItemFailures }; - }; +export default function createUpsertLetterHandler(deps: Deps): SQSHandler { + return metricScope((metrics) => { + return async (event: SQSEvent) => { + const batchItemFailures: SQSBatchItemFailure[] = []; + 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); + const snsEvent = JSON.parse(message); + supplier = getSupplierId(snsEvent); + const letterEvent: unknown = removeEventBridgeWrapper(snsEvent); + const type = getType(letterEvent); + + const operation = getOperationFromType(type); + + await runUpsert(operation, letterEvent, deps); + + 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}`, + ); + 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/package-lock.json b/package-lock.json index 64a58b073..221eebc49 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", @@ -6159,9 +6166,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", @@ -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", 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" },