From 761e26a84f73777cdd38a456cea39a4dd16ca21c Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Fri, 24 Apr 2026 16:53:38 +0100 Subject: [PATCH 1/9] CCM-17116: Adding metrics to eventPublisher in nodejs. To add in other lambdas and in python --- .../terraform/components/dl/locals.tf | 1 + .../dl/module_lambda_core_notifier.tf | 1 + .../dl/module_lambda_file_scanner.tf | 1 + .../dl/module_lambda_mesh_acknowledge.tf | 1 + .../dl/module_lambda_mesh_download.tf | 1 + .../components/dl/module_lambda_mesh_poll.tf | 1 + .../dl/module_lambda_move_scanned_files.tf | 1 + .../dl/module_lambda_nhsapp_status_handler.tf | 1 + .../components/dl/module_lambda_pdm_poll.tf | 1 + .../dl/module_lambda_pdm_uploader.tf | 1 + .../dl/module_lambda_print_analyser.tf | 1 + .../dl/module_lambda_print_sender.tf | 1 + .../dl/module_lambda_print_status_handler.tf | 1 + .../module_lambda_report_event_transformer.tf | 3 + .../dl/module_lambda_report_generator.tf | 1 + .../dl/module_lambda_report_scheduler.tf | 1 + .../dl/module_lambda_report_sender.tf | 1 + .../components/dl/module_lambda_ttl_create.tf | 1 + .../dl/module_lambda_ttl_handle_expiry.tf | 1 + .../components/dl/module_lambda_ttl_poll.tf | 9 +- .../src/__tests__/infra/config.test.ts | 11 +- lambdas/core-notifier-lambda/src/container.ts | 8 +- .../core-notifier-lambda/src/infra/config.ts | 2 + .../src/__tests__/infra/config.test.ts | 19 +- .../file-scanner-lambda/src/infra/config.ts | 7 + package-lock.json | 307 ++++++++++++------ utils/utils/package.json | 1 + .../cloudwatch/metric-handler.test.ts | 169 ++++++++++ .../event-publisher/event-publisher.test.ts | 12 + utils/utils/src/cloudwatch/metric-handler.ts | 101 ++++++ .../src/event-publisher/event-publisher.ts | 26 +- 31 files changed, 575 insertions(+), 118 deletions(-) create mode 100644 utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts create mode 100644 utils/utils/src/cloudwatch/metric-handler.ts diff --git a/infrastructure/terraform/components/dl/locals.tf b/infrastructure/terraform/components/dl/locals.tf index 7a1ef5b92..bd679ec18 100644 --- a/infrastructure/terraform/components/dl/locals.tf +++ b/infrastructure/terraform/components/dl/locals.tf @@ -16,4 +16,5 @@ locals { ssm_senders_prefix = "${local.ssm_prefix}/senders" ttl_shard_count = 3 unscanned_files_bucket = local.acct.additional_s3_buckets["digital-letters_unscanned-files"]["id"] + metrics_namespace_name = "nhs-${var.environment}-${var.component}" } diff --git a/infrastructure/terraform/components/dl/module_lambda_core_notifier.tf b/infrastructure/terraform/components/dl/module_lambda_core_notifier.tf index 4e0d8c59e..24284fede 100644 --- a/infrastructure/terraform/components/dl/module_lambda_core_notifier.tf +++ b/infrastructure/terraform/components/dl/module_lambda_core_notifier.tf @@ -41,6 +41,7 @@ module "core_notifier" { "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url "ENVIRONMENT" = var.environment "NHS_APP_BASE_URL" = var.nhs_app_base_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf b/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf index bca3ff929..880472772 100644 --- a/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf +++ b/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf @@ -40,6 +40,7 @@ module "file_scanner" { "UNSCANNED_FILES_PATH_PREFIX" = local.csi "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_mesh_acknowledge.tf b/infrastructure/terraform/components/dl/module_lambda_mesh_acknowledge.tf index acbb5890c..38b2b6448 100644 --- a/infrastructure/terraform/components/dl/module_lambda_mesh_acknowledge.tf +++ b/infrastructure/terraform/components/dl/module_lambda_mesh_acknowledge.tf @@ -43,6 +43,7 @@ module "mesh_acknowledge" { SSM_MESH_PREFIX = local.ssm_mesh_prefix SSM_SENDERS_PREFIX = local.ssm_senders_prefix USE_MESH_MOCK = var.enable_mock_mesh ? "true" : "false" + DL_METRICS_NAMESPACE = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_mesh_download.tf b/infrastructure/terraform/components/dl/module_lambda_mesh_download.tf index ee77f050d..3b39e0113 100644 --- a/infrastructure/terraform/components/dl/module_lambda_mesh_download.tf +++ b/infrastructure/terraform/components/dl/module_lambda_mesh_download.tf @@ -48,6 +48,7 @@ module "mesh_download" { SSM_MESH_PREFIX = local.ssm_mesh_prefix SSM_SENDERS_PREFIX = local.ssm_senders_prefix USE_MESH_MOCK = var.enable_mock_mesh ? "true" : "false" + DL_METRICS_NAMESPACE = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_mesh_poll.tf b/infrastructure/terraform/components/dl/module_lambda_mesh_poll.tf index 806fd3a9a..219e91d24 100644 --- a/infrastructure/terraform/components/dl/module_lambda_mesh_poll.tf +++ b/infrastructure/terraform/components/dl/module_lambda_mesh_poll.tf @@ -48,6 +48,7 @@ module "mesh_poll" { SSM_MESH_PREFIX = local.ssm_mesh_prefix SSM_SENDERS_PREFIX = local.ssm_senders_prefix USE_MESH_MOCK = var.enable_mock_mesh ? "true" : "false" + DL_METRICS_NAMESPACE = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_move_scanned_files.tf b/infrastructure/terraform/components/dl/module_lambda_move_scanned_files.tf index a26f4912f..7484fe44e 100644 --- a/infrastructure/terraform/components/dl/module_lambda_move_scanned_files.tf +++ b/infrastructure/terraform/components/dl/module_lambda_move_scanned_files.tf @@ -42,6 +42,7 @@ module "move_scanned_files" { "UNSCANNED_FILE_S3_BUCKET_NAME" = local.unscanned_files_bucket "SAFE_FILE_S3_BUCKET_NAME" = module.s3bucket_file_safe.bucket "QUARANTINE_FILE_S3_BUCKET_NAME" = module.s3bucket_file_quarantine.bucket + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf b/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf index a4e1d6b26..27176417b 100644 --- a/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf +++ b/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf @@ -38,6 +38,7 @@ module "nhsapp_status_handler" { "TTL_TABLE_NAME" = aws_dynamodb_table.ttl.name "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf b/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf index 5d1126c01..b5458a0ba 100644 --- a/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf +++ b/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf @@ -40,6 +40,7 @@ module "pdm_poll" { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url "POLL_MAX_RETRIES" = 10 + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf b/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf index f76984ffc..8edaea1a9 100644 --- a/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf +++ b/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf @@ -39,6 +39,7 @@ module "pdm_uploader" { "APIM_ACCESS_TOKEN_SSM_PARAMETER_NAME" = local.pdm_access_token_ssm_parameter_name "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf b/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf index 1c97647dc..7880b5790 100644 --- a/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf +++ b/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf @@ -37,6 +37,7 @@ module "print_analyser" { lambda_env_vars = { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_print_sender.tf b/infrastructure/terraform/components/dl/module_lambda_print_sender.tf index a7c191c38..6f85b0604 100644 --- a/infrastructure/terraform/components/dl/module_lambda_print_sender.tf +++ b/infrastructure/terraform/components/dl/module_lambda_print_sender.tf @@ -39,6 +39,7 @@ module "print_sender" { "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url "ENVIRONMENT" = var.environment "ACCOUNT_TYPE" = var.aws_account_type + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf b/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf index 29c1a42af..8c18f640b 100644 --- a/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf +++ b/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf @@ -37,6 +37,7 @@ module "print_status_handler" { lambda_env_vars = { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_report_event_transformer.tf b/infrastructure/terraform/components/dl/module_lambda_report_event_transformer.tf index 83fda076e..fdb17fa36 100644 --- a/infrastructure/terraform/components/dl/module_lambda_report_event_transformer.tf +++ b/infrastructure/terraform/components/dl/module_lambda_report_event_transformer.tf @@ -29,4 +29,7 @@ module "report_event_transformer" { log_destination_arn = local.log_destination_arn log_subscription_role_arn = local.acct.log_subscription_role_arn + lambda_env_vars = { + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name + } } diff --git a/infrastructure/terraform/components/dl/module_lambda_report_generator.tf b/infrastructure/terraform/components/dl/module_lambda_report_generator.tf index ca82ecdbe..05ca57b03 100644 --- a/infrastructure/terraform/components/dl/module_lambda_report_generator.tf +++ b/infrastructure/terraform/components/dl/module_lambda_report_generator.tf @@ -42,6 +42,7 @@ module "report_generator" { "REPORTING_BUCKET" = module.s3bucket_reporting.bucket "REPORT_NAME" = "completed_communications" "WAIT_FOR_IN_SECONDS" = var.athena_query_polling_time_seconds + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_report_scheduler.tf b/infrastructure/terraform/components/dl/module_lambda_report_scheduler.tf index 047f8d244..67de1c80d 100644 --- a/infrastructure/terraform/components/dl/module_lambda_report_scheduler.tf +++ b/infrastructure/terraform/components/dl/module_lambda_report_scheduler.tf @@ -39,6 +39,7 @@ module "report_scheduler" { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url "ENVIRONMENT" = var.environment + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_report_sender.tf b/infrastructure/terraform/components/dl/module_lambda_report_sender.tf index cc8ec956c..35a052e55 100644 --- a/infrastructure/terraform/components/dl/module_lambda_report_sender.tf +++ b/infrastructure/terraform/components/dl/module_lambda_report_sender.tf @@ -45,6 +45,7 @@ module "report_sender" { SSM_MESH_PREFIX = local.ssm_mesh_prefix SSM_SENDERS_PREFIX = local.ssm_senders_prefix USE_MESH_MOCK = var.enable_mock_mesh ? "true" : "false" + DL_METRICS_NAMESPACE = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_ttl_create.tf b/infrastructure/terraform/components/dl/module_lambda_ttl_create.tf index 2f78f41f9..d0eac037e 100644 --- a/infrastructure/terraform/components/dl/module_lambda_ttl_create.tf +++ b/infrastructure/terraform/components/dl/module_lambda_ttl_create.tf @@ -40,6 +40,7 @@ module "ttl_create" { "TTL_SHARD_COUNT" = local.ttl_shard_count "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf b/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf index ac51eae9d..746b663f9 100644 --- a/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf +++ b/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf @@ -38,6 +38,7 @@ module "ttl_handle_expiry" { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url "DLQ_URL" = module.sqs_ttl_handle_expiry_errors.sqs_queue_url + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf b/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf index 4ca436578..95b9da97f 100644 --- a/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf +++ b/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf @@ -36,10 +36,11 @@ module "ttl_poll" { log_subscription_role_arn = local.acct.log_subscription_role_arn lambda_env_vars = { - "TTL_TABLE_NAME" = aws_dynamodb_table.ttl.name - "CONCURRENCY" = 60 - "MAX_PROCESS_SECONDS" = 300 - "TTL_SHARD_COUNT" = local.ttl_shard_count + "TTL_TABLE_NAME" = aws_dynamodb_table.ttl.name + "CONCURRENCY" = 60 + "MAX_PROCESS_SECONDS" = 300 + "TTL_SHARD_COUNT" = local.ttl_shard_count + "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/lambdas/core-notifier-lambda/src/__tests__/infra/config.test.ts b/lambdas/core-notifier-lambda/src/__tests__/infra/config.test.ts index 7e69debc8..897c7b0c9 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/infra/config.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/infra/config.test.ts @@ -24,6 +24,7 @@ describe('loadConfig', () => { apimBaseUrl: 'https://api.test.nhs.uk', nhsAppBaseUrl: 'https://example.com', environment: 'test', + dlMetricsNamespace: 'test-namespace', }; mockGetValue @@ -32,12 +33,13 @@ describe('loadConfig', () => { .mockReturnValueOnce(mockConfig.apimAccessTokenSsmParameterName) .mockReturnValueOnce(mockConfig.apimBaseUrl) .mockReturnValueOnce(mockConfig.nhsAppBaseUrl) - .mockReturnValueOnce(mockConfig.environment); + .mockReturnValueOnce(mockConfig.environment) + .mockReturnValueOnce(mockConfig.dlMetricsNamespace); const result = loadConfig(); expect(result).toEqual(mockConfig); - expect(mockGetValue).toHaveBeenCalledTimes(6); + expect(mockGetValue).toHaveBeenCalledTimes(7); expect(mockGetValue).toHaveBeenNthCalledWith( 1, 'EVENT_PUBLISHER_EVENT_BUS_ARN', @@ -50,6 +52,7 @@ describe('loadConfig', () => { expect(mockGetValue).toHaveBeenNthCalledWith(4, 'APIM_BASE_URL'); expect(mockGetValue).toHaveBeenNthCalledWith(5, 'NHS_APP_BASE_URL'); expect(mockGetValue).toHaveBeenNthCalledWith(6, 'ENVIRONMENT'); + expect(mockGetValue).toHaveBeenNthCalledWith(7, 'DL_METRICS_NAMESPACE'); }); it('returns config with correct types', () => { @@ -59,7 +62,8 @@ describe('loadConfig', () => { .mockReturnValueOnce('/param') .mockReturnValueOnce('https://api') .mockReturnValueOnce('https://example.com') - .mockReturnValueOnce('prod'); + .mockReturnValueOnce('prod') + .mockReturnValueOnce('test-namespace'); const result: NotifySendMessageConfig = loadConfig(); @@ -69,5 +73,6 @@ describe('loadConfig', () => { expect(typeof result.apimBaseUrl).toBe('string'); expect(typeof result.nhsAppBaseUrl).toBe('string'); expect(typeof result.environment).toBe('string'); + expect(typeof result.dlMetricsNamespace).toBe('string'); }); }); diff --git a/lambdas/core-notifier-lambda/src/container.ts b/lambdas/core-notifier-lambda/src/container.ts index 378ebe5d9..3887ff6c5 100644 --- a/lambdas/core-notifier-lambda/src/container.ts +++ b/lambdas/core-notifier-lambda/src/container.ts @@ -14,6 +14,7 @@ import { SenderManagement } from 'sender-management'; import { CoreRequestMapper } from 'domain/core-request-mapper'; import { MessageRequestSubmittedMapper } from 'domain/message-request-submitted-mapper'; import { MessageRequestRejectedMapper } from 'domain/message-request-rejected-mapper'; +import { MetricHandler } from '../../../utils/utils/src/cloudwatch/metric-handler'; export async function createContainer(): Promise { const parameterStore = new ParameterStoreCache(); @@ -41,7 +42,11 @@ export async function createContainer(): Promise { logger, }); - const { eventPublisherDlqUrl, eventPublisherEventBusArn } = config; + const { eventPublisherDlqUrl, eventPublisherEventBusArn, dlMetricsNamespace } = config; + const metricHandler = new MetricHandler(dlMetricsNamespace, [{ + Name: 'Environment', + Value: 'environment', + }]); const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, @@ -49,6 +54,7 @@ export async function createContainer(): Promise { logger, sqsClient, eventBridgeClient, + metricHandler, }); const coreRequestMapper = new CoreRequestMapper(config.nhsAppBaseUrl); diff --git a/lambdas/core-notifier-lambda/src/infra/config.ts b/lambdas/core-notifier-lambda/src/infra/config.ts index 85f9afd77..4be2c001f 100644 --- a/lambdas/core-notifier-lambda/src/infra/config.ts +++ b/lambdas/core-notifier-lambda/src/infra/config.ts @@ -7,6 +7,7 @@ export type NotifySendMessageConfig = { apimBaseUrl: string; nhsAppBaseUrl: string; environment: string; + dlMetricsNamespace: string; }; export function loadConfig(): NotifySendMessageConfig { @@ -23,5 +24,6 @@ export function loadConfig(): NotifySendMessageConfig { apimBaseUrl: defaultConfigReader.getValue('APIM_BASE_URL'), nhsAppBaseUrl: defaultConfigReader.getValue('NHS_APP_BASE_URL'), environment: defaultConfigReader.getValue('ENVIRONMENT'), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts b/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts index 825baa305..01462e7eb 100644 --- a/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts +++ b/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts @@ -20,6 +20,7 @@ describe('loadConfig', () => { 'arn:aws:events:us-east-1:123456789012:event-bus/test-bus'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq'; + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; const config = loadConfig(); @@ -31,6 +32,7 @@ describe('loadConfig', () => { 'arn:aws:events:us-east-1:123456789012:event-bus/test-bus', eventPublisherDlqUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq', + dlMetricsNamespace: 'test-namespace', }); }); @@ -39,7 +41,7 @@ describe('loadConfig', () => { process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; - + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; expect(() => loadConfig()).toThrow('DOCUMENT_REFERENCE_BUCKET is not set'); }); @@ -48,6 +50,7 @@ describe('loadConfig', () => { process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; expect(() => loadConfig()).toThrow('UNSCANNED_FILES_BUCKET is not set'); }); @@ -57,6 +60,7 @@ describe('loadConfig', () => { process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; expect(() => loadConfig()).toThrow( 'UNSCANNED_FILES_PATH_PREFIX is not set', @@ -68,6 +72,7 @@ describe('loadConfig', () => { process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; expect(() => loadConfig()).toThrow( 'EVENT_PUBLISHER_EVENT_BUS_ARN is not set', @@ -79,6 +84,7 @@ describe('loadConfig', () => { process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; expect(() => loadConfig()).toThrow('EVENT_PUBLISHER_DLQ_URL is not set'); }); @@ -89,7 +95,18 @@ describe('loadConfig', () => { process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; expect(() => loadConfig()).toThrow('DOCUMENT_REFERENCE_BUCKET is not set'); }); + + it('should throw error when DL_METRICS_NAMESPACE is missing', () => { + process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; + process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; + process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; + process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; + + expect(() => loadConfig()).toThrow('DL_METRICS_NAMESPACE is not set'); + }); }); diff --git a/lambdas/file-scanner-lambda/src/infra/config.ts b/lambdas/file-scanner-lambda/src/infra/config.ts index 802931add..cfe61aef6 100644 --- a/lambdas/file-scanner-lambda/src/infra/config.ts +++ b/lambdas/file-scanner-lambda/src/infra/config.ts @@ -6,6 +6,7 @@ export interface Config { unscannedFilesPathPrefix: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; } export function loadConfig(): Config { @@ -14,6 +15,7 @@ export function loadConfig(): Config { const unscannedFilesPathPrefix = process.env.UNSCANNED_FILES_PATH_PREFIX; const eventPublisherEventBusArn = process.env.EVENT_PUBLISHER_EVENT_BUS_ARN; const eventPublisherDlqUrl = process.env.EVENT_PUBLISHER_DLQ_URL; + const dlMetricsNamespace = process.env.DL_METRICS_NAMESPACE; if (!documentReferenceBucket) { throw new Error('DOCUMENT_REFERENCE_BUCKET is not set'); @@ -34,12 +36,16 @@ export function loadConfig(): Config { if (!eventPublisherDlqUrl) { throw new Error('EVENT_PUBLISHER_DLQ_URL is not set'); } + if (!dlMetricsNamespace) { + throw new Error('DL_METRICS_NAMESPACE is not set'); + } logger.info({ description: 'Configuration loaded', documentReferenceBucket, unscannedFilesBucket, unscannedFilesPathPrefix, + dlMetricsNamespace, }); return { @@ -48,5 +54,6 @@ export function loadConfig(): Config { unscannedFilesPathPrefix, eventPublisherEventBusArn, eventPublisherDlqUrl, + dlMetricsNamespace, }; } diff --git a/package-lock.json b/package-lock.json index 64dd9bd22..5d42e7379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5765,6 +5765,58 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-cloudwatch": { + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.984.0.tgz", + "integrity": "sha512-qkf1lcKBsxsacE7R4tNXtIsnOCBKYq+R+ONjo3GX/MowZv4J9rYUBUq3G6Hw3sHQYnpPoJXBfBj6dCY6wYUAnw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/credential-provider-node": "^3.972.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.984.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-compression": "^4.3.27", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@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.11.1", + "@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.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@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-logs": { "version": "3.999.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.999.0.tgz", @@ -5834,6 +5886,22 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@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-dynamodb": { "version": "3.981.0", "license": "Apache-2.0", @@ -10562,20 +10630,20 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.6", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.6.tgz", - "integrity": "sha512-4xE+0L2NrsFKpEVFlFELkIHQddBvMbQ41LRIP74dGCXnY1zQ9DgksrBcRBDJT+iOzGy4VEJIeU3hkUK5mn06kg==", + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.11", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-body-length-browser": "^4.2.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-stream": "^4.5.15", - "@smithy/util-utf8": "^4.2.1", - "@smithy/uuid": "^1.1.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -10669,15 +10737,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.11.tgz", - "integrity": "sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.10", - "@smithy/querystring-builder": "^4.2.10", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -10742,9 +10810,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.1.tgz", - "integrity": "sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10767,6 +10835,27 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/middleware-compression": { + "version": "4.3.46", + "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.3.46.tgz", + "integrity": "sha512-9f4AZ5dKqKRmO49MPhOoxFoQBLfBgxE9YKG8bQ6lsW9xk+Bn8rkfGlpW8OYlvhuarN+8mja9PjhEudFiR8wGFQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-utf8": "^4.2.2", + "fflate": "0.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.10.tgz", @@ -10848,14 +10937,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.10.tgz", - "integrity": "sha512-UALRbJtVX34AdP2VECKVlnNgidLHA2A7YgcJzwSBg1hzmnO/bZBHl/LDQQyYifzUwp1UOODnl9JJ3KNawpUJ9w==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10863,15 +10952,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.12.tgz", - "integrity": "sha512-zo1+WKJkR9x7ZtMeMDAAsq2PufwiLDmkhcjpWPRRkmeIuOm6nq1qjFICSZbnjBvD09ei8KMo26BWxsu2BUU+5w==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/querystring-builder": "^4.2.10", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10879,12 +10967,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.10.tgz", - "integrity": "sha512-5jm60P0CU7tom0eNrZ7YrkgBaoLFXzmqB0wVS+4uK8PPGmosSrLNf6rRd50UBvukztawZ7zyA8TxlrKpF5z9jw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10892,12 +10980,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.10.tgz", - "integrity": "sha512-2NzVWpYY0tRdfeCJLsgrR89KE3NTWT2wGulhNUxYlRmtRmPwLQwKzhrfVaiNlA9ZpJvbW7cjTVChYKgnkqXj1A==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10905,13 +10993,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.10.tgz", - "integrity": "sha512-HeN7kEvuzO2DmAzLukE9UryiUvejD3tMp9a1D1NJETerIfKobBUCLfviP6QEk500166eD2IATaXM59qgUI+YDA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", - "@smithy/util-uri-escape": "^4.2.1", + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -10919,12 +11007,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.10.tgz", - "integrity": "sha512-4Mh18J26+ao1oX5wXJfWlTT+Q1OpDR8ssiC9PDOuEgVBGloqg18Fw7h5Ct8DyT9NBYwJgtJ2nLjKKFU6RP1G1Q==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10944,12 +11032,12 @@ } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.5.tgz", - "integrity": "sha512-pHgASxl50rrtOztgQCPmOXFjRW+mCd7ALr/3uXNzRrRoGV5G2+78GOsQ3HlQuBVHCh9o6xqMNvlIKZjWn4Euug==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10994,9 +11082,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11006,13 +11094,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.10.tgz", - "integrity": "sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.10", - "@smithy/types": "^4.13.0", + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11020,13 +11108,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.1.tgz", - "integrity": "sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.1", - "@smithy/util-utf8": "^4.2.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -11034,9 +11122,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.1.tgz", - "integrity": "sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11058,12 +11146,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.1.tgz", - "integrity": "sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.1", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -11071,9 +11159,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.1.tgz", - "integrity": "sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11130,9 +11218,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.1.tgz", - "integrity": "sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11142,12 +11230,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.10.tgz", - "integrity": "sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11169,18 +11257,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.15", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.15.tgz", - "integrity": "sha512-OlOKnaqnkU9X+6wEkd7mN+WB7orPbCVDauXOj22Q7VtiTkvy7ZdSsOg4QiNAZMgI4OkvNf+/VLUC3VXkxuWJZw==", + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.11", - "@smithy/node-http-handler": "^4.4.12", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-buffer-from": "^4.2.1", - "@smithy/util-hex-encoding": "^4.2.1", - "@smithy/util-utf8": "^4.2.1", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -11188,9 +11276,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.1.tgz", - "integrity": "sha512-YmiUDn2eo2IOiWYYvGQkgX5ZkBSiTQu4FlDo5jNPpAxng2t6Sjb6WutnZV9l6VR4eJul1ABmCrnWBC9hKHQa6Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11200,12 +11288,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.1.tgz", - "integrity": "sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.1", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -11227,9 +11315,9 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.1.tgz", - "integrity": "sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -16308,6 +16396,12 @@ "version": "4.2.3", "license": "MIT" }, + "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/file-entry-cache": { "version": "8.0.0", "dev": true, @@ -27906,6 +28000,7 @@ "version": "0.0.1", "dependencies": { "@aws-sdk/client-athena": "^3.984.0", + "@aws-sdk/client-cloudwatch": "^3.984.0", "@aws-sdk/client-dynamodb": "^3.984.0", "@aws-sdk/client-eventbridge": "^3.984.0", "@aws-sdk/client-lambda": "^3.984.0", diff --git a/utils/utils/package.json b/utils/utils/package.json index b9414a801..378c2412e 100644 --- a/utils/utils/package.json +++ b/utils/utils/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@aws-sdk/client-athena": "^3.984.0", + "@aws-sdk/client-cloudwatch": "^3.984.0", "@aws-sdk/client-dynamodb": "^3.984.0", "@aws-sdk/client-eventbridge": "^3.984.0", "@aws-sdk/client-lambda": "^3.984.0", diff --git a/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts b/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts new file mode 100644 index 000000000..30bd94a2b --- /dev/null +++ b/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts @@ -0,0 +1,169 @@ +import { MetricHandler } from '../../cloudwatch/metric-handler'; + +const logMock = jest.spyOn(global.console, 'log').mockImplementation(); + +const dimensions = [ + { + Name: 'Environment', + Value: 'internal-dev', + }, +]; + +let metricHandler = new MetricHandler('namespace', dimensions); + +beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2022-01-01')); +}); + +beforeEach(() => { + metricHandler = new MetricHandler('namespace', dimensions); +}); + +afterEach(() => { + logMock.mockClear(); +}); + +afterAll(() => { + jest.useRealTimers(); + logMock.mockRestore(); +}); + +it('puts metric data without timestamp', () => { + metricHandler.addMetrics(['metric', 'Count', 47]); + + expect(logMock).toHaveBeenCalledTimes(1); + + const lastCalledWith = logMock.mock.calls[0][0]; + + expect(JSON.parse(lastCalledWith)).toEqual({ + _aws: { + Timestamp: new Date('2022-01-01').valueOf(), + CloudWatchMetrics: [ + { + Namespace: 'namespace', + Dimensions: [['Environment']], + Metrics: [ + { + Name: 'metric', + Unit: 'Count', + StorageResolution: 60, + }, + ], + }, + ], + }, + metric: 47, + Environment: 'internal-dev', + }); +}); + +it('logs multiple metrics', () => { + metricHandler.addMetrics([ + ['metric1', 'Count', 47], + ['metric2', 'Count', 50], + ]); + + expect(logMock).toHaveBeenCalledTimes(1); + + const calledWith = logMock.mock.calls[0][0]; + + expect(JSON.parse(calledWith)).toEqual({ + _aws: { + Timestamp: new Date('2022-01-01').valueOf(), + CloudWatchMetrics: [ + { + Namespace: 'namespace', + Dimensions: [['Environment']], + Metrics: [ + { + Name: 'metric1', + Unit: 'Count', + StorageResolution: 60, + }, + { + Name: 'metric2', + Unit: 'Count', + StorageResolution: 60, + }, + ], + }, + ], + }, + metric1: 47, + metric2: 50, + Environment: 'internal-dev', + }); +}); + +it('puts metric data with timestamp', () => { + metricHandler.addMetrics(['metric', 'Count', 47], { + timestamp: new Date('2022-01-02'), + }); + + expect(logMock).toHaveBeenCalledTimes(1); + + const lastCalledWith = logMock.mock.calls[0][0]; + + expect(JSON.parse(lastCalledWith)).toEqual({ + _aws: { + Timestamp: new Date('2022-01-02').valueOf(), + CloudWatchMetrics: [ + { + Namespace: 'namespace', + Dimensions: [['Environment']], + Metrics: [ + { + Name: 'metric', + Unit: 'Count', + StorageResolution: 60, + }, + ], + }, + ], + }, + metric: 47, + Environment: 'internal-dev', + }); +}); + +it('generates child metric handler', () => { + const childMetricHandler = metricHandler.getChildMetricHandler([ + { + Name: 'Client ID', + Value: 'vaccs', + }, + ]); + + childMetricHandler.addMetrics(['metric', 'Count', 47], { + timestamp: new Date('2022-01-02'), + extraDimensions: [{ Name: 'Request ID', Value: '123' }], + }); + + expect(logMock).toHaveBeenCalledTimes(1); + + const lastCalledWith = logMock.mock.calls[0][0]; + + expect(JSON.parse(lastCalledWith)).toEqual({ + _aws: { + Timestamp: new Date('2022-01-02').valueOf(), + CloudWatchMetrics: [ + { + Namespace: 'namespace', + Dimensions: [['Environment', 'Client ID', 'Request ID']], + Metrics: [ + { + Name: 'metric', + Unit: 'Count', + StorageResolution: 60, + }, + ], + }, + ], + }, + metric: 47, + Environment: 'internal-dev', + 'Client ID': 'vaccs', + 'Request ID': '123', + }); +}); diff --git a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts index 83bd09538..1a7a7c48b 100644 --- a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts +++ b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts @@ -8,9 +8,14 @@ import { randomInt, randomUUID } from 'node:crypto'; import { mockClient } from 'aws-sdk-client-mock'; import { Logger } from 'logger'; import { EventPublisher, EventPublisherDependencies } from 'event-publisher'; +import { MetricHandler } from 'cloudwatch/metric-handler'; const eventBridgeMock = mockClient(EventBridgeClient); const sqsMock = mockClient(SQSClient); +const metricHandlerMock: MetricHandler = { + addMetrics: jest.fn(), + getChildMetricHandler: jest.fn(), +} as unknown as MetricHandler; const mockLogger: Logger = { info: jest.fn(), @@ -27,6 +32,7 @@ const testConfig: EventPublisherDependencies = { logger: mockLogger, sqsClient: sqsMock as unknown as SQSClient, eventBridgeClient: eventBridgeMock as unknown as EventBridgeClient, + metricHandler: metricHandlerMock, }; const event: TestEvent = { @@ -57,6 +63,7 @@ describe('Event Publishing', () => { expect(result).toEqual([]); expect(eventBridgeMock.calls()).toHaveLength(0); expect(sqsMock.calls()).toHaveLength(0); + expect(metricHandlerMock.addMetrics).not.toHaveBeenCalled(); }); test('should send valid events to EventBridge', async () => { @@ -71,6 +78,7 @@ describe('Event Publishing', () => { expect(result).toEqual([]); expect(eventBridgeMock.calls()).toHaveLength(1); expect(sqsMock.calls()).toHaveLength(0); + expect(metricHandlerMock.addMetrics).toHaveBeenCalledWith(['uk.nhs.notify.digital.letters.sent.v1_batchSuccess', 'Count', 2]); const eventBridgeCall = eventBridgeMock.calls()[0]; expect(eventBridgeCall.args[0].input).toEqual({ @@ -152,6 +160,8 @@ describe('Event Publishing', () => { const sqsInput = sqsCall.args[0].input as any; expect(sqsInput.Entries).toHaveLength(1); expect(sqsInput.Entries[0].MessageBody).toBe(JSON.stringify(event)); + expect(metricHandlerMock.addMetrics).toHaveBeenNthCalledWith(1,['uk.nhs.notify.digital.letters.sent.v1_batchSuccess', 'Count', 1]); + expect(metricHandlerMock.addMetrics).toHaveBeenNthCalledWith(2,['uk.nhs.notify.digital.letters.sent.v1_batchFailure', 'Count', 1]); }); test('should handle EventBridge send error and send all events to DLQ', async () => { @@ -453,6 +463,8 @@ describe('Event Publishing', () => { ), ), ); + + expect(metricHandlerMock.addMetrics).toHaveBeenCalled(); }); }); diff --git a/utils/utils/src/cloudwatch/metric-handler.ts b/utils/utils/src/cloudwatch/metric-handler.ts new file mode 100644 index 000000000..9fa36cd00 --- /dev/null +++ b/utils/utils/src/cloudwatch/metric-handler.ts @@ -0,0 +1,101 @@ +import type { Dimension } from '@aws-sdk/client-cloudwatch'; + +export type { Dimension as MetricDimension } from '@aws-sdk/client-cloudwatch'; + +export type MetricUnit = + | 'Seconds' + | 'Microseconds' + | 'Milliseconds' + | 'Bytes' + | 'Kilobytes' + | 'Megabytes' + | 'Gigabytes' + | 'Terabytes' + | 'Bits' + | 'Kilobits' + | 'Megabits' + | 'Gigabits' + | 'Terabits' + | 'Percent' + | 'Count' + | 'Bytes/Second' + | 'Kilobytes/Second' + | 'Megabytes/Second' + | 'Gigabytes/Second' + | 'Terabytes/Second' + | 'Bits/Second' + | 'Kilobits/Second' + | 'Megabits/Second' + | 'Gigabits/Second' + | 'Terabits/Second' + | 'Count/Second' + | 'None'; + +type Metric = [name: string, unit: MetricUnit, value: number]; + +export class MetricHandler { + // Used in add metric calls so that all dimensions can be present in a namespace to simplify aggregation + public static readonly DIMENSION_NOT_APPLICABLE = 'not_applicable'; + + constructor( + private readonly namespace: string, + private readonly dimensions: Dimension[] + ) {} + + public addMetrics( + metricOrMetrics: Metric | Metric[], + options: { + timestamp?: Date; + extraDimensions?: Dimension[]; + storageResolution?: number; + } = {} + ) { + const { + timestamp = new Date(), + extraDimensions = [], + storageResolution = 60, + } = options; + + const metrics = ( + Array.isArray(metricOrMetrics) && Array.isArray(metricOrMetrics[0]) + ? metricOrMetrics + : [metricOrMetrics] + ) as Metric[]; + + const dimensions: Record = {}; + + for (const dimension of [...this.dimensions, ...extraDimensions]) { + dimensions[dimension.Name as string] = dimension.Value as string; + } + + const metric = { + _aws: { + Timestamp: timestamp.valueOf(), + CloudWatchMetrics: [ + { + Namespace: this.namespace, + Dimensions: [Object.keys(dimensions)], + Metrics: metrics.map(([name, unit]) => ({ + Name: name, + Unit: unit, + StorageResolution: storageResolution, + })), + }, + ], + }, + ...dimensions, + ...Object.fromEntries(metrics.map(([name, , value]) => [name, value])), + }; + // eslint-disable-next-line no-console + console.log(JSON.stringify(metric)); + } + + public getChildMetricHandler( + childMetricHandlerDimensions: Dimension[] + ): MetricHandler { + return new MetricHandler(this.namespace, [ + ...this.dimensions, + ...childMetricHandlerDimensions, + ]); + } +} diff --git a/utils/utils/src/event-publisher/event-publisher.ts b/utils/utils/src/event-publisher/event-publisher.ts index e4fdc20c9..16802e224 100644 --- a/utils/utils/src/event-publisher/event-publisher.ts +++ b/utils/utils/src/event-publisher/event-publisher.ts @@ -5,6 +5,7 @@ import { import { SQSClient, SendMessageBatchCommand } from '@aws-sdk/client-sqs'; import { randomUUID } from 'node:crypto'; import { Logger } from '../logger'; +import { MetricHandler } from '../cloudwatch/metric-handler'; type DlqReason = 'INVALID_EVENT' | 'EVENTBRIDGE_FAILURE'; @@ -16,6 +17,7 @@ export interface EventPublisherDependencies { logger: Logger; sqsClient: SQSClient; eventBridgeClient: EventBridgeClient; + metricHandler: MetricHandler; } type PublishableEvent = { id: string; source: string; type: string }; @@ -31,6 +33,8 @@ export class EventPublisher { private readonly logger: Logger; + private readonly metricHandler: MetricHandler; + constructor(config: EventPublisherDependencies) { if (!config.eventBusArn) { throw new Error('eventBusArn has not been specified'); @@ -47,11 +51,16 @@ export class EventPublisher { if (!config.eventBridgeClient) { throw new Error('eventBridgeClient has not been provided'); } + if (!config.metricHandler) { + throw new Error('metricHandler has not been provided'); + } this.config = config; this.logger = config.logger; this.eventBridge = config.eventBridgeClient; this.sqs = config.sqsClient; + this.metricHandler = config.metricHandler; + } private async sendToEventBridge( @@ -82,14 +91,25 @@ export class EventPublisher { new PutEventsCommand({ Entries: entries }), ); + const successfulCount = batch.length - (response.FailedEntryCount || 0); + const failedEntryCount = response.FailedEntryCount || 0; this.logger.info({ description: 'EventBridge batch sent', batchSize: batch.length, - failedEntryCount: response.FailedEntryCount || 0, - successfulCount: batch.length - (response.FailedEntryCount || 0), + failedEntryCount: failedEntryCount, + successfulCount: successfulCount, }); - if (response.FailedEntryCount && response.Entries) { + if (successfulCount > 0) { + this.metricHandler.addMetrics([`${entries[0].DetailType}_batchSuccess`, 'Count', successfulCount]); + } + + if (failedEntryCount > 0) { + this.metricHandler.addMetrics([`${entries[0].DetailType}_batchFailure`, 'Count', failedEntryCount]); + } + + + if (failedEntryCount && response.Entries) { for (const [idx, entry] of response.Entries.entries()) { if (entry.ErrorCode) { this.logger.warn({ From 812b2bfe928f0dee413735122bea917583e165bb Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Mon, 27 Apr 2026 16:29:48 +0100 Subject: [PATCH 2/9] CCM-17116: Updating dependency for the events publisher in the nodejs lambdas --- .../dl/module_lambda_file_scanner.tf | 1 + .../dl/module_lambda_nhsapp_status_handler.tf | 1 + .../components/dl/module_lambda_pdm_poll.tf | 1 + .../dl/module_lambda_pdm_uploader.tf | 1 + .../dl/module_lambda_print_analyser.tf | 1 + .../dl/module_lambda_print_status_handler.tf | 1 + .../dl/module_lambda_report_generator.tf | 1 + .../dl/module_lambda_ttl_handle_expiry.tf | 1 + .../components/dl/module_lambda_ttl_poll.tf | 1 + .../src/__tests__/container.test.ts | 2 + lambdas/core-notifier-lambda/src/container.ts | 16 +++++--- .../src/__tests__/container.test.ts | 4 ++ .../src/__tests__/index.test.ts | 4 ++ .../src/__tests__/infra/config.test.ts | 20 +++++++++ lambdas/file-scanner-lambda/src/container.ts | 6 +++ .../file-scanner-lambda/src/infra/config.ts | 7 ++++ .../__tests__/app/move-file-handler.test.ts | 1 + .../src/__tests__/container.test.ts | 3 ++ .../src/__tests__/infra/config.test.ts | 17 +++++--- .../src/container.ts | 17 +++++++- .../src/infra/config.ts | 2 + .../src/__tests__/container.test.ts | 4 ++ .../nhsapp-status-handler/src/container.ts | 13 +++++- .../nhsapp-status-handler/src/infra/config.ts | 4 ++ .../src/__tests__/container.test.ts | 3 ++ lambdas/pdm-poll-lambda/src/container.ts | 6 +++ lambdas/pdm-poll-lambda/src/infra/config.ts | 4 ++ .../src/__tests__/container.test.ts | 3 ++ lambdas/pdm-uploader-lambda/src/container.ts | 6 +++ .../pdm-uploader-lambda/src/infra/config.ts | 4 ++ .../src/__tests__/container.test.ts | 3 ++ lambdas/print-analyser/src/container.ts | 18 +++++++- lambdas/print-analyser/src/infra/config.ts | 4 ++ .../src/__tests__/container.test.ts | 5 +++ lambdas/print-sender-lambda/src/container.ts | 14 ++++++- .../print-sender-lambda/src/infra/config.ts | 2 + .../src/__tests__/container.test.ts | 3 ++ lambdas/print-status-handler/src/container.ts | 18 +++++++- .../print-status-handler/src/infra/config.ts | 4 ++ .../src/__tests__/container.test.ts | 3 ++ lambdas/report-generator/src/container.ts | 8 ++++ lambdas/report-generator/src/infra/config.ts | 4 ++ .../src/__tests__/container.test.ts | 3 ++ lambdas/report-scheduler/src/container.ts | 11 ++++- lambdas/report-scheduler/src/infra/config.ts | 4 ++ .../src/__tests__/container.test.ts | 2 + lambdas/ttl-create-lambda/src/container.ts | 5 +++ lambdas/ttl-create-lambda/src/infra/config.ts | 2 + .../src/__tests__/container.test.ts | 4 ++ .../src/__tests__/index.test.ts | 1 + .../src/__tests__/infra/config.test.ts | 16 +++++++- .../ttl-handle-expiry-lambda/src/container.ts | 20 +++++++-- .../src/infra/config.ts | 4 ++ tests/playwright/helpers/event-bus-helpers.ts | 18 +++++++- .../cloudwatch/metric-handler.test.ts | 2 +- .../event-publisher/event-publisher.test.ts | 18 ++++++-- utils/utils/src/cloudwatch/index.ts | 1 + utils/utils/src/cloudwatch/metric-handler.ts | 8 ++-- .../src/event-publisher/event-publisher.ts | 41 ++++++++++++++----- utils/utils/src/index.ts | 1 + 60 files changed, 356 insertions(+), 46 deletions(-) create mode 100644 utils/utils/src/cloudwatch/index.ts diff --git a/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf b/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf index 880472772..337ec7359 100644 --- a/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf +++ b/infrastructure/terraform/components/dl/module_lambda_file_scanner.tf @@ -40,6 +40,7 @@ module "file_scanner" { "UNSCANNED_FILES_PATH_PREFIX" = local.csi "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf b/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf index 27176417b..e656f1b3f 100644 --- a/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf +++ b/infrastructure/terraform/components/dl/module_lambda_nhsapp_status_handler.tf @@ -38,6 +38,7 @@ module "nhsapp_status_handler" { "TTL_TABLE_NAME" = aws_dynamodb_table.ttl.name "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf b/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf index b5458a0ba..a3b703a84 100644 --- a/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf +++ b/infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf @@ -40,6 +40,7 @@ module "pdm_poll" { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url "POLL_MAX_RETRIES" = 10 + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf b/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf index 8edaea1a9..5c3d305d9 100644 --- a/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf +++ b/infrastructure/terraform/components/dl/module_lambda_pdm_uploader.tf @@ -39,6 +39,7 @@ module "pdm_uploader" { "APIM_ACCESS_TOKEN_SSM_PARAMETER_NAME" = local.pdm_access_token_ssm_parameter_name "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf b/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf index 7880b5790..a72acec32 100644 --- a/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf +++ b/infrastructure/terraform/components/dl/module_lambda_print_analyser.tf @@ -37,6 +37,7 @@ module "print_analyser" { lambda_env_vars = { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf b/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf index 8c18f640b..245f42f2f 100644 --- a/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf +++ b/infrastructure/terraform/components/dl/module_lambda_print_status_handler.tf @@ -37,6 +37,7 @@ module "print_status_handler" { lambda_env_vars = { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_report_generator.tf b/infrastructure/terraform/components/dl/module_lambda_report_generator.tf index 05ca57b03..e8fc2734b 100644 --- a/infrastructure/terraform/components/dl/module_lambda_report_generator.tf +++ b/infrastructure/terraform/components/dl/module_lambda_report_generator.tf @@ -42,6 +42,7 @@ module "report_generator" { "REPORTING_BUCKET" = module.s3bucket_reporting.bucket "REPORT_NAME" = "completed_communications" "WAIT_FOR_IN_SECONDS" = var.athena_query_polling_time_seconds + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf b/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf index 746b663f9..08a35f25d 100644 --- a/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf +++ b/infrastructure/terraform/components/dl/module_lambda_ttl_handle_expiry.tf @@ -38,6 +38,7 @@ module "ttl_handle_expiry" { "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url "DLQ_URL" = module.sqs_ttl_handle_expiry_errors.sqs_queue_url + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf b/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf index 95b9da97f..164d88e39 100644 --- a/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf +++ b/infrastructure/terraform/components/dl/module_lambda_ttl_poll.tf @@ -40,6 +40,7 @@ module "ttl_poll" { "CONCURRENCY" = 60 "MAX_PROCESS_SECONDS" = 300 "TTL_SHARD_COUNT" = local.ttl_shard_count + "ENVIRONMENT" = var.environment "DL_METRICS_NAMESPACE" = local.metrics_namespace_name } } diff --git a/lambdas/core-notifier-lambda/src/__tests__/container.test.ts b/lambdas/core-notifier-lambda/src/__tests__/container.test.ts index 4c7b25a3a..bf1fcd19a 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/container.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/container.test.ts @@ -19,6 +19,7 @@ jest.mock('utils', () => ({ debug: jest.fn(), }, EventPublisher: jest.fn(), + MetricHandler: jest.fn(), eventBridgeClient: {}, sqsClient: {}, })); @@ -42,6 +43,7 @@ describe('createContainer', () => { apimBaseUrl: 'https://api.test.nhs.uk', environment: 'test', nhsAppBaseUrl: 'https://example.com', + dlMetricsNamespace: 'test-namespace', }; const mockSenderManagement = mock(); diff --git a/lambdas/core-notifier-lambda/src/container.ts b/lambdas/core-notifier-lambda/src/container.ts index 3887ff6c5..2540b7865 100644 --- a/lambdas/core-notifier-lambda/src/container.ts +++ b/lambdas/core-notifier-lambda/src/container.ts @@ -1,5 +1,6 @@ import { EventPublisher, + MetricHandler, ParameterStoreCache, createGetApimAccessToken, eventBridgeClient, @@ -14,7 +15,6 @@ import { SenderManagement } from 'sender-management'; import { CoreRequestMapper } from 'domain/core-request-mapper'; import { MessageRequestSubmittedMapper } from 'domain/message-request-submitted-mapper'; import { MessageRequestRejectedMapper } from 'domain/message-request-rejected-mapper'; -import { MetricHandler } from '../../../utils/utils/src/cloudwatch/metric-handler'; export async function createContainer(): Promise { const parameterStore = new ParameterStoreCache(); @@ -42,11 +42,17 @@ export async function createContainer(): Promise { logger, }); - const { eventPublisherDlqUrl, eventPublisherEventBusArn, dlMetricsNamespace } = config; - const metricHandler = new MetricHandler(dlMetricsNamespace, [{ + const { + dlMetricsNamespace, + eventPublisherDlqUrl, + eventPublisherEventBusArn, + } = config; + const metricHandler = new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', - Value: 'environment', - }]); + Value: config.environment, + }, + ]); const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, diff --git a/lambdas/file-scanner-lambda/src/__tests__/container.test.ts b/lambdas/file-scanner-lambda/src/__tests__/container.test.ts index f4275bab7..4db8157ef 100644 --- a/lambdas/file-scanner-lambda/src/__tests__/container.test.ts +++ b/lambdas/file-scanner-lambda/src/__tests__/container.test.ts @@ -15,11 +15,13 @@ describe('createContainer', () => { it('should create container with all dependencies', () => { mockLoadConfig.mockReturnValue({ documentReferenceBucket: 'test-doc-ref-bucket', + environment: 'test', unscannedFilesBucket: 'test-unscanned-bucket', unscannedFilesPathPrefix: 'dev', eventPublisherEventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/test', eventPublisherDlqUrl: 'https://sqs.us-east-1.amazonaws.com/dlq', + dlMetricsNamespace: 'test-namespace', }); const container = createContainer(); @@ -33,10 +35,12 @@ describe('createContainer', () => { it('should call loadConfig to get configuration', () => { const mockConfig = { documentReferenceBucket: 'test-bucket', + environment: 'test', unscannedFilesBucket: 'test-unscanned', unscannedFilesPathPrefix: 'dev', eventPublisherEventBusArn: 'arn:test', eventPublisherDlqUrl: 'url:test', + dlMetricsNamespace: 'test-namespace', }; mockLoadConfig.mockReturnValue(mockConfig); diff --git a/lambdas/file-scanner-lambda/src/__tests__/index.test.ts b/lambdas/file-scanner-lambda/src/__tests__/index.test.ts index 00232a5e9..f7fe480f2 100644 --- a/lambdas/file-scanner-lambda/src/__tests__/index.test.ts +++ b/lambdas/file-scanner-lambda/src/__tests__/index.test.ts @@ -1,11 +1,13 @@ // Set environment variables before any imports process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; +process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'test-prefix'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:us-east-1:123456789012:event-bus/test-bus'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq'; +process.env.DL_METRICS_NAMESPACE = 'test-namespace'; // eslint-disable-next-line import-x/first import { handler } from '..'; @@ -13,10 +15,12 @@ import { handler } from '..'; describe('Lambda Handler', () => { afterAll(() => { delete process.env.DOCUMENT_REFERENCE_BUCKET; + delete process.env.ENVIRONMENT; delete process.env.UNSCANNED_FILES_BUCKET; delete process.env.UNSCANNED_FILES_PATH_PREFIX; delete process.env.EVENT_PUBLISHER_EVENT_BUS_ARN; delete process.env.EVENT_PUBLISHER_DLQ_URL; + delete process.env.DL_METRICS_NAMESPACE; }); it('should export handler function', () => { diff --git a/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts b/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts index 01462e7eb..a93e1fe01 100644 --- a/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts +++ b/lambdas/file-scanner-lambda/src/__tests__/infra/config.test.ts @@ -14,6 +14,7 @@ describe('loadConfig', () => { it('should load valid configuration', () => { process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = @@ -26,6 +27,7 @@ describe('loadConfig', () => { expect(config).toEqual({ documentReferenceBucket: 'test-doc-ref-bucket', + environment: 'test', unscannedFilesBucket: 'test-unscanned-bucket', unscannedFilesPathPrefix: 'dev', eventPublisherEventBusArn: @@ -37,6 +39,7 @@ describe('loadConfig', () => { }); it('should throw error when DOCUMENT_REFERENCE_BUCKET is missing', () => { + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; @@ -47,6 +50,7 @@ describe('loadConfig', () => { it('should throw error when UNSCANNED_FILES_BUCKET is missing', () => { process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; @@ -57,6 +61,7 @@ describe('loadConfig', () => { it('should throw error when UNSCANNED_FILES_PATH_PREFIX is missing', () => { process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; @@ -69,6 +74,7 @@ describe('loadConfig', () => { it('should throw error when EVENT_PUBLISHER_EVENT_BUS_ARN is missing', () => { process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; @@ -81,6 +87,7 @@ describe('loadConfig', () => { it('should throw error when EVENT_PUBLISHER_DLQ_URL is missing', () => { process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; @@ -91,6 +98,7 @@ describe('loadConfig', () => { it('should handle empty string values as missing', () => { process.env.DOCUMENT_REFERENCE_BUCKET = ''; + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; @@ -102,6 +110,7 @@ describe('loadConfig', () => { it('should throw error when DL_METRICS_NAMESPACE is missing', () => { process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.ENVIRONMENT = 'test'; process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; @@ -109,4 +118,15 @@ describe('loadConfig', () => { expect(() => loadConfig()).toThrow('DL_METRICS_NAMESPACE is not set'); }); + + it('should throw error when ENVIRONMENT is missing', () => { + process.env.DOCUMENT_REFERENCE_BUCKET = 'test-doc-ref-bucket'; + process.env.UNSCANNED_FILES_BUCKET = 'test-unscanned-bucket'; + process.env.UNSCANNED_FILES_PATH_PREFIX = 'dev'; + process.env.EVENT_PUBLISHER_EVENT_BUS_ARN = 'arn:aws:events:test'; + process.env.EVENT_PUBLISHER_DLQ_URL = 'https://sqs.test.com/dlq'; + process.env.DL_METRICS_NAMESPACE = 'test-namespace'; + + expect(() => loadConfig()).toThrow('ENVIRONMENT is not set'); + }); }); diff --git a/lambdas/file-scanner-lambda/src/container.ts b/lambdas/file-scanner-lambda/src/container.ts index 106871151..9b20d4603 100644 --- a/lambdas/file-scanner-lambda/src/container.ts +++ b/lambdas/file-scanner-lambda/src/container.ts @@ -3,6 +3,7 @@ import { FileScanner } from 'app/file-scanner'; import { loadConfig } from 'infra/config'; import { EventPublisher, + MetricHandler, eventBridgeClient, logger, s3Client, @@ -11,7 +12,9 @@ import { export const createContainer = (): HandlerDependencies => { const { + dlMetricsNamespace, documentReferenceBucket, + environment, eventPublisherDlqUrl, eventPublisherEventBusArn, unscannedFilesBucket, @@ -24,6 +27,9 @@ export const createContainer = (): HandlerDependencies => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); const fileScanner = new FileScanner({ diff --git a/lambdas/file-scanner-lambda/src/infra/config.ts b/lambdas/file-scanner-lambda/src/infra/config.ts index cfe61aef6..bdb5dbfa4 100644 --- a/lambdas/file-scanner-lambda/src/infra/config.ts +++ b/lambdas/file-scanner-lambda/src/infra/config.ts @@ -2,6 +2,7 @@ import { logger } from 'utils'; export interface Config { documentReferenceBucket: string; + environment: string; unscannedFilesBucket: string; unscannedFilesPathPrefix: string; eventPublisherEventBusArn: string; @@ -11,6 +12,7 @@ export interface Config { export function loadConfig(): Config { const documentReferenceBucket = process.env.DOCUMENT_REFERENCE_BUCKET; + const environment = process.env.ENVIRONMENT; const unscannedFilesBucket = process.env.UNSCANNED_FILES_BUCKET; const unscannedFilesPathPrefix = process.env.UNSCANNED_FILES_PATH_PREFIX; const eventPublisherEventBusArn = process.env.EVENT_PUBLISHER_EVENT_BUS_ARN; @@ -21,6 +23,10 @@ export function loadConfig(): Config { throw new Error('DOCUMENT_REFERENCE_BUCKET is not set'); } + if (!environment) { + throw new Error('ENVIRONMENT is not set'); + } + if (!unscannedFilesBucket) { throw new Error('UNSCANNED_FILES_BUCKET is not set'); } @@ -50,6 +56,7 @@ export function loadConfig(): Config { return { documentReferenceBucket, + environment, unscannedFilesBucket, unscannedFilesPathPrefix, eventPublisherEventBusArn, diff --git a/lambdas/move-scanned-files-lambda/src/__tests__/app/move-file-handler.test.ts b/lambdas/move-scanned-files-lambda/src/__tests__/app/move-file-handler.test.ts index 27d54fc02..2dd7188c3 100644 --- a/lambdas/move-scanned-files-lambda/src/__tests__/app/move-file-handler.test.ts +++ b/lambdas/move-scanned-files-lambda/src/__tests__/app/move-file-handler.test.ts @@ -65,6 +65,7 @@ describe('MoveFileHandler', () => { unscannedFileS3BucketName: 'unscanned-bucket', safeFileS3BucketName: 'safe-bucket', quarantineFileS3BucketName: 'quarantine-bucket', + dlMetricsNamespace: 'test-namespace', }; const mockCopyAndDeleteObjectS3 = jest.mocked(utils.copyAndDeleteObjectS3); diff --git a/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts b/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts index c0008c762..3e552c10d 100644 --- a/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts +++ b/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts @@ -12,6 +12,7 @@ jest.mock('utils', () => ({ debug: jest.fn(), }, EventPublisher: jest.fn(), + MetricHandler: jest.fn(), eventBridgeClient: {}, sqsClient: {}, })); @@ -25,6 +26,7 @@ describe('createContainer', () => { 'arn:aws:events:eu-west-2:123456789012:event-bus/test-bus', eventPublisherDlqUrl: 'https://sqs.eu-west-2.amazonaws.com/123456789012/test-dlq', + dlMetricsNamespace: 'test-namespace', environment: 'test', keyPrefixUnscannedFiles: 'dl/', unscannedFileS3BucketName: 'unscanned-bucket', @@ -79,6 +81,7 @@ describe('createContainer', () => { logger, sqsClient: expect.any(Object), eventBridgeClient: expect.any(Object), + metricHandler: expect.any(Object), }), ); }); diff --git a/lambdas/move-scanned-files-lambda/src/__tests__/infra/config.test.ts b/lambdas/move-scanned-files-lambda/src/__tests__/infra/config.test.ts index 15b9f91b5..8c3a01879 100644 --- a/lambdas/move-scanned-files-lambda/src/__tests__/infra/config.test.ts +++ b/lambdas/move-scanned-files-lambda/src/__tests__/infra/config.test.ts @@ -20,6 +20,7 @@ describe('loadConfig', () => { 'arn:aws:events:eu-west-2:123456789012:event-bus/test-bus', eventPublisherDlqUrl: 'https://sqs.eu-west-2.amazonaws.com/123456789012/test-dlq', + dlMetricsNamespace: 'test-namespace', environment: 'test', keyPrefixUnscannedFiles: 'dl/', unscannedFileS3BucketName: 'unscanned-bucket', @@ -30,6 +31,7 @@ describe('loadConfig', () => { mockGetValue .mockReturnValueOnce(mockConfig.eventPublisherEventBusArn) .mockReturnValueOnce(mockConfig.eventPublisherDlqUrl) + .mockReturnValueOnce(mockConfig.dlMetricsNamespace) .mockReturnValueOnce(mockConfig.keyPrefixUnscannedFiles) .mockReturnValueOnce(mockConfig.unscannedFileS3BucketName) .mockReturnValueOnce(mockConfig.safeFileS3BucketName) @@ -39,32 +41,34 @@ describe('loadConfig', () => { const result = loadConfig(); expect(result).toEqual(mockConfig); - expect(mockGetValue).toHaveBeenCalledTimes(7); + expect(mockGetValue).toHaveBeenCalledTimes(8); expect(mockGetValue).toHaveBeenNthCalledWith( 1, 'EVENT_PUBLISHER_EVENT_BUS_ARN', ); expect(mockGetValue).toHaveBeenNthCalledWith(2, 'EVENT_PUBLISHER_DLQ_URL'); + expect(mockGetValue).toHaveBeenNthCalledWith(3, 'DL_METRICS_NAMESPACE'); expect(mockGetValue).toHaveBeenNthCalledWith( - 3, + 4, 'KEY_PREFIX_UNSCANNED_FILES', ); expect(mockGetValue).toHaveBeenNthCalledWith( - 4, + 5, 'UNSCANNED_FILE_S3_BUCKET_NAME', ); - expect(mockGetValue).toHaveBeenNthCalledWith(5, 'SAFE_FILE_S3_BUCKET_NAME'); + expect(mockGetValue).toHaveBeenNthCalledWith(6, 'SAFE_FILE_S3_BUCKET_NAME'); expect(mockGetValue).toHaveBeenNthCalledWith( - 6, + 7, 'QUARANTINE_FILE_S3_BUCKET_NAME', ); - expect(mockGetValue).toHaveBeenNthCalledWith(7, 'ENVIRONMENT'); + expect(mockGetValue).toHaveBeenNthCalledWith(8, 'ENVIRONMENT'); }); it('returns config with correct types', () => { mockGetValue .mockReturnValueOnce('arn:test') .mockReturnValueOnce('https://dlq') + .mockReturnValueOnce('test-namespace') .mockReturnValueOnce('dl/') .mockReturnValueOnce('unscanned-bucket') .mockReturnValueOnce('safe-bucket') @@ -75,6 +79,7 @@ describe('loadConfig', () => { expect(typeof result.eventPublisherEventBusArn).toBe('string'); expect(typeof result.eventPublisherDlqUrl).toBe('string'); + expect(typeof result.dlMetricsNamespace).toBe('string'); expect(typeof result.environment).toBe('string'); expect(typeof result.keyPrefixUnscannedFiles).toBe('string'); expect(typeof result.unscannedFileS3BucketName).toBe('string'); diff --git a/lambdas/move-scanned-files-lambda/src/container.ts b/lambdas/move-scanned-files-lambda/src/container.ts index 868249f9a..51508f9de 100644 --- a/lambdas/move-scanned-files-lambda/src/container.ts +++ b/lambdas/move-scanned-files-lambda/src/container.ts @@ -1,4 +1,10 @@ -import { EventPublisher, eventBridgeClient, logger, sqsClient } from 'utils'; +import { + EventPublisher, + MetricHandler, + eventBridgeClient, + logger, + sqsClient, +} from 'utils'; import type { SqsHandlerDependencies } from 'apis/sqs-handler'; import { loadConfig } from 'infra/config'; import { MoveFileHandler } from 'app/move-file-handler'; @@ -6,7 +12,11 @@ import { MoveFileHandler } from 'app/move-file-handler'; export async function createContainer(): Promise { const config = loadConfig(); - const { eventPublisherDlqUrl, eventPublisherEventBusArn } = config; + const { + dlMetricsNamespace, + eventPublisherDlqUrl, + eventPublisherEventBusArn, + } = config; const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, @@ -14,6 +24,9 @@ export async function createContainer(): Promise { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: config.environment }, + ]), }); const moveFileHandler = new MoveFileHandler(logger, config); diff --git a/lambdas/move-scanned-files-lambda/src/infra/config.ts b/lambdas/move-scanned-files-lambda/src/infra/config.ts index 96702b6d0..6454f3b5d 100644 --- a/lambdas/move-scanned-files-lambda/src/infra/config.ts +++ b/lambdas/move-scanned-files-lambda/src/infra/config.ts @@ -3,6 +3,7 @@ import { defaultConfigReader } from 'utils'; export type MoveScannedFilesConfig = { eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; environment: string; unscannedFileS3BucketName: string; safeFileS3BucketName: string; @@ -18,6 +19,7 @@ export function loadConfig(): MoveScannedFilesConfig { eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), // There is a limitation of how many buckets can be scanned with GuardDuty per account. // As DL will share the same bucket with other services, this is a safeguard to only process events for files for digital letters. keyPrefixUnscannedFiles: defaultConfigReader.getValue( diff --git a/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts b/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts index d52ce0228..2f9aa0fb1 100644 --- a/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts +++ b/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts @@ -2,8 +2,10 @@ import { createContainer } from 'container'; jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ + environment: 'test', eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', ttlTableName: 'test-table', })), })); @@ -18,9 +20,11 @@ jest.mock('app/ttl-actions', () => ({ jest.mock('utils', () => ({ EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), dynamoDocumentClient: {}, eventBridgeClient: {}, logger: {}, + sqsClient: {}, })); describe('container', () => { diff --git a/lambdas/nhsapp-status-handler/src/container.ts b/lambdas/nhsapp-status-handler/src/container.ts index 132a9f1d2..4f829ccf1 100644 --- a/lambdas/nhsapp-status-handler/src/container.ts +++ b/lambdas/nhsapp-status-handler/src/container.ts @@ -1,5 +1,6 @@ import { EventPublisher, + MetricHandler, dynamoDocumentClient, eventBridgeClient, logger, @@ -10,8 +11,13 @@ import { TtlRepository } from 'infra/ttl-repository'; import { TtlActions } from 'app/ttl-actions'; export const createContainer = () => { - const { eventPublisherDlqUrl, eventPublisherEventBusArn, ttlTableName } = - loadConfig(); + const { + dlMetricsNamespace, + environment, + eventPublisherDlqUrl, + eventPublisherEventBusArn, + ttlTableName, + } = loadConfig(); const requestTtlRepository = new TtlRepository( ttlTableName, @@ -26,6 +32,9 @@ export const createContainer = () => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); return { diff --git a/lambdas/nhsapp-status-handler/src/infra/config.ts b/lambdas/nhsapp-status-handler/src/infra/config.ts index d2122e00e..3f81612a2 100644 --- a/lambdas/nhsapp-status-handler/src/infra/config.ts +++ b/lambdas/nhsapp-status-handler/src/infra/config.ts @@ -1,13 +1,16 @@ import { defaultConfigReader } from 'utils'; export type TtlCreateConfig = { + environment: string; ttlTableName: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; }; export function loadConfig(): TtlCreateConfig { return { + environment: defaultConfigReader.getValue('ENVIRONMENT'), ttlTableName: defaultConfigReader.getValue('TTL_TABLE_NAME'), eventPublisherEventBusArn: defaultConfigReader.getValue( 'EVENT_PUBLISHER_EVENT_BUS_ARN', @@ -15,5 +18,6 @@ export function loadConfig(): TtlCreateConfig { eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts index 8833bc850..a8b362e93 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts @@ -4,8 +4,10 @@ jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ apimBaseUrl: 'https://test-apim-url', apimAccessTokenSsmParameterName: 'test-ssm-parameter-name', + environment: 'test', eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', maxPollCount: 10, })), })); @@ -14,6 +16,7 @@ jest.mock('utils', () => ({ createGetApimAccessToken: jest.fn(() => ({})), eventBridgeClient: {}, EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), logger: {}, ParameterStoreCache: jest.fn(() => ({})), PdmClient: jest.fn(() => ({})), diff --git a/lambdas/pdm-poll-lambda/src/container.ts b/lambdas/pdm-poll-lambda/src/container.ts index d8514aa84..3efc05781 100644 --- a/lambdas/pdm-poll-lambda/src/container.ts +++ b/lambdas/pdm-poll-lambda/src/container.ts @@ -3,6 +3,7 @@ import { Pdm } from 'app/pdm'; import { loadConfig } from 'infra/config'; import { EventPublisher, + MetricHandler, ParameterStoreCache, PdmClient, createGetApimAccessToken, @@ -15,6 +16,8 @@ export const createContainer = (): HandlerDependencies => { const { apimAccessTokenSsmParameterName, apimBaseUrl, + dlMetricsNamespace, + environment, eventPublisherDlqUrl, eventPublisherEventBusArn, pollMaxRetries, @@ -26,6 +29,9 @@ export const createContainer = (): HandlerDependencies => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); const parameterStore = new ParameterStoreCache(); diff --git a/lambdas/pdm-poll-lambda/src/infra/config.ts b/lambdas/pdm-poll-lambda/src/infra/config.ts index e40455ff8..45ee2850f 100644 --- a/lambdas/pdm-poll-lambda/src/infra/config.ts +++ b/lambdas/pdm-poll-lambda/src/infra/config.ts @@ -2,15 +2,18 @@ import { defaultConfigReader } from 'utils'; export type PdmCreateConfig = { apimBaseUrl: string; + environment: string; pollMaxRetries: number; apimAccessTokenSsmParameterName: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; }; export function loadConfig(): PdmCreateConfig { return { apimBaseUrl: defaultConfigReader.getValue('APIM_BASE_URL'), + environment: defaultConfigReader.getValue('ENVIRONMENT'), pollMaxRetries: defaultConfigReader.getInt('POLL_MAX_RETRIES'), apimAccessTokenSsmParameterName: defaultConfigReader.getValue( 'APIM_ACCESS_TOKEN_SSM_PARAMETER_NAME', @@ -21,5 +24,6 @@ export function loadConfig(): PdmCreateConfig { eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts b/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts index de443d4a6..23a03094f 100644 --- a/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts +++ b/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts @@ -4,8 +4,10 @@ jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ apimBaseUrl: 'https://test-apim-url', apimAccessTokenSsmParameterName: 'test-ssm-parameter-name', + environment: 'test', eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', })), })); @@ -17,6 +19,7 @@ jest.mock('utils', () => ({ createGetApimAccessToken: jest.fn(() => ({})), eventBridgeClient: {}, EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), logger: {}, ParameterStoreCache: jest.fn(() => ({})), PdmClient: jest.fn(() => ({})), diff --git a/lambdas/pdm-uploader-lambda/src/container.ts b/lambdas/pdm-uploader-lambda/src/container.ts index bdca7a878..54c19249a 100644 --- a/lambdas/pdm-uploader-lambda/src/container.ts +++ b/lambdas/pdm-uploader-lambda/src/container.ts @@ -1,5 +1,6 @@ import { EventPublisher, + MetricHandler, ParameterStoreCache, PdmClient, createGetApimAccessToken, @@ -14,6 +15,8 @@ export const createContainer = () => { const { apimAccessTokenSsmParameterName, apimBaseUrl, + dlMetricsNamespace, + environment, eventPublisherDlqUrl, eventPublisherEventBusArn, } = loadConfig(); @@ -38,6 +41,9 @@ export const createContainer = () => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); return { diff --git a/lambdas/pdm-uploader-lambda/src/infra/config.ts b/lambdas/pdm-uploader-lambda/src/infra/config.ts index 2005ccdf3..56883c85d 100644 --- a/lambdas/pdm-uploader-lambda/src/infra/config.ts +++ b/lambdas/pdm-uploader-lambda/src/infra/config.ts @@ -3,8 +3,10 @@ import { defaultConfigReader } from 'utils'; export type PdmCreateConfig = { apimBaseUrl: string; apimAccessTokenSsmParameterName: string; + environment: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; }; export function loadConfig(): PdmCreateConfig { @@ -13,11 +15,13 @@ export function loadConfig(): PdmCreateConfig { apimAccessTokenSsmParameterName: defaultConfigReader.getValue( 'APIM_ACCESS_TOKEN_SSM_PARAMETER_NAME', ), + environment: defaultConfigReader.getValue('ENVIRONMENT'), eventPublisherEventBusArn: defaultConfigReader.getValue( 'EVENT_PUBLISHER_EVENT_BUS_ARN', ), eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/print-analyser/src/__tests__/container.test.ts b/lambdas/print-analyser/src/__tests__/container.test.ts index 64f1a694d..75af881d3 100644 --- a/lambdas/print-analyser/src/__tests__/container.test.ts +++ b/lambdas/print-analyser/src/__tests__/container.test.ts @@ -2,14 +2,17 @@ import { createContainer } from 'container'; jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ + environment: 'test', eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', })), })); jest.mock('utils', () => ({ eventBridgeClient: {}, EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), logger: {}, sqsClient: {}, })); diff --git a/lambdas/print-analyser/src/container.ts b/lambdas/print-analyser/src/container.ts index 7fe49378d..971ebe65b 100644 --- a/lambdas/print-analyser/src/container.ts +++ b/lambdas/print-analyser/src/container.ts @@ -1,9 +1,20 @@ import { HandlerDependencies } from 'apis/sqs-handler'; import { loadConfig } from 'infra/config'; -import { EventPublisher, eventBridgeClient, logger, sqsClient } from 'utils'; +import { + EventPublisher, + MetricHandler, + eventBridgeClient, + logger, + sqsClient, +} from 'utils'; export const createContainer = (): HandlerDependencies => { - const { eventPublisherDlqUrl, eventPublisherEventBusArn } = loadConfig(); + const { + dlMetricsNamespace, + environment, + eventPublisherDlqUrl, + eventPublisherEventBusArn, + } = loadConfig(); const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, @@ -11,6 +22,9 @@ export const createContainer = (): HandlerDependencies => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); return { eventPublisher, logger }; diff --git a/lambdas/print-analyser/src/infra/config.ts b/lambdas/print-analyser/src/infra/config.ts index 855e66108..059052dad 100644 --- a/lambdas/print-analyser/src/infra/config.ts +++ b/lambdas/print-analyser/src/infra/config.ts @@ -1,17 +1,21 @@ import { defaultConfigReader } from 'utils'; export type Config = { + environment: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; }; export function loadConfig(): Config { return { + environment: defaultConfigReader.getValue('ENVIRONMENT'), eventPublisherEventBusArn: defaultConfigReader.getValue( 'EVENT_PUBLISHER_EVENT_BUS_ARN', ), eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/print-sender-lambda/src/__tests__/container.test.ts b/lambdas/print-sender-lambda/src/__tests__/container.test.ts index 4715eb42f..ee71cb0b1 100644 --- a/lambdas/print-sender-lambda/src/__tests__/container.test.ts +++ b/lambdas/print-sender-lambda/src/__tests__/container.test.ts @@ -4,6 +4,9 @@ jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', + environment: 'test', + accountType: 'test-account', })), })); @@ -13,8 +16,10 @@ jest.mock('app/print-sender', () => ({ jest.mock('utils', () => ({ EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), eventBridgeClient: {}, logger: {}, + sqsClient: {}, })); describe('container', () => { diff --git a/lambdas/print-sender-lambda/src/container.ts b/lambdas/print-sender-lambda/src/container.ts index a7e9af9ad..540aa5c76 100644 --- a/lambdas/print-sender-lambda/src/container.ts +++ b/lambdas/print-sender-lambda/src/container.ts @@ -1,21 +1,33 @@ -import { EventPublisher, eventBridgeClient, logger, sqsClient } from 'utils'; +import { + EventPublisher, + MetricHandler, + eventBridgeClient, + logger, + sqsClient, +} from 'utils'; import { loadConfig } from 'infra/config'; import { PrintSender } from 'app/print-sender'; export const createContainer = () => { const { accountType, + dlMetricsNamespace, environment, eventPublisherDlqUrl, eventPublisherEventBusArn, } = loadConfig(); + const metricHandler = new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]); + const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, dlqUrl: eventPublisherDlqUrl, logger, sqsClient, eventBridgeClient, + metricHandler, }); const printSender = new PrintSender( diff --git a/lambdas/print-sender-lambda/src/infra/config.ts b/lambdas/print-sender-lambda/src/infra/config.ts index a119f426e..3393d27a2 100644 --- a/lambdas/print-sender-lambda/src/infra/config.ts +++ b/lambdas/print-sender-lambda/src/infra/config.ts @@ -3,6 +3,7 @@ import { defaultConfigReader } from 'utils'; export type PrintSenderConfig = { eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; environment: string; accountType: string; }; @@ -15,6 +16,7 @@ export function loadConfig(): PrintSenderConfig { eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), environment: defaultConfigReader.getValue('ENVIRONMENT'), accountType: defaultConfigReader.getValue('ACCOUNT_TYPE'), }; diff --git a/lambdas/print-status-handler/src/__tests__/container.test.ts b/lambdas/print-status-handler/src/__tests__/container.test.ts index 64f1a694d..75af881d3 100644 --- a/lambdas/print-status-handler/src/__tests__/container.test.ts +++ b/lambdas/print-status-handler/src/__tests__/container.test.ts @@ -2,14 +2,17 @@ import { createContainer } from 'container'; jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ + environment: 'test', eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', })), })); jest.mock('utils', () => ({ eventBridgeClient: {}, EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), logger: {}, sqsClient: {}, })); diff --git a/lambdas/print-status-handler/src/container.ts b/lambdas/print-status-handler/src/container.ts index 7fe49378d..971ebe65b 100644 --- a/lambdas/print-status-handler/src/container.ts +++ b/lambdas/print-status-handler/src/container.ts @@ -1,9 +1,20 @@ import { HandlerDependencies } from 'apis/sqs-handler'; import { loadConfig } from 'infra/config'; -import { EventPublisher, eventBridgeClient, logger, sqsClient } from 'utils'; +import { + EventPublisher, + MetricHandler, + eventBridgeClient, + logger, + sqsClient, +} from 'utils'; export const createContainer = (): HandlerDependencies => { - const { eventPublisherDlqUrl, eventPublisherEventBusArn } = loadConfig(); + const { + dlMetricsNamespace, + environment, + eventPublisherDlqUrl, + eventPublisherEventBusArn, + } = loadConfig(); const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, @@ -11,6 +22,9 @@ export const createContainer = (): HandlerDependencies => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); return { eventPublisher, logger }; diff --git a/lambdas/print-status-handler/src/infra/config.ts b/lambdas/print-status-handler/src/infra/config.ts index 855e66108..059052dad 100644 --- a/lambdas/print-status-handler/src/infra/config.ts +++ b/lambdas/print-status-handler/src/infra/config.ts @@ -1,17 +1,21 @@ import { defaultConfigReader } from 'utils'; export type Config = { + environment: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; }; export function loadConfig(): Config { return { + environment: defaultConfigReader.getValue('ENVIRONMENT'), eventPublisherEventBusArn: defaultConfigReader.getValue( 'EVENT_PUBLISHER_EVENT_BUS_ARN', ), eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/report-generator/src/__tests__/container.test.ts b/lambdas/report-generator/src/__tests__/container.test.ts index c9aa4fe99..9f5db51d2 100644 --- a/lambdas/report-generator/src/__tests__/container.test.ts +++ b/lambdas/report-generator/src/__tests__/container.test.ts @@ -3,8 +3,10 @@ import { createContainer } from 'container'; jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ athenaNamedQueryId: 'test-named-query-id', + environment: 'test', eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', maxPollLimit: 10, reportName: 'test-report', reportingBucket: 'test-bucket', @@ -21,6 +23,7 @@ jest.mock('utils', () => ({ s3Client: {}, eventBridgeClient: {}, EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), logger: {}, sqsClient: {}, })); diff --git a/lambdas/report-generator/src/container.ts b/lambdas/report-generator/src/container.ts index b4bfc43b5..b76b14833 100644 --- a/lambdas/report-generator/src/container.ts +++ b/lambdas/report-generator/src/container.ts @@ -2,6 +2,7 @@ import { AthenaDataRepository, AthenaDataRepositoryDependencies, EventPublisher, + MetricHandler, ReportService, S3StorageRepository, eventBridgeClient, @@ -17,6 +18,8 @@ import { AthenaClient } from '@aws-sdk/client-athena'; export const createContainer = () => { const { athenaNamedQueryId, + dlMetricsNamespace, + environment, eventPublisherDlqUrl, eventPublisherEventBusArn, maxPollLimit, @@ -56,12 +59,17 @@ export const createContainer = () => { athenaNamedQueryId, ); + const metricHandler = new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]); + const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, dlqUrl: eventPublisherDlqUrl, logger, sqsClient, eventBridgeClient, + metricHandler, }); return { diff --git a/lambdas/report-generator/src/infra/config.ts b/lambdas/report-generator/src/infra/config.ts index c0c1f9701..01ae84f54 100644 --- a/lambdas/report-generator/src/infra/config.ts +++ b/lambdas/report-generator/src/infra/config.ts @@ -2,8 +2,10 @@ import { defaultConfigReader } from 'utils'; export type ReportGeneratorConfig = { athenaNamedQueryId: string; + environment: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; maxPollLimit: number; reportingBucket: string; reportName: string; @@ -13,12 +15,14 @@ export type ReportGeneratorConfig = { export function loadConfig(): ReportGeneratorConfig { return { athenaNamedQueryId: defaultConfigReader.getValue('ATHENA_NAMED_QUERY_ID'), + environment: defaultConfigReader.getValue('ENVIRONMENT'), eventPublisherEventBusArn: defaultConfigReader.getValue( 'EVENT_PUBLISHER_EVENT_BUS_ARN', ), eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), maxPollLimit: defaultConfigReader.getInt('MAX_POLL_LIMIT'), reportingBucket: defaultConfigReader.getValue('REPORTING_BUCKET'), reportName: defaultConfigReader.getValue('REPORT_NAME'), diff --git a/lambdas/report-scheduler/src/__tests__/container.test.ts b/lambdas/report-scheduler/src/__tests__/container.test.ts index 2abec6611..383769e94 100644 --- a/lambdas/report-scheduler/src/__tests__/container.test.ts +++ b/lambdas/report-scheduler/src/__tests__/container.test.ts @@ -2,8 +2,10 @@ import { createContainer } from 'container'; jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ + environment: 'test', eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', })), })); @@ -13,6 +15,7 @@ jest.mock('sender-management', () => ({ jest.mock('utils', () => ({ EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), ParameterStoreCache: jest.fn(() => ({})), eventBridgeClient: {}, logger: {}, diff --git a/lambdas/report-scheduler/src/container.ts b/lambdas/report-scheduler/src/container.ts index 3a2166556..aa0af5ffa 100644 --- a/lambdas/report-scheduler/src/container.ts +++ b/lambdas/report-scheduler/src/container.ts @@ -3,6 +3,7 @@ import { CreateHandlerDependencies } from 'apis/scheduled-event-handler'; import { SenderManagement } from 'sender-management'; import { EventPublisher, + MetricHandler, ParameterStoreCache, eventBridgeClient, logger, @@ -10,7 +11,12 @@ import { } from 'utils'; export const createContainer = (): CreateHandlerDependencies => { - const { eventPublisherDlqUrl, eventPublisherEventBusArn } = loadConfig(); + const { + dlMetricsNamespace, + environment, + eventPublisherDlqUrl, + eventPublisherEventBusArn, + } = loadConfig(); const parameterStore = new ParameterStoreCache(); const senderManagement = SenderManagement({ @@ -23,6 +29,9 @@ export const createContainer = (): CreateHandlerDependencies => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); return { senderManagement, eventPublisher }; diff --git a/lambdas/report-scheduler/src/infra/config.ts b/lambdas/report-scheduler/src/infra/config.ts index ec2dbd5e5..a7080ea81 100644 --- a/lambdas/report-scheduler/src/infra/config.ts +++ b/lambdas/report-scheduler/src/infra/config.ts @@ -1,17 +1,21 @@ import { defaultConfigReader } from 'utils'; export type ReportSchedulerConfig = { + environment: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; }; export function loadConfig(): ReportSchedulerConfig { return { + environment: defaultConfigReader.getValue('ENVIRONMENT'), eventPublisherEventBusArn: defaultConfigReader.getValue( 'EVENT_PUBLISHER_EVENT_BUS_ARN', ), eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/ttl-create-lambda/src/__tests__/container.test.ts b/lambdas/ttl-create-lambda/src/__tests__/container.test.ts index 515723fa5..797f258f4 100644 --- a/lambdas/ttl-create-lambda/src/__tests__/container.test.ts +++ b/lambdas/ttl-create-lambda/src/__tests__/container.test.ts @@ -4,6 +4,7 @@ jest.mock('infra/config', () => ({ loadConfig: jest.fn(() => ({ eventPublisherDlqUrl: 'test-url', eventPublisherEventBusArn: 'test-arn', + dlMetricsNamespace: 'test-namespace', ttlShardCount: 1, ttlTableName: 'test-table', environment: 'test-environment', @@ -24,6 +25,7 @@ jest.mock('sender-management', () => ({ jest.mock('utils', () => ({ EventPublisher: jest.fn(() => ({})), + MetricHandler: jest.fn(() => ({})), dynamoClient: {}, eventBridgeClient: {}, logger: {}, diff --git a/lambdas/ttl-create-lambda/src/container.ts b/lambdas/ttl-create-lambda/src/container.ts index 8b892dfdd..ae401ef2c 100644 --- a/lambdas/ttl-create-lambda/src/container.ts +++ b/lambdas/ttl-create-lambda/src/container.ts @@ -1,5 +1,6 @@ import { EventPublisher, + MetricHandler, ParameterStoreCache, dynamoClient, eventBridgeClient, @@ -13,6 +14,7 @@ import { SenderManagement } from 'sender-management'; export const createContainer = () => { const { + dlMetricsNamespace, environment, eventPublisherDlqUrl, eventPublisherEventBusArn, @@ -43,6 +45,9 @@ export const createContainer = () => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); return { diff --git a/lambdas/ttl-create-lambda/src/infra/config.ts b/lambdas/ttl-create-lambda/src/infra/config.ts index 1e12c8489..6fe66c62f 100644 --- a/lambdas/ttl-create-lambda/src/infra/config.ts +++ b/lambdas/ttl-create-lambda/src/infra/config.ts @@ -6,6 +6,7 @@ export type TtlCreateConfig = { ttlShardCount: number; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; }; export function loadConfig(): TtlCreateConfig { @@ -19,5 +20,6 @@ export function loadConfig(): TtlCreateConfig { eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), }; } diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts index 5e814ba3f..f2be8c30f 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts @@ -4,6 +4,7 @@ import { createContainer } from 'container'; jest.mock('utils', () => ({ EventPublisher: jest.fn(), + MetricHandler: jest.fn(), eventBridgeClient: {}, logger: {}, sqsClient: {}, @@ -27,6 +28,8 @@ describe('createContainer', () => { 'arn:aws:events:us-east-1:123456789012:event-bus/test-bus', eventPublisherDlqUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/test-event-dlq', + environment: 'test', + dlMetricsNamespace: 'test-namespace', dlqUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq', }; @@ -47,6 +50,7 @@ describe('createContainer', () => { logger: expect.any(Object), sqsClient: expect.any(Object), eventBridgeClient: expect.any(Object), + metricHandler: expect.any(Object), }); expect(container).toEqual({ diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts index f9f0dca65..e1d5d9546 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts @@ -5,6 +5,7 @@ jest.mock('utils', () => ({ EventPublisher: jest.fn().mockImplementation(() => ({ sendEvents: jest.fn().mockResolvedValue({}), })), + MetricHandler: jest.fn().mockImplementation(() => ({})), eventBridgeClient: {}, logger: { info: jest.fn(), diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/infra/config.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/infra/config.test.ts index 74e6a0d91..ea23047f2 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/infra/config.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/infra/config.test.ts @@ -25,41 +25,55 @@ describe('loadConfig', () => { 'https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq'; mockGetValue + .mockReturnValueOnce('test') .mockReturnValueOnce(mockEventBusArn) .mockReturnValueOnce(mockEventPublisherDlqUrl) + .mockReturnValueOnce('test-namespace') .mockReturnValueOnce(mockDlqUrl); const config = loadConfig(); - expect(defaultConfigReader.getValue).toHaveBeenCalledTimes(3); + expect(defaultConfigReader.getValue).toHaveBeenCalledTimes(5); + expect(defaultConfigReader.getValue).toHaveBeenCalledWith('ENVIRONMENT'); expect(defaultConfigReader.getValue).toHaveBeenCalledWith( 'EVENT_PUBLISHER_EVENT_BUS_ARN', ); expect(defaultConfigReader.getValue).toHaveBeenCalledWith( 'EVENT_PUBLISHER_DLQ_URL', ); + expect(defaultConfigReader.getValue).toHaveBeenCalledWith( + 'DL_METRICS_NAMESPACE', + ); expect(defaultConfigReader.getValue).toHaveBeenCalledWith('DLQ_URL'); expect(config).toEqual({ + environment: 'test', eventPublisherEventBusArn: mockEventBusArn, eventPublisherDlqUrl: mockEventPublisherDlqUrl, + dlMetricsNamespace: 'test-namespace', dlqUrl: mockDlqUrl, }); }); it('should return config with correct structure', () => { mockGetValue + .mockReturnValueOnce('prod') .mockReturnValueOnce('test-bus-arn') .mockReturnValueOnce('test-publisher-dlq-url') + .mockReturnValueOnce('test-namespace') .mockReturnValueOnce('test-dlq-url'); const config = loadConfig(); + expect(config).toHaveProperty('environment'); expect(config).toHaveProperty('eventPublisherEventBusArn'); expect(config).toHaveProperty('eventPublisherDlqUrl'); + expect(config).toHaveProperty('dlMetricsNamespace'); expect(config).toHaveProperty('dlqUrl'); + expect(typeof config.environment).toBe('string'); expect(typeof config.eventPublisherEventBusArn).toBe('string'); expect(typeof config.eventPublisherDlqUrl).toBe('string'); + expect(typeof config.dlMetricsNamespace).toBe('string'); expect(typeof config.dlqUrl).toBe('string'); }); }); diff --git a/lambdas/ttl-handle-expiry-lambda/src/container.ts b/lambdas/ttl-handle-expiry-lambda/src/container.ts index 3b3e8c652..2d95e23cc 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/container.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/container.ts @@ -1,11 +1,22 @@ -import { EventPublisher, eventBridgeClient, logger, sqsClient } from 'utils'; +import { + EventPublisher, + MetricHandler, + eventBridgeClient, + logger, + sqsClient, +} from 'utils'; import { CreateHandlerDependencies } from 'apis/dynamodb-stream-handler'; import { loadConfig } from 'infra/config'; import { Dlq } from 'app/dlq'; export const createContainer = (): CreateHandlerDependencies => { - const { dlqUrl, eventPublisherDlqUrl, eventPublisherEventBusArn } = - loadConfig(); + const { + dlMetricsNamespace, + dlqUrl, + environment, + eventPublisherDlqUrl, + eventPublisherEventBusArn, + } = loadConfig(); const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, @@ -13,6 +24,9 @@ export const createContainer = (): CreateHandlerDependencies => { logger, sqsClient, eventBridgeClient, + metricHandler: new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]), }); const dlq = new Dlq({ diff --git a/lambdas/ttl-handle-expiry-lambda/src/infra/config.ts b/lambdas/ttl-handle-expiry-lambda/src/infra/config.ts index f94a9af26..beefe3117 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/infra/config.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/infra/config.ts @@ -1,19 +1,23 @@ import { defaultConfigReader } from 'utils'; export type SendRequestConfig = { + environment: string; eventPublisherEventBusArn: string; eventPublisherDlqUrl: string; + dlMetricsNamespace: string; dlqUrl: string; }; export function loadConfig(): SendRequestConfig { return { + environment: defaultConfigReader.getValue('ENVIRONMENT'), eventPublisherEventBusArn: defaultConfigReader.getValue( 'EVENT_PUBLISHER_EVENT_BUS_ARN', ), eventPublisherDlqUrl: defaultConfigReader.getValue( 'EVENT_PUBLISHER_DLQ_URL', ), + dlMetricsNamespace: defaultConfigReader.getValue('DL_METRICS_NAMESPACE'), dlqUrl: defaultConfigReader.getValue('DLQ_URL'), }; } diff --git a/tests/playwright/helpers/event-bus-helpers.ts b/tests/playwright/helpers/event-bus-helpers.ts index 6edfa99e0..604484a4f 100644 --- a/tests/playwright/helpers/event-bus-helpers.ts +++ b/tests/playwright/helpers/event-bus-helpers.ts @@ -1,12 +1,26 @@ -import { EVENT_BUS_ARN, EVENT_BUS_DLQ_URL } from 'constants/backend-constants'; -import { EventPublisher, eventBridgeClient, logger, sqsClient } from 'utils'; +import { + ENV, + EVENT_BUS_ARN, + EVENT_BUS_DLQ_URL, +} from 'constants/backend-constants'; +import { + EventPublisher, + MetricHandler, + eventBridgeClient, + logger, + sqsClient, +} from 'utils'; +const metricHandler = new MetricHandler(`nhs-${ENV}-dl-component-test`, [ + { Name: 'Environment', Value: ENV }, +]); const eventPublisher = new EventPublisher({ eventBusArn: EVENT_BUS_ARN, dlqUrl: EVENT_BUS_DLQ_URL, logger, sqsClient, eventBridgeClient, + metricHandler, }); export default eventPublisher; diff --git a/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts b/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts index 30bd94a2b..013af1e16 100644 --- a/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts +++ b/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts @@ -1,6 +1,6 @@ import { MetricHandler } from '../../cloudwatch/metric-handler'; -const logMock = jest.spyOn(global.console, 'log').mockImplementation(); +const logMock = jest.spyOn(globalThis.console, 'log').mockImplementation(); const dimensions = [ { diff --git a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts index 1a7a7c48b..381299272 100644 --- a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts +++ b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts @@ -78,7 +78,11 @@ describe('Event Publishing', () => { expect(result).toEqual([]); expect(eventBridgeMock.calls()).toHaveLength(1); expect(sqsMock.calls()).toHaveLength(0); - expect(metricHandlerMock.addMetrics).toHaveBeenCalledWith(['uk.nhs.notify.digital.letters.sent.v1_batchSuccess', 'Count', 2]); + expect(metricHandlerMock.addMetrics).toHaveBeenCalledWith([ + 'uk.nhs.notify.digital.letters.sent.v1_batchSuccess', + 'Count', + 2, + ]); const eventBridgeCall = eventBridgeMock.calls()[0]; expect(eventBridgeCall.args[0].input).toEqual({ @@ -160,8 +164,16 @@ describe('Event Publishing', () => { const sqsInput = sqsCall.args[0].input as any; expect(sqsInput.Entries).toHaveLength(1); expect(sqsInput.Entries[0].MessageBody).toBe(JSON.stringify(event)); - expect(metricHandlerMock.addMetrics).toHaveBeenNthCalledWith(1,['uk.nhs.notify.digital.letters.sent.v1_batchSuccess', 'Count', 1]); - expect(metricHandlerMock.addMetrics).toHaveBeenNthCalledWith(2,['uk.nhs.notify.digital.letters.sent.v1_batchFailure', 'Count', 1]); + expect(metricHandlerMock.addMetrics).toHaveBeenNthCalledWith(1, [ + 'uk.nhs.notify.digital.letters.sent.v1_batchSuccess', + 'Count', + 1, + ]); + expect(metricHandlerMock.addMetrics).toHaveBeenNthCalledWith(2, [ + 'uk.nhs.notify.digital.letters.sent.v1_batchFailure', + 'Count', + 1, + ]); }); test('should handle EventBridge send error and send all events to DLQ', async () => { diff --git a/utils/utils/src/cloudwatch/index.ts b/utils/utils/src/cloudwatch/index.ts new file mode 100644 index 000000000..e11c8872b --- /dev/null +++ b/utils/utils/src/cloudwatch/index.ts @@ -0,0 +1 @@ +export * from './metric-handler'; diff --git a/utils/utils/src/cloudwatch/metric-handler.ts b/utils/utils/src/cloudwatch/metric-handler.ts index 9fa36cd00..2b8297a7c 100644 --- a/utils/utils/src/cloudwatch/metric-handler.ts +++ b/utils/utils/src/cloudwatch/metric-handler.ts @@ -39,7 +39,7 @@ export class MetricHandler { constructor( private readonly namespace: string, - private readonly dimensions: Dimension[] + private readonly dimensions: Dimension[], ) {} public addMetrics( @@ -48,12 +48,12 @@ export class MetricHandler { timestamp?: Date; extraDimensions?: Dimension[]; storageResolution?: number; - } = {} + } = {}, ) { const { - timestamp = new Date(), extraDimensions = [], storageResolution = 60, + timestamp = new Date(), } = options; const metrics = ( @@ -91,7 +91,7 @@ export class MetricHandler { } public getChildMetricHandler( - childMetricHandlerDimensions: Dimension[] + childMetricHandlerDimensions: Dimension[], ): MetricHandler { return new MetricHandler(this.namespace, [ ...this.dimensions, diff --git a/utils/utils/src/event-publisher/event-publisher.ts b/utils/utils/src/event-publisher/event-publisher.ts index 16802e224..b110429dd 100644 --- a/utils/utils/src/event-publisher/event-publisher.ts +++ b/utils/utils/src/event-publisher/event-publisher.ts @@ -60,7 +60,6 @@ export class EventPublisher { this.eventBridge = config.eventBridgeClient; this.sqs = config.sqsClient; this.metricHandler = config.metricHandler; - } private async sendToEventBridge( @@ -96,18 +95,11 @@ export class EventPublisher { this.logger.info({ description: 'EventBridge batch sent', batchSize: batch.length, - failedEntryCount: failedEntryCount, - successfulCount: successfulCount, + failedEntryCount, + successfulCount, }); - if (successfulCount > 0) { - this.metricHandler.addMetrics([`${entries[0].DetailType}_batchSuccess`, 'Count', successfulCount]); - } - - if (failedEntryCount > 0) { - this.metricHandler.addMetrics([`${entries[0].DetailType}_batchFailure`, 'Count', failedEntryCount]); - } - + this.recordMetrics(successfulCount, entries, failedEntryCount); if (failedEntryCount && response.Entries) { for (const [idx, entry] of response.Entries.entries()) { @@ -135,6 +127,33 @@ export class EventPublisher { return failedEvents; } + private recordMetrics( + successfulCount: number, + entries: { + Source: string; + DetailType: string; + Detail: string; + EventBusName: string; + }[], + failedEntryCount: number, + ) { + if (successfulCount > 0) { + this.metricHandler.addMetrics([ + `${entries[0].DetailType}_batchSuccess`, + 'Count', + successfulCount, + ]); + } + + if (failedEntryCount > 0) { + this.metricHandler.addMetrics([ + `${entries[0].DetailType}_batchFailure`, + 'Count', + failedEntryCount, + ]); + } + } + private async sendToDLQ( events: T[], reason: DlqReason, diff --git a/utils/utils/src/index.ts b/utils/utils/src/index.ts index 4b9333bc9..b8571fbee 100644 --- a/utils/utils/src/index.ts +++ b/utils/utils/src/index.ts @@ -16,3 +16,4 @@ export * from './key-generation-utils'; export * from './schema-utils'; export * from './pdm-client'; export * from './reporting'; +export * from './cloudwatch'; From 418d89031ce39aaaa2ae98302bd1905c7ba3e7e2 Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Tue, 28 Apr 2026 15:08:33 +0100 Subject: [PATCH 3/9] CCM-17116: replacing console.log as its wrapped by winston --- .../src/__tests__/cloudwatch/metric-handler.test.ts | 10 +++++----- utils/utils/src/cloudwatch/metric-handler.ts | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts b/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts index 013af1e16..7ab1ec5fc 100644 --- a/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts +++ b/utils/utils/src/__tests__/cloudwatch/metric-handler.test.ts @@ -1,6 +1,6 @@ import { MetricHandler } from '../../cloudwatch/metric-handler'; -const logMock = jest.spyOn(globalThis.console, 'log').mockImplementation(); +const logMock = jest.spyOn(process.stdout, 'write').mockImplementation(); const dimensions = [ { @@ -34,7 +34,7 @@ it('puts metric data without timestamp', () => { expect(logMock).toHaveBeenCalledTimes(1); - const lastCalledWith = logMock.mock.calls[0][0]; + const lastCalledWith = logMock.mock.calls[0][0] as string; expect(JSON.parse(lastCalledWith)).toEqual({ _aws: { @@ -66,7 +66,7 @@ it('logs multiple metrics', () => { expect(logMock).toHaveBeenCalledTimes(1); - const calledWith = logMock.mock.calls[0][0]; + const calledWith = logMock.mock.calls[0][0] as string; expect(JSON.parse(calledWith)).toEqual({ _aws: { @@ -103,7 +103,7 @@ it('puts metric data with timestamp', () => { expect(logMock).toHaveBeenCalledTimes(1); - const lastCalledWith = logMock.mock.calls[0][0]; + const lastCalledWith = logMock.mock.calls[0][0] as string; expect(JSON.parse(lastCalledWith)).toEqual({ _aws: { @@ -142,7 +142,7 @@ it('generates child metric handler', () => { expect(logMock).toHaveBeenCalledTimes(1); - const lastCalledWith = logMock.mock.calls[0][0]; + const lastCalledWith = logMock.mock.calls[0][0] as string; expect(JSON.parse(lastCalledWith)).toEqual({ _aws: { diff --git a/utils/utils/src/cloudwatch/metric-handler.ts b/utils/utils/src/cloudwatch/metric-handler.ts index 2b8297a7c..d0c523443 100644 --- a/utils/utils/src/cloudwatch/metric-handler.ts +++ b/utils/utils/src/cloudwatch/metric-handler.ts @@ -86,8 +86,7 @@ export class MetricHandler { ...dimensions, ...Object.fromEntries(metrics.map(([name, , value]) => [name, value])), }; - // eslint-disable-next-line no-console - console.log(JSON.stringify(metric)); + process.stdout.write(JSON.stringify(metric)); } public getChildMetricHandler( From 0a2043ab234479039fc65daa530dc493df0a8184 Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Tue, 28 Apr 2026 17:26:07 +0100 Subject: [PATCH 4/9] CCM-17116: Adding metric for total senders --- .../apis/scheduled-event-handler.test.ts | 19 ++++++++++++++++++- .../src/apis/scheduled-event-handler.ts | 5 ++++- lambdas/report-scheduler/src/container.ts | 10 ++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts index 83abed366..f0b5d0845 100644 --- a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts +++ b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts @@ -1,4 +1,4 @@ -import { EventPublisher, Logger, Sender } from 'utils'; +import { EventPublisher, Logger, MetricHandler, Sender } from 'utils'; import { ISenderManagement } from 'sender-management'; import { GenerateReport, validateGenerateReport } from 'digital-letters-events'; import { createHandler } from 'apis/scheduled-event-handler'; @@ -7,6 +7,7 @@ describe('scheduled-event-handler', () => { let mockSenderManagement: jest.Mocked; let mockEventPublisher: jest.Mocked; let mockLogger: jest.Mocked; + let mockMetricHandler: jest.Mocked; beforeEach(() => { mockSenderManagement = { @@ -24,6 +25,10 @@ describe('scheduled-event-handler', () => { child: jest.fn().mockReturnThis(), } as unknown as jest.Mocked; + mockMetricHandler = { + addMetrics: jest.fn(), + } as unknown as jest.Mocked; + jest.useFakeTimers(); }); @@ -39,11 +44,13 @@ describe('scheduled-event-handler', () => { const handler = createHandler({ senderManagement: mockSenderManagement, eventPublisher: mockEventPublisher, + metricHandler: mockMetricHandler, }); await handler(); expect(mockSenderManagement.listSenders).toHaveBeenCalledTimes(1); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledTimes(1); }); it('should publish generate report events for each sender', async () => { @@ -62,6 +69,7 @@ describe('scheduled-event-handler', () => { const handler = createHandler({ senderManagement: mockSenderManagement, eventPublisher: mockEventPublisher, + metricHandler: mockMetricHandler, }); await handler(); @@ -71,6 +79,7 @@ describe('scheduled-event-handler', () => { expect(events).toHaveLength(3); expect(validator).toBeDefined(); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 3]); }); it('should create events with correct structure for each sender', async () => { @@ -87,6 +96,7 @@ describe('scheduled-event-handler', () => { const handler = createHandler({ senderManagement: mockSenderManagement, eventPublisher: mockEventPublisher, + metricHandler: mockMetricHandler, }); await handler(); @@ -115,6 +125,7 @@ describe('scheduled-event-handler', () => { expect(event.datacontenttype).toBe('application/json'); expect(() => validateGenerateReport(event, mockLogger)).not.toThrow(); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 1]); }); it('should handle empty sender list', async () => { @@ -124,12 +135,14 @@ describe('scheduled-event-handler', () => { const handler = createHandler({ senderManagement: mockSenderManagement, eventPublisher: mockEventPublisher, + metricHandler: mockMetricHandler, }); await handler(); const [[events]] = mockEventPublisher.sendEvents.mock.calls; expect(events).toHaveLength(0); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 0]); }); it('should handle event publisher errors', async () => { @@ -142,9 +155,11 @@ describe('scheduled-event-handler', () => { const handler = createHandler({ senderManagement: mockSenderManagement, eventPublisher: mockEventPublisher, + metricHandler: mockMetricHandler, }); await expect(handler()).rejects.toThrow('Failed to publish events'); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledTimes(0); }); it('should generate unique event IDs for multiple senders', async () => { @@ -159,6 +174,7 @@ describe('scheduled-event-handler', () => { const handler = createHandler({ senderManagement: mockSenderManagement, eventPublisher: mockEventPublisher, + metricHandler: mockMetricHandler, }); await handler(); @@ -167,6 +183,7 @@ describe('scheduled-event-handler', () => { const eventIds = events.map((e) => e.id); expect(new Set(eventIds).size).toBe(eventIds.length); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 2]); }); }); }); diff --git a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts index 506b64853..1d381d2c7 100644 --- a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts +++ b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts @@ -1,4 +1,4 @@ -import { EventPublisher } from 'utils'; +import { EventPublisher, MetricHandler } from 'utils'; import { ISenderManagement } from 'sender-management'; import { GenerateReport, validateGenerateReport } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; @@ -6,11 +6,13 @@ import { randomUUID } from 'node:crypto'; export type CreateHandlerDependencies = { senderManagement: ISenderManagement; eventPublisher: EventPublisher; + metricHandler: MetricHandler; }; export const createHandler = ({ eventPublisher, senderManagement, + metricHandler, }: CreateHandlerDependencies) => { return async () => { const yesterday = new Date(); @@ -43,5 +45,6 @@ export const createHandler = ({ })), validateGenerateReport, ); + metricHandler.addMetrics(['TotalSenders', 'Count', senders.length]); }; }; diff --git a/lambdas/report-scheduler/src/container.ts b/lambdas/report-scheduler/src/container.ts index aa0af5ffa..8a79b39ad 100644 --- a/lambdas/report-scheduler/src/container.ts +++ b/lambdas/report-scheduler/src/container.ts @@ -23,18 +23,20 @@ export const createContainer = (): CreateHandlerDependencies => { parameterStore, }); + const metricHandler = new MetricHandler(dlMetricsNamespace, [ + { Name: 'Environment', Value: environment }, + ]); + const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, dlqUrl: eventPublisherDlqUrl, logger, sqsClient, eventBridgeClient, - metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, - ]), + metricHandler, }); - return { senderManagement, eventPublisher }; + return { senderManagement, eventPublisher, metricHandler }; }; export default createContainer; From ae40d8781178d0687c0954064446b76c2f45eeeb Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Tue, 28 Apr 2026 17:28:49 +0100 Subject: [PATCH 5/9] CCM-17116: Adding metric for total senders --- .../apis/scheduled-event-handler.test.ts | 24 +- .../src/apis/scheduled-event-handler.ts | 2 +- lambdas/report-scheduler/src/container.ts | 4 +- package-lock.json | 863 +++++++++--------- 4 files changed, 474 insertions(+), 419 deletions(-) diff --git a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts index f0b5d0845..cd143a7d5 100644 --- a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts +++ b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts @@ -79,7 +79,11 @@ describe('scheduled-event-handler', () => { expect(events).toHaveLength(3); expect(validator).toBeDefined(); - expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 3]); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith([ + 'TotalSenders', + 'Count', + 3, + ]); }); it('should create events with correct structure for each sender', async () => { @@ -125,7 +129,11 @@ describe('scheduled-event-handler', () => { expect(event.datacontenttype).toBe('application/json'); expect(() => validateGenerateReport(event, mockLogger)).not.toThrow(); - expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 1]); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith([ + 'TotalSenders', + 'Count', + 1, + ]); }); it('should handle empty sender list', async () => { @@ -142,7 +150,11 @@ describe('scheduled-event-handler', () => { const [[events]] = mockEventPublisher.sendEvents.mock.calls; expect(events).toHaveLength(0); - expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 0]); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith([ + 'TotalSenders', + 'Count', + 0, + ]); }); it('should handle event publisher errors', async () => { @@ -183,7 +195,11 @@ describe('scheduled-event-handler', () => { const eventIds = events.map((e) => e.id); expect(new Set(eventIds).size).toBe(eventIds.length); - expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith(['TotalSenders', 'Count', 2]); + expect(mockMetricHandler.addMetrics).toHaveBeenCalledWith([ + 'TotalSenders', + 'Count', + 2, + ]); }); }); }); diff --git a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts index 1d381d2c7..d85d37635 100644 --- a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts +++ b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts @@ -11,8 +11,8 @@ export type CreateHandlerDependencies = { export const createHandler = ({ eventPublisher, - senderManagement, metricHandler, + senderManagement, }: CreateHandlerDependencies) => { return async () => { const yesterday = new Date(); diff --git a/lambdas/report-scheduler/src/container.ts b/lambdas/report-scheduler/src/container.ts index 8a79b39ad..8ccc3b770 100644 --- a/lambdas/report-scheduler/src/container.ts +++ b/lambdas/report-scheduler/src/container.ts @@ -24,8 +24,8 @@ export const createContainer = (): CreateHandlerDependencies => { }); const metricHandler = new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, - ]); + { Name: 'Environment', Value: environment }, + ]); const eventPublisher = new EventPublisher({ eventBusArn: eventPublisherEventBusArn, diff --git a/package-lock.json b/package-lock.json index 5d42e7379..f4783935a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5766,51 +5766,51 @@ } }, "node_modules/@aws-sdk/client-cloudwatch": { - "version": "3.984.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.984.0.tgz", - "integrity": "sha512-qkf1lcKBsxsacE7R4tNXtIsnOCBKYq+R+ONjo3GX/MowZv4J9rYUBUq3G6Hw3sHQYnpPoJXBfBj6dCY6wYUAnw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.1038.0.tgz", + "integrity": "sha512-n/6aUdWXZ3AUtjZ0ONqq1v1THYHyXKdz4uUfmfEH5CdeBFShPpgN9DFegcresKHixAmL9gAYmS+k5i+NyyuKlw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/credential-provider-node": "^3.972.5", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.984.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-compression": "^4.3.27", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", - "@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.11.1", - "@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.28", - "@smithy/util-defaults-mode-node": "^4.2.31", - "@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", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.22", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-compression": "^4.3.46", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.6", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.5", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -5887,15 +5887,15 @@ } }, "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { - "version": "3.984.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", - "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", "tslib": "^2.6.2" }, "engines": { @@ -6375,23 +6375,24 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.15.tgz", - "integrity": "sha512-AlC0oQ1/mdJ8vCIqu524j5RB7M8i8E24bbkZmya1CuiQxkY7SdIZAyw7NDNMGaNINQFq/8oGRMX0HeOfCVsl/A==", + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.6.tgz", + "integrity": "sha512-8Vu7zGxu+39ChR/s5J7nXBw3a2kMHAi0OfKT8ohgTVjX0qYed/8mIfdBb638oBmKrWCwwKjYAM5J/4gMJ8nAJA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/xml-builder": "^3.972.8", - "@smithy/core": "^3.23.6", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/signature-v4": "^5.3.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.20", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.5", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -6412,15 +6413,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.13.tgz", - "integrity": "sha512-6ljXKIQ22WFKyIs1jbORIkGanySBHaPPTOI4OxACP5WXgbcR0nDYfqNJfXEGwCK7IzHdNbCSFsNKKs0qCexR8Q==", + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.32.tgz", + "integrity": "sha512-7vA4GHg8NSmQxquJHSBcSM3RgB4ZaaRi6u4+zGFKOmOH6aqlgr2Sda46clkZDYzlirgfY96w15Zj0jh6PT48ng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6428,20 +6429,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.15.tgz", - "integrity": "sha512-dJuSTreu/T8f24SHDNTjd7eQ4rabr0TzPh2UTCwYexQtzG3nTDKm1e5eIdhiroTMDkPEJeY+WPkA6F9wod/20A==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.34.tgz", + "integrity": "sha512-vBrhWujFCLp1u8ptJRWYlipMutzPptb8pDQ00rKVH9q67T7rGd3VTWIj63aKrlLuY6qSsw1Rt5F/D/7wnNgryA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/types": "^3.973.4", - "@smithy/fetch-http-handler": "^5.3.11", - "@smithy/node-http-handler": "^4.4.12", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.15", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -6449,24 +6450,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.13.tgz", - "integrity": "sha512-JKSoGb7XeabZLBJptpqoZIFbROUIS65NuQnEHGOpuT9GuuZwag2qciKANiDLFiYk4u8nSrJC9JIOnWKVvPVjeA==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.36.tgz", + "integrity": "sha512-FBHyCmV8EB0gUvh1d+CZm87zt2PrdC7OyWexLRoH3I5zWSOUGa+9t58Y5jbxRfwUp3AWpHAFvKY6YzgR845sVA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/credential-provider-env": "^3.972.13", - "@aws-sdk/credential-provider-http": "^3.972.15", - "@aws-sdk/credential-provider-login": "^3.972.13", - "@aws-sdk/credential-provider-process": "^3.972.13", - "@aws-sdk/credential-provider-sso": "^3.972.13", - "@aws-sdk/credential-provider-web-identity": "^3.972.13", - "@aws-sdk/nested-clients": "^3.996.3", - "@aws-sdk/types": "^3.973.4", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-login": "^3.972.36", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6474,18 +6475,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.13.tgz", - "integrity": "sha512-RtYcrxdnJHKY8MFQGLltCURcjuMjnaQpAxPE6+/QEdDHHItMKZgabRe/KScX737F9vJMQsmJy9EmMOkCnoC1JQ==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.36.tgz", + "integrity": "sha512-IFap01lJKxQc0C/OHmZwZQr/cKq0DhrcmKedRrdnnl42D+P0SImnnnWQjv07uIPqpEdtqmkPXb9TiPYTU+prxQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/nested-clients": "^3.996.3", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6493,22 +6494,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.14.tgz", - "integrity": "sha512-WqoC2aliIjQM/L3oFf6j+op/enT2i9Cc4UTxxMEKrJNECkq4/PlKE5BOjSYFcq6G9mz65EFbXJh7zOU4CvjSKQ==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.37.tgz", + "integrity": "sha512-/WFixFAAiw8WpmjZcI0l4t3DerXLmVinOIfuotmRZnu2qmsFPoqqmstASz0z8bi1pGdFXzeLzf6bwucM3mZcUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.13", - "@aws-sdk/credential-provider-http": "^3.972.15", - "@aws-sdk/credential-provider-ini": "^3.972.13", - "@aws-sdk/credential-provider-process": "^3.972.13", - "@aws-sdk/credential-provider-sso": "^3.972.13", - "@aws-sdk/credential-provider-web-identity": "^3.972.13", - "@aws-sdk/types": "^3.973.4", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-ini": "^3.972.36", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6516,16 +6517,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.13.tgz", - "integrity": "sha512-rsRG0LQA4VR+jnDyuqtXi2CePYSmfm5GNL9KxiW8DSe25YwJSr06W8TdUfONAC+rjsTI+aIH2rBGG5FjMeANrw==", + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.32.tgz", + "integrity": "sha512-uZp4tlGbpczV8QxmtIwOpSkcyGtBRR8/T4BAumRKfAt1nwCig3FSCZvrKl6ARDIDVRYn5p2oRcAsfFR01EgMGA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6533,18 +6534,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.13.tgz", - "integrity": "sha512-fr0UU1wx8kNHDhTQBXioc/YviSW8iXuAxHvnH7eQUtn8F8o/FU3uu6EUMvAQgyvn7Ne5QFnC0Cj0BFlwCk+RFw==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.36.tgz", + "integrity": "sha512-DsLr0UHMyKzRJKe2bjlwU8q1cfoXg8TIJKV/xwvnalAemiZLOZunFzj/whGnFDZIBVLdnbLiwv5SvRf1+CSwkg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/nested-clients": "^3.996.3", - "@aws-sdk/token-providers": "3.999.0", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/token-providers": "3.1038.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6552,17 +6553,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.13.tgz", - "integrity": "sha512-a6iFMh1pgUH0TdcouBppLJUfPM7Yd3R9S1xFodPtCRoLqCz2RQFA3qjA8x4112PVYXEd4/pHX2eihapq39w0rA==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.36.tgz", + "integrity": "sha512-uzrURO7frJhHQVVNR5zBJcCYeMYflmXcWBK1+MiBym2Dfjh6nXATrMixrmGZi+97Q7ETZ+y/4lUwAy0Nfnznjw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/nested-clients": "^3.996.3", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6693,14 +6694,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.6.tgz", - "integrity": "sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6722,13 +6723,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.6.tgz", - "integrity": "sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6736,15 +6737,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.6.tgz", - "integrity": "sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", + "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6752,24 +6753,24 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.15.tgz", - "integrity": "sha512-WDLgssevOU5BFx1s8jA7jj6cE5HuImz28sy9jKOaVtz0AW1lYqSzotzdyiybFaBcQTs5zxXOb2pUfyMxgEKY3Q==", + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.35.tgz", + "integrity": "sha512-lLppaNTAz+wNgLdi4FtHzrlwrGF0ODTnBWHBaFg85SKs0eJ+M+tP5ifrA8f/0lNd+Ak3MC1NGC6RavV3ny4HTg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/util-arn-parser": "^3.972.2", - "@smithy/core": "^3.23.6", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/signature-v4": "^5.3.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", - "@smithy/util-config-provider": "^4.2.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-stream": "^4.5.15", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -6808,17 +6809,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.15.tgz", - "integrity": "sha512-ABlFVcIMmuRAwBT+8q5abAxOr7WmaINirDJBnqGY5b5jSDo00UMlg/G4a0xoAgwm6oAECeJcwkvDlxDwKf58fQ==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.36.tgz", + "integrity": "sha512-O2beToxguBvrZFFZ+fFgPbbae8MvyIBjQ6lImee4APHEXXNAD5ZJ2ayLF1mb7rsKw86TM81y5czg82bZncjSjg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/util-endpoints": "^3.996.3", - "@smithy/core": "^3.23.6", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.5", "tslib": "^2.6.2" }, "engines": { @@ -6826,15 +6828,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz", - "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==", + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", - "@smithy/util-endpoints": "^3.3.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", "tslib": "^2.6.2" }, "engines": { @@ -6842,48 +6844,66 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.3.tgz", - "integrity": "sha512-AU5TY1V29xqwg/MxmA2odwysTez+ccFAhmfRJk+QZT5HNv90UTA9qKd1J9THlsQkvmH7HWTEV1lDNxkQO5PzNw==", + "version": "3.997.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.4.tgz", + "integrity": "sha512-4Sf+WY1lMJzXlw5MiyCMe/UzdILCwvuaHThbqMXS6dfh9gZy3No360I42RXquOI/ULUOhWy2HCyU0Fp20fQGPQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/middleware-host-header": "^3.972.6", - "@aws-sdk/middleware-logger": "^3.972.6", - "@aws-sdk/middleware-recursion-detection": "^3.972.6", - "@aws-sdk/middleware-user-agent": "^3.972.15", - "@aws-sdk/region-config-resolver": "^3.972.6", - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/util-endpoints": "^3.996.3", - "@aws-sdk/util-user-agent-browser": "^3.972.6", - "@aws-sdk/util-user-agent-node": "^3.973.0", - "@smithy/config-resolver": "^4.4.9", - "@smithy/core": "^3.23.6", - "@smithy/fetch-http-handler": "^5.3.11", - "@smithy/hash-node": "^4.2.10", - "@smithy/invalid-dependency": "^4.2.10", - "@smithy/middleware-content-length": "^4.2.10", - "@smithy/middleware-endpoint": "^4.4.20", - "@smithy/middleware-retry": "^4.4.37", - "@smithy/middleware-serde": "^4.2.11", - "@smithy/middleware-stack": "^4.2.10", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/node-http-handler": "^4.4.12", - "@smithy/protocol-http": "^5.3.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-body-length-browser": "^4.2.1", - "@smithy/util-body-length-node": "^4.2.2", - "@smithy/util-defaults-mode-browser": "^4.3.36", - "@smithy/util-defaults-mode-node": "^4.2.39", - "@smithy/util-endpoints": "^3.3.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-retry": "^4.2.10", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.23", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.22", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.6", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.5", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.23.tgz", + "integrity": "sha512-wBbys3Y53Ikly556vyADurKpYQHXS7Jjaskbz+Ga9PZCz7PB/9f3VdKbDlz7dqIzn+xwz7L/a6TR4iXcOi8IRw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.35", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6891,15 +6911,15 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz", - "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==", + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", - "@smithy/util-endpoints": "^3.3.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", "tslib": "^2.6.2" }, "engines": { @@ -6907,15 +6927,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.6.tgz", - "integrity": "sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@smithy/config-resolver": "^4.4.9", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6938,17 +6958,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.999.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.999.0.tgz", - "integrity": "sha512-cx0hHUlgXULfykx4rdu/ciNAJaa3AL5xz3rieCz7NKJ68MJwlj3664Y8WR5MGgxfyYJBdamnkjNSx5Kekuc0cg==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1038.0.tgz", + "integrity": "sha512-Qniru+9oGGb/HNK/gGZWbV3jsD0k71ngE7qMQ/x6gYNYLd2EOwHCS6E2E6jfkaqO4i0d+nNKmfRy8bNcshKdGQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.15", - "@aws-sdk/nested-clients": "^3.996.3", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6956,12 +6976,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", - "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -6969,7 +6989,9 @@ } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.2", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7016,27 +7038,28 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.6.tgz", - "integrity": "sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.0.tgz", - "integrity": "sha512-A9J2G4Nf236e9GpaC1JnA8wRn6u6GjnOXiTwBLA6NUJhlBTIGfrTy+K1IazmF8y+4OFdW3O5TZlhyspJMqiqjA==", + "version": "3.973.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.22.tgz", + "integrity": "sha512-YTYqTmOUrwbm1h99Ee4y/mVYpFRl0oSO/amtP5cc1BZZWdaAVWs9zj3TkyRHWvR9aI/ZS8m3mS6awXtYUlWyaw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.15", - "@aws-sdk/types": "^3.973.4", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/middleware-user-agent": "^3.972.36", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -7052,13 +7075,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", - "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.21.tgz", + "integrity": "sha512-qxNiHUtlrsjTeSlrPWiFkWps7uD6YB4eKzg7eLAFH8jbiHTlt0ePNlo2Xu+WlftP38JIcMaIX4jTUjOlE2ySWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.5.8", + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" }, "engines": { @@ -9925,6 +9949,18 @@ "zod": "^4.1.11" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -10613,16 +10649,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.9.tgz", - "integrity": "sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q==", + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-config-provider": "^4.2.1", - "@smithy/util-endpoints": "^3.3.1", - "@smithy/util-middleware": "^4.2.10", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -10651,15 +10687,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.10.tgz", - "integrity": "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -10768,14 +10804,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.10.tgz", - "integrity": "sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", - "@smithy/util-buffer-from": "^4.2.1", - "@smithy/util-utf8": "^4.2.1", + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -10797,12 +10833,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.10.tgz", - "integrity": "sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10857,13 +10893,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.10.tgz", - "integrity": "sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10871,18 +10907,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.20", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.20.tgz", - "integrity": "sha512-9W6Np4ceBP3XCYAGLoMCmn8t2RRVzuD1ndWPLBbv7H9CrwM9Bprf6Up6BM9ZA/3alodg0b7Kf6ftBK9R1N04vw==", + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.6", - "@smithy/middleware-serde": "^4.2.11", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", - "@smithy/util-middleware": "^4.2.10", + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -10890,19 +10926,20 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.37", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.37.tgz", - "integrity": "sha512-/1psZZllBBSQ7+qo5+hhLz7AEPGLx3Z0+e3ramMBEuPK2PfvLK4SrncDB9VegX5mBn+oP/UTDrM6IHrFjvX1ZA==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/service-error-classification": "^4.2.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-retry": "^4.2.10", - "@smithy/uuid": "^1.1.1", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -10910,13 +10947,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.11.tgz", - "integrity": "sha512-STQdONGPwbbC7cusL60s7vOa6He6A9w2jWhoapL0mgVjmR19pr26slV+yoSP76SIssMTX/95e5nOZ6UQv6jolg==", + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -10924,12 +10962,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.10.tgz", - "integrity": "sha512-pmts/WovNcE/tlyHa8z/groPeOtqtEpp61q3W0nW1nDJuMq/x+hWa/OVQBtgU0tBqupeXq0VBOLA4UZwE8I0YA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11020,12 +11058,12 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.10.tgz", - "integrity": "sha512-0R/+/Il5y8nB/By90o8hy/bWVYptbIfvoTYad0igYQO5RefhNCDmNzqxaMx7K1t/QWo0d6UynqpqN5cCQt1MCg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0" + "@smithy/types": "^4.14.1" }, "engines": { "node": ">=18.0.0" @@ -11045,18 +11083,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.10.tgz", - "integrity": "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.1", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-hex-encoding": "^4.2.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-uri-escape": "^4.2.1", - "@smithy/util-utf8": "^4.2.1", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -11064,17 +11102,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.0.tgz", - "integrity": "sha512-R8bQ9K3lCcXyZmBnQqUZJF4ChZmtWT5NLi6x5kgWx5D+/j0KorXcA0YcFg/X5TOgnTCy1tbKc6z2g2y4amFupQ==", + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.6", - "@smithy/middleware-endpoint": "^4.4.20", - "@smithy/middleware-stack": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.15", + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -11134,9 +11172,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.2.tgz", - "integrity": "sha512-4rHqBvxtJEBvsZcFQSPQqXP2b/yy/YlB66KlcEgcH2WNoOKCKB03DSLzXmOsXjbl8dJ4OEYTn31knhdznwk7zw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11171,14 +11209,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.36", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.36.tgz", - "integrity": "sha512-R0smq7EHQXRVMxkAxtH5akJ/FvgAmNF6bUy/GwY/N20T4GrwjT633NFm0VuRpC+8Bbv8R9A0DoJ9OiZL/M3xew==", + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11186,17 +11224,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.39", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.39.tgz", - "integrity": "sha512-otWuoDm35btJV1L8MyHrPl462B07QCdMTktKc7/yM+Psv6KbED/ziXiHnmr7yPHUjfIwE9S8Max0LO24Mo3ZVg==", + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.9", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/smithy-client": "^4.12.0", - "@smithy/types": "^4.13.0", + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11204,13 +11242,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.1.tgz", - "integrity": "sha512-xyctc4klmjmieQiF9I1wssBWleRV0RhJ2DpO8+8yzi2LO1Z+4IWOZNGZGNj4+hq9kdo+nyfrRLmQTzc16Op2Vg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.10", - "@smithy/types": "^4.13.0", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11243,13 +11281,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.10.tgz", - "integrity": "sha512-HrBzistfpyE5uqTwiyLsFHscgnwB0kgv8vySp7q5kZ0Eltn/tjosaSGGDj/jJ9ys7pWzIP/icE2d+7vMKXLv7A==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz", + "integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.10", - "@smithy/types": "^4.13.0", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -11301,13 +11339,12 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.10.tgz", - "integrity": "sha512-4eTWph/Lkg1wZEDAyObwme0kmhEb7J/JjibY2znJdrYRgKbKqB7YoEhhJVJ4R1g/SYih4zuwX7LpJaM8RsnTVg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.10", - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -16318,9 +16355,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", "funding": [ { "type": "github", @@ -16333,9 +16370,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -16344,9 +16381,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -22547,9 +22585,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", - "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -24530,9 +24568,9 @@ } }, "node_modules/strnum": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", - "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", "funding": [ { "type": "github", @@ -26102,6 +26140,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", From 287b3c26700297cc066708706f9370b0d810fee9 Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Wed, 29 Apr 2026 14:57:46 +0100 Subject: [PATCH 6/9] CCM-17116: Adding dimensions --- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/core-notifier-lambda/src/container.ts | 11 ++++--- lambdas/file-scanner-lambda/src/container.ts | 11 ++++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ .../src/container.ts | 11 ++++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ .../nhsapp-status-handler/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/pdm-poll-lambda/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/pdm-uploader-lambda/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/print-analyser/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/print-sender-lambda/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/print-status-handler/src/container.ts | 8 ++++- lambdas/report-generator/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/report-scheduler/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ lambdas/ttl-create-lambda/src/container.ts | 8 ++++- .../src/__tests__/container.test.ts | 22 +++++++++++++ .../src/__tests__/index.test.ts | 22 +++++++++++++ .../ttl-handle-expiry-lambda/src/container.ts | 8 ++++- utils/utils/src/cloudwatch/metric-handler.ts | 32 +++++++++++++++++++ 26 files changed, 393 insertions(+), 16 deletions(-) diff --git a/lambdas/core-notifier-lambda/src/__tests__/container.test.ts b/lambdas/core-notifier-lambda/src/__tests__/container.test.ts index bf1fcd19a..19e961e61 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/container.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/container.test.ts @@ -22,6 +22,28 @@ jest.mock('utils', () => ({ MetricHandler: jest.fn(), eventBridgeClient: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); jest.mock('app/notify-api-client'); diff --git a/lambdas/core-notifier-lambda/src/container.ts b/lambdas/core-notifier-lambda/src/container.ts index 2540b7865..9a6d4580b 100644 --- a/lambdas/core-notifier-lambda/src/container.ts +++ b/lambdas/core-notifier-lambda/src/container.ts @@ -1,5 +1,8 @@ import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, ParameterStoreCache, createGetApimAccessToken, @@ -48,10 +51,10 @@ export async function createContainer(): Promise { eventPublisherEventBusArn, } = config; const metricHandler = new MetricHandler(dlMetricsNamespace, [ - { - Name: 'Environment', - Value: config.environment, - }, + { Name: DimensionName.Environment, Value: config.environment }, + { Name: DimensionName.Flow, Value: FlowDimension.DigitalLetter }, + { Name: DimensionName.Component, Value: ComponentDimension.CoreNotify }, + { Name: DimensionName.LambdaFunction, Value: 'core-notifier' }, ]); const eventPublisher = new EventPublisher({ diff --git a/lambdas/file-scanner-lambda/src/container.ts b/lambdas/file-scanner-lambda/src/container.ts index 9b20d4603..af402bc3c 100644 --- a/lambdas/file-scanner-lambda/src/container.ts +++ b/lambdas/file-scanner-lambda/src/container.ts @@ -2,7 +2,10 @@ import { HandlerDependencies } from 'apis/sqs-handler'; import { FileScanner } from 'app/file-scanner'; import { loadConfig } from 'infra/config'; import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, eventBridgeClient, logger, @@ -28,7 +31,13 @@ export const createContainer = (): HandlerDependencies => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.Print }, + { + Name: DimensionName.Component, + Value: ComponentDimension.VirusScanning, + }, + { Name: DimensionName.LambdaFunction, Value: 'file-scanner' }, ]), }); diff --git a/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts b/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts index 3e552c10d..9bd619e76 100644 --- a/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts +++ b/lambdas/move-scanned-files-lambda/src/__tests__/container.test.ts @@ -15,6 +15,28 @@ jest.mock('utils', () => ({ MetricHandler: jest.fn(), eventBridgeClient: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); jest.mock('app/move-file-handler'); diff --git a/lambdas/move-scanned-files-lambda/src/container.ts b/lambdas/move-scanned-files-lambda/src/container.ts index 51508f9de..91e27bf94 100644 --- a/lambdas/move-scanned-files-lambda/src/container.ts +++ b/lambdas/move-scanned-files-lambda/src/container.ts @@ -1,5 +1,8 @@ import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, eventBridgeClient, logger, @@ -25,7 +28,13 @@ export async function createContainer(): Promise { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: config.environment }, + { Name: DimensionName.Environment, Value: config.environment }, + { Name: DimensionName.Flow, Value: FlowDimension.DigitalLetter }, + { + Name: DimensionName.Component, + Value: ComponentDimension.VirusScanning, + }, + { Name: DimensionName.LambdaFunction, Value: 'move-scanned-files' }, ]), }); diff --git a/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts b/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts index 2f9aa0fb1..021b472f3 100644 --- a/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts +++ b/lambdas/nhsapp-status-handler/src/__tests__/container.test.ts @@ -25,6 +25,28 @@ jest.mock('utils', () => ({ eventBridgeClient: {}, logger: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/nhsapp-status-handler/src/container.ts b/lambdas/nhsapp-status-handler/src/container.ts index 4f829ccf1..885567d88 100644 --- a/lambdas/nhsapp-status-handler/src/container.ts +++ b/lambdas/nhsapp-status-handler/src/container.ts @@ -1,5 +1,8 @@ import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, dynamoDocumentClient, eventBridgeClient, @@ -33,7 +36,10 @@ export const createContainer = () => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.DigitalLetter }, + { Name: DimensionName.Component, Value: ComponentDimension.Callbacks }, + { Name: DimensionName.LambdaFunction, Value: 'nhsapp-status-handler' }, ]), }); diff --git a/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts index a8b362e93..11e2690e8 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/container.test.ts @@ -21,6 +21,28 @@ jest.mock('utils', () => ({ ParameterStoreCache: jest.fn(() => ({})), PdmClient: jest.fn(() => ({})), sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/pdm-poll-lambda/src/container.ts b/lambdas/pdm-poll-lambda/src/container.ts index 3efc05781..7e29b00b6 100644 --- a/lambdas/pdm-poll-lambda/src/container.ts +++ b/lambdas/pdm-poll-lambda/src/container.ts @@ -2,7 +2,10 @@ import { HandlerDependencies } from 'apis/sqs-handler'; import { Pdm } from 'app/pdm'; import { loadConfig } from 'infra/config'; import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, ParameterStoreCache, PdmClient, @@ -30,7 +33,10 @@ export const createContainer = (): HandlerDependencies => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.DigitalLetter }, + { Name: DimensionName.Component, Value: ComponentDimension.PDM }, + { Name: DimensionName.LambdaFunction, Value: 'pdm-poll' }, ]), }); diff --git a/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts b/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts index 23a03094f..ec1055787 100644 --- a/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts +++ b/lambdas/pdm-uploader-lambda/src/__tests__/container.test.ts @@ -24,6 +24,28 @@ jest.mock('utils', () => ({ ParameterStoreCache: jest.fn(() => ({})), PdmClient: jest.fn(() => ({})), sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/pdm-uploader-lambda/src/container.ts b/lambdas/pdm-uploader-lambda/src/container.ts index 54c19249a..804312731 100644 --- a/lambdas/pdm-uploader-lambda/src/container.ts +++ b/lambdas/pdm-uploader-lambda/src/container.ts @@ -1,5 +1,8 @@ import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, ParameterStoreCache, PdmClient, @@ -42,7 +45,10 @@ export const createContainer = () => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.DigitalLetter }, + { Name: DimensionName.Component, Value: ComponentDimension.PDM }, + { Name: DimensionName.LambdaFunction, Value: 'pdm-uploader' }, ]), }); diff --git a/lambdas/print-analyser/src/__tests__/container.test.ts b/lambdas/print-analyser/src/__tests__/container.test.ts index 75af881d3..68029e3d4 100644 --- a/lambdas/print-analyser/src/__tests__/container.test.ts +++ b/lambdas/print-analyser/src/__tests__/container.test.ts @@ -15,6 +15,28 @@ jest.mock('utils', () => ({ MetricHandler: jest.fn(() => ({})), logger: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/print-analyser/src/container.ts b/lambdas/print-analyser/src/container.ts index 971ebe65b..b0d91cb60 100644 --- a/lambdas/print-analyser/src/container.ts +++ b/lambdas/print-analyser/src/container.ts @@ -1,7 +1,10 @@ import { HandlerDependencies } from 'apis/sqs-handler'; import { loadConfig } from 'infra/config'; import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, eventBridgeClient, logger, @@ -23,7 +26,10 @@ export const createContainer = (): HandlerDependencies => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.Print }, + { Name: DimensionName.Component, Value: ComponentDimension.PrintRequest }, + { Name: DimensionName.LambdaFunction, Value: 'print-analyser' }, ]), }); diff --git a/lambdas/print-sender-lambda/src/__tests__/container.test.ts b/lambdas/print-sender-lambda/src/__tests__/container.test.ts index ee71cb0b1..75a231bc4 100644 --- a/lambdas/print-sender-lambda/src/__tests__/container.test.ts +++ b/lambdas/print-sender-lambda/src/__tests__/container.test.ts @@ -20,6 +20,28 @@ jest.mock('utils', () => ({ eventBridgeClient: {}, logger: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/print-sender-lambda/src/container.ts b/lambdas/print-sender-lambda/src/container.ts index 540aa5c76..c9a1f76fa 100644 --- a/lambdas/print-sender-lambda/src/container.ts +++ b/lambdas/print-sender-lambda/src/container.ts @@ -1,5 +1,8 @@ import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, eventBridgeClient, logger, @@ -18,7 +21,10 @@ export const createContainer = () => { } = loadConfig(); const metricHandler = new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.Print }, + { Name: DimensionName.Component, Value: ComponentDimension.PrintRequest }, + { Name: DimensionName.LambdaFunction, Value: 'print-sender' }, ]); const eventPublisher = new EventPublisher({ diff --git a/lambdas/print-status-handler/src/__tests__/container.test.ts b/lambdas/print-status-handler/src/__tests__/container.test.ts index 75af881d3..68029e3d4 100644 --- a/lambdas/print-status-handler/src/__tests__/container.test.ts +++ b/lambdas/print-status-handler/src/__tests__/container.test.ts @@ -15,6 +15,28 @@ jest.mock('utils', () => ({ MetricHandler: jest.fn(() => ({})), logger: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/print-status-handler/src/container.ts b/lambdas/print-status-handler/src/container.ts index 971ebe65b..81f7811cc 100644 --- a/lambdas/print-status-handler/src/container.ts +++ b/lambdas/print-status-handler/src/container.ts @@ -1,7 +1,10 @@ import { HandlerDependencies } from 'apis/sqs-handler'; import { loadConfig } from 'infra/config'; import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, eventBridgeClient, logger, @@ -23,7 +26,10 @@ export const createContainer = (): HandlerDependencies => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.Print }, + { Name: DimensionName.Component, Value: ComponentDimension.Callbacks }, + { Name: DimensionName.LambdaFunction, Value: 'print-status-handler' }, ]), }); diff --git a/lambdas/report-generator/src/container.ts b/lambdas/report-generator/src/container.ts index b76b14833..b5b6929fb 100644 --- a/lambdas/report-generator/src/container.ts +++ b/lambdas/report-generator/src/container.ts @@ -1,7 +1,10 @@ import { AthenaDataRepository, AthenaDataRepositoryDependencies, + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, ReportService, S3StorageRepository, @@ -60,7 +63,10 @@ export const createContainer = () => { ); const metricHandler = new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.TrustReporting }, + { Name: DimensionName.Component, Value: ComponentDimension.Reporting }, + { Name: DimensionName.LambdaFunction, Value: 'report-generator' }, ]); const eventPublisher = new EventPublisher({ diff --git a/lambdas/report-scheduler/src/__tests__/container.test.ts b/lambdas/report-scheduler/src/__tests__/container.test.ts index 383769e94..a62c7d92b 100644 --- a/lambdas/report-scheduler/src/__tests__/container.test.ts +++ b/lambdas/report-scheduler/src/__tests__/container.test.ts @@ -20,6 +20,28 @@ jest.mock('utils', () => ({ eventBridgeClient: {}, logger: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/report-scheduler/src/container.ts b/lambdas/report-scheduler/src/container.ts index 8ccc3b770..4b5a57920 100644 --- a/lambdas/report-scheduler/src/container.ts +++ b/lambdas/report-scheduler/src/container.ts @@ -2,7 +2,10 @@ import { loadConfig } from 'infra/config'; import { CreateHandlerDependencies } from 'apis/scheduled-event-handler'; import { SenderManagement } from 'sender-management'; import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, ParameterStoreCache, eventBridgeClient, @@ -24,7 +27,10 @@ export const createContainer = (): CreateHandlerDependencies => { }); const metricHandler = new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.TrustReporting }, + { Name: DimensionName.Component, Value: ComponentDimension.Reporting }, + { Name: DimensionName.LambdaFunction, Value: 'report-scheduler' }, ]); const eventPublisher = new EventPublisher({ diff --git a/lambdas/ttl-create-lambda/src/__tests__/container.test.ts b/lambdas/ttl-create-lambda/src/__tests__/container.test.ts index 797f258f4..c7566f85d 100644 --- a/lambdas/ttl-create-lambda/src/__tests__/container.test.ts +++ b/lambdas/ttl-create-lambda/src/__tests__/container.test.ts @@ -31,6 +31,28 @@ jest.mock('utils', () => ({ logger: {}, sqsClient: {}, ParameterStoreCache: jest.fn(() => ({})), + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('container', () => { diff --git a/lambdas/ttl-create-lambda/src/container.ts b/lambdas/ttl-create-lambda/src/container.ts index ae401ef2c..e830eec63 100644 --- a/lambdas/ttl-create-lambda/src/container.ts +++ b/lambdas/ttl-create-lambda/src/container.ts @@ -1,5 +1,8 @@ import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, ParameterStoreCache, dynamoClient, @@ -46,7 +49,10 @@ export const createContainer = () => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.DigitalLetter }, + { Name: DimensionName.Component, Value: ComponentDimension.TTL }, + { Name: DimensionName.LambdaFunction, Value: 'ttl-create' }, ]), }); diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts index f2be8c30f..623076c00 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/container.test.ts @@ -8,6 +8,28 @@ jest.mock('utils', () => ({ eventBridgeClient: {}, logger: {}, sqsClient: {}, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); jest.mock('infra/config', () => ({ diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts index e1d5d9546..5d846b315 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/index.test.ts @@ -21,6 +21,28 @@ jest.mock('utils', () => ({ return 'test-value'; }), }, + DimensionName: { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', + }, + FlowDimension: { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', + }, + ComponentDimension: { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', + }, })); describe('index module integration', () => { diff --git a/lambdas/ttl-handle-expiry-lambda/src/container.ts b/lambdas/ttl-handle-expiry-lambda/src/container.ts index 2d95e23cc..286a1714b 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/container.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/container.ts @@ -1,5 +1,8 @@ import { + ComponentDimension, + DimensionName, EventPublisher, + FlowDimension, MetricHandler, eventBridgeClient, logger, @@ -25,7 +28,10 @@ export const createContainer = (): CreateHandlerDependencies => { sqsClient, eventBridgeClient, metricHandler: new MetricHandler(dlMetricsNamespace, [ - { Name: 'Environment', Value: environment }, + { Name: DimensionName.Environment, Value: environment }, + { Name: DimensionName.Flow, Value: FlowDimension.Print }, + { Name: DimensionName.Component, Value: ComponentDimension.TTL }, + { Name: DimensionName.LambdaFunction, Value: 'ttl-handle-expiry' }, ]), }); diff --git a/utils/utils/src/cloudwatch/metric-handler.ts b/utils/utils/src/cloudwatch/metric-handler.ts index d0c523443..e394b60ac 100644 --- a/utils/utils/src/cloudwatch/metric-handler.ts +++ b/utils/utils/src/cloudwatch/metric-handler.ts @@ -31,6 +31,38 @@ export type MetricUnit = | 'Count/Second' | 'None'; +export const DimensionName = { + Component: 'Component', + Flow: 'Flow', + Environment: 'Environment', + SenderId: 'SenderId', + LambdaFunction: 'LambdaFunction', +} as const; + +export type DimensionName = (typeof DimensionName)[keyof typeof DimensionName]; + +export const ComponentDimension = { + MESH: 'MESH', + PDM: 'PDM', + TTL: 'TTL', + CoreNotify: 'Core Notify', + VirusScanning: 'Virus Scanning', + PrintRequest: 'Print Request', + Reporting: 'Reporting', + Callbacks: 'Callbacks', +} as const; + +export type ComponentDimension = + (typeof ComponentDimension)[keyof typeof ComponentDimension]; + +export const FlowDimension = { + DigitalLetter: 'Digital Letter', + Print: 'Print', + TrustReporting: 'Trust Reporting', +} as const; + +export type FlowDimension = (typeof FlowDimension)[keyof typeof FlowDimension]; + type Metric = [name: string, unit: MetricUnit, value: number]; export class MetricHandler { From 2c867152b5ee581b43e7cba754265dba25da6d92 Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Fri, 1 May 2026 16:47:33 +0100 Subject: [PATCH 7/9] CCM-17116: Adding MESH metrics --- .../mesh_poll/__tests__/test_processor.py | 74 ++++++++++++++----- lambdas/mesh-poll/mesh_poll/config.py | 27 ++++++- lambdas/mesh-poll/mesh_poll/handler.py | 4 +- lambdas/mesh-poll/mesh_poll/processor.py | 9 +++ .../py-mock-mesh/py_mock_mesh/mesh_client.py | 20 +++++ .../src/event-publisher/event-publisher.ts | 4 +- 6 files changed, 115 insertions(+), 23 deletions(-) diff --git a/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py b/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py index 56ac05d9c..6d9c0e70f 100644 --- a/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py +++ b/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py @@ -26,13 +26,17 @@ def setup_mocks(): log = Mock() polling_metric = Mock() + remaining_mesh_messages_metric = Mock() + unfinished_reading_mesh_metric = Mock() return ( config, sender_lookup, mesh_client, log, - polling_metric + polling_metric, + remaining_mesh_messages_metric, + unfinished_reading_mesh_metric ) @@ -68,7 +72,7 @@ class TestMeshMessageProcessor: def test_process_messages_iterates_through_inbox(self, mock_event_publisher_class): """Test that processor iterates through all messages in MESH inbox""" - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message1 = setup_message_data("1") message2 = setup_message_data("2") @@ -78,10 +82,13 @@ def test_process_messages_iterates_through_inbox(self, mock_event_publisher_clas mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) mesh_client.iterate_all_messages.return_value = [message1, message2] + mesh_client.list_messages.return_value = ["messageId1", "messageId2"] sender_lookup.is_valid_sender.return_value = True processor.process_messages() @@ -90,10 +97,12 @@ def test_process_messages_iterates_through_inbox(self, mock_event_publisher_clas assert mesh_client.iterate_all_messages.call_count == 1 assert sender_lookup.is_valid_sender.call_count == 2 # Both messages validated polling_metric.record.assert_called_once() + remaining_mesh_messages_metric.record.assert_called_once_with(2) + unfinished_reading_mesh_metric.record.assert_not_called() def test_process_messages_stops_near_timeout(self, mock_event_publisher_class): """Test that processor stops processing when near timeout""" - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message1 = setup_message_data("1") mock_event_publisher = Mock() @@ -105,20 +114,25 @@ def test_process_messages_stops_near_timeout(self, mock_event_publisher_class): mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis_near_timeout, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) mesh_client.iterate_all_messages.return_value = [message1] + mesh_client.list_messages.return_value = ["messageId1"] processor.process_messages() sender_lookup.is_valid_sender.assert_not_called() mock_event_publisher.send_events.assert_not_called() # No events published when timeout polling_metric.record.assert_called_once() + remaining_mesh_messages_metric.record.assert_called_once_with(1) + unfinished_reading_mesh_metric.record.assert_called_once() def test_process_message_with_valid_sender(self, mock_event_publisher_class): """Test processing a single message from valid sender""" - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message = setup_message_data("1") mock_event_publisher = Mock() @@ -131,9 +145,13 @@ def test_process_message_with_valid_sender(self, mock_event_publisher_class): mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) + mesh_client.list_messages.return_value = ["messageId1"] + sender_lookup.is_valid_sender.return_value = True processor.process_message(message) @@ -145,7 +163,7 @@ def test_process_message_with_valid_sender(self, mock_event_publisher_class): def test_process_message_with_unknown_sender(self, mock_event_publisher_class): """Test that messages from unknown senders are rejected silently""" - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message = setup_message_data("1") mock_event_publisher = Mock() @@ -153,6 +171,7 @@ def test_process_message_with_unknown_sender(self, mock_event_publisher_class): # Invalid sender sender_lookup.is_valid_sender.return_value = False + mesh_client.list_messages.return_value = ["messageId1"] processor = MeshMessageProcessor( config=config, @@ -160,7 +179,9 @@ def test_process_message_with_unknown_sender(self, mock_event_publisher_class): mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) processor.process_message(message) @@ -174,12 +195,13 @@ def test_process_message_logs_error_on_event_publish_failure(self, mock_event_pu Test that processor logs error when event publishing fails and does not acknowledge message """ - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message = setup_message_data("1") mock_event_publisher = Mock() mock_event_publisher.send_events.return_value = [{"id": "failed-event-1"}] mock_event_publisher_class.return_value = mock_event_publisher + mesh_client.list_messages.return_value = ["messageId1"] sender_lookup.is_valid_sender.return_value = True sender_lookup.get_sender_id.return_value = "test_sender_id" @@ -190,7 +212,9 @@ def test_process_message_logs_error_on_event_publish_failure(self, mock_event_pu mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) processor.process_message(message) @@ -202,10 +226,11 @@ def test_process_message_logs_error_on_event_publish_failure(self, mock_event_pu def test_all_messages_are_processed_in_a_single_iteration(self, mock_event_publisher_class): """Test that processor processes all messages in a single iteration""" - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message1 = setup_message_data("1") message2 = setup_message_data("2") message3 = setup_message_data("3") + mesh_client.list_messages.return_value = ["messageId1", "messageId2", "messageId3"] mock_event_publisher = Mock() mock_event_publisher.send_events.return_value = [] # No failed events @@ -217,7 +242,9 @@ def test_all_messages_are_processed_in_a_single_iteration(self, mock_event_publi mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) mesh_client.iterate_all_messages.return_value = [message1, message2, message3] @@ -230,10 +257,13 @@ def test_all_messages_are_processed_in_a_single_iteration(self, mock_event_publi assert sender_lookup.is_valid_sender.call_count == 3 assert mock_event_publisher.send_events.call_count == 3 polling_metric.record.assert_called_once() + remaining_mesh_messages_metric.record.assert_called_once_with(3) + unfinished_reading_mesh_metric.record.assert_not_called() + def test_process_message_rejects_missing_local_id(self, mock_event_publisher_class): """Test that processor publishes MESHInboxMessageInvalid event for missing local_id""" - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message = setup_message_data("1") message.local_id = None @@ -250,7 +280,9 @@ def test_process_message_rejects_missing_local_id(self, mock_event_publisher_cla mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) processor.process_message(message) @@ -260,7 +292,7 @@ def test_process_message_rejects_missing_local_id(self, mock_event_publisher_cla def test_process_message_rejects_empty_local_id(self, mock_event_publisher_class): """Test that processor publishes MESHInboxMessageInvalid event for empty local_id""" - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message = setup_message_data("1") message.local_id = "" @@ -277,7 +309,9 @@ def test_process_message_rejects_empty_local_id(self, mock_event_publisher_class mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) processor.process_message(message) @@ -290,7 +324,7 @@ def test_process_message_rejects_whitespace_only_local_id(self, mock_event_publi Test that processor publishes MESHInboxMessageInvalid event for whitespace-only local_id """ - (config, sender_lookup, mesh_client, log, polling_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() message = setup_message_data("1") message.local_id = " " @@ -307,7 +341,9 @@ def test_process_message_rejects_whitespace_only_local_id(self, mock_event_publi mesh_client=mesh_client, get_remaining_time_in_millis=get_remaining_time_in_millis, log=log, - polling_metric=polling_metric + polling_metric=polling_metric, + remaining_mesh_messages_metric=remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric ) processor.process_message(message) diff --git a/lambdas/mesh-poll/mesh_poll/config.py b/lambdas/mesh-poll/mesh_poll/config.py index cfc4ab51c..c832e19d1 100644 --- a/lambdas/mesh-poll/mesh_poll/config.py +++ b/lambdas/mesh-poll/mesh_poll/config.py @@ -15,7 +15,8 @@ "certificate_expiry_metric_name": "CERTIFICATE_EXPIRY_METRIC_NAME", "certificate_expiry_metric_namespace": "CERTIFICATE_EXPIRY_METRIC_NAMESPACE", "polling_metric_name": "POLLING_METRIC_NAME", - "polling_metric_namespace": "POLLING_METRIC_NAMESPACE" + "polling_metric_namespace": "POLLING_METRIC_NAMESPACE", + "dl_metrics_namespace": "DL_METRICS_NAMESPACE", } @@ -31,12 +32,16 @@ def __init__(self, ssm=None): super().__init__(ssm=ssm) self.polling_metric = None + self.remaining_mesh_messages_metric = None + self.unfinished_reading_mesh_metric = None def __enter__(self): super().__enter__() # Build polling metric self.polling_metric = self.build_polling_metric() + self.remaining_mesh_messages_metric = self.build_remaining_mesh_messages_metric() + self.unfinished_reading_mesh_metric = self.build_unfinished_reading_mesh_metric() return self @@ -49,3 +54,23 @@ def build_polling_metric(self): namespace=self.polling_metric_namespace, dimensions={"Environment": self.environment} ) + + def build_remaining_mesh_messages_metric(self): + """ + Returns a custom metric to record remaining messages in the MESH inbox after polling + """ + return Metric( + name="RemainingMeshMessages", + namespace=self.dl_metrics_namespace, + dimensions={"Environment": self.environment} + ) + + def build_unfinished_reading_mesh_metric(self): + """ + Returns a custom metric to record that the poll lambda is exiting before processing all messages in the MESH inbox due to time constraints + """ + return Metric( + name="UnfinishedReadingMeshMessages", + namespace=self.dl_metrics_namespace, + dimensions={"Environment": self.environment} + ) diff --git a/lambdas/mesh-poll/mesh_poll/handler.py b/lambdas/mesh-poll/mesh_poll/handler.py index 61d6738eb..bb0ced4d8 100644 --- a/lambdas/mesh-poll/mesh_poll/handler.py +++ b/lambdas/mesh-poll/mesh_poll/handler.py @@ -15,6 +15,8 @@ def handler(_, context): mesh_client=config.mesh_client, get_remaining_time_in_millis=context.get_remaining_time_in_millis, log=log, - polling_metric=config.polling_metric) + polling_metric=config.polling_metric, + remaining_mesh_messages_metric=config.remaining_mesh_messages_metric, + unfinished_reading_mesh_metric=config.unfinished_reading_mesh_metric) processor.process_messages() diff --git a/lambdas/mesh-poll/mesh_poll/processor.py b/lambdas/mesh-poll/mesh_poll/processor.py index e17e95b50..0a9e2db0b 100644 --- a/lambdas/mesh-poll/mesh_poll/processor.py +++ b/lambdas/mesh-poll/mesh_poll/processor.py @@ -27,6 +27,8 @@ def __init__(self, **kwargs): self.__get_remaining_time_in_millis = kwargs['get_remaining_time_in_millis'] self.__mesh_client.handshake() self.__polling_metric = kwargs['polling_metric'] + self.__remaining_mesh_messages_metric = kwargs['remaining_mesh_messages_metric'] + self.__unfinished_reading_mesh_metric = kwargs['unfinished_reading_mesh_metric'] environment = 'development' deployment = 'primary' @@ -57,6 +59,10 @@ def process_messages(self): """ self.__log.info('Polling for messages') + # Record how many messages are in the mailbox + remaining_messages = self.__mesh_client.list_messages() + self.__remaining_mesh_messages_metric.record(len(remaining_messages)) + # Process all messages in the inbox message_count = 0 for message in self.__mesh_client.iterate_all_messages(): @@ -65,17 +71,20 @@ def process_messages(self): self.__log.info( 'Not enough time to process more files. Exiting') self.__polling_metric.record(1) + self.__unfinished_reading_mesh_metric.record(1) return self.process_message(message) if message_count == 0: self.__log.info('No messages found in inbox') + self.__remaining_mesh_messages_metric.record(0) else: self.__log.info(f'Processed {message_count} message(s)') self.__polling_metric.record(1) + def process_message(self, message): """ Processes an individual message from a MESH inbox - validates sender and publishes event diff --git a/utils/py-mock-mesh/py_mock_mesh/mesh_client.py b/utils/py-mock-mesh/py_mock_mesh/mesh_client.py index 2317b5bdd..cea21ca45 100644 --- a/utils/py-mock-mesh/py_mock_mesh/mesh_client.py +++ b/utils/py-mock-mesh/py_mock_mesh/mesh_client.py @@ -50,6 +50,26 @@ def iterate_all_messages(self): ContinuationToken=continuation_token ) + def list_messages(self, max_results: Optional[int] = None, workflow_filter: Optional[str] = None) -> List[str]: + """ + Lists message IDs in the inbox, with optional filtering by workflow_id and max_results + """ + response = self.s3_client.list_objects_v2( + Bucket=self.s3_bucket, + Prefix=self.inbox_prefix) + + message_ids = [] + + for s3_object in response.get('Contents', []): + + message_id = s3_object['Key'].split('/')[-1] + message_ids.append(message_id) + + if max_results and len(message_ids) >= max_results: + return message_ids + + return message_ids + def retrieve_message(self, message_id): """ Retrieves a specific message by ID from the inbox diff --git a/utils/utils/src/event-publisher/event-publisher.ts b/utils/utils/src/event-publisher/event-publisher.ts index b110429dd..26b120260 100644 --- a/utils/utils/src/event-publisher/event-publisher.ts +++ b/utils/utils/src/event-publisher/event-publisher.ts @@ -139,7 +139,7 @@ export class EventPublisher { ) { if (successfulCount > 0) { this.metricHandler.addMetrics([ - `${entries[0].DetailType}_batchSuccess`, + `${entries[0].DetailType}_success`, 'Count', successfulCount, ]); @@ -147,7 +147,7 @@ export class EventPublisher { if (failedEntryCount > 0) { this.metricHandler.addMetrics([ - `${entries[0].DetailType}_batchFailure`, + `${entries[0].DetailType}_failure`, 'Count', failedEntryCount, ]); From a0a5dbdaef4986f30dd60f97f14d57465e702f18 Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Fri, 1 May 2026 17:21:14 +0100 Subject: [PATCH 8/9] WIP --- lambdas/mesh-poll/mesh_poll/config.py | 11 +++++++++++ lambdas/mesh-poll/mesh_poll/processor.py | 2 ++ utils/py-utils/dl_utils/event_publisher.py | 1 + utils/py-utils/dl_utils/metric_client.py | 6 ++++-- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lambdas/mesh-poll/mesh_poll/config.py b/lambdas/mesh-poll/mesh_poll/config.py index c832e19d1..c11539e43 100644 --- a/lambdas/mesh-poll/mesh_poll/config.py +++ b/lambdas/mesh-poll/mesh_poll/config.py @@ -34,6 +34,7 @@ def __init__(self, ssm=None): self.polling_metric = None self.remaining_mesh_messages_metric = None self.unfinished_reading_mesh_metric = None + self.event_publisher_metric = None def __enter__(self): super().__enter__() @@ -42,6 +43,7 @@ def __enter__(self): self.polling_metric = self.build_polling_metric() self.remaining_mesh_messages_metric = self.build_remaining_mesh_messages_metric() self.unfinished_reading_mesh_metric = self.build_unfinished_reading_mesh_metric() + self.event_publisher_metric = self.build_event_publisher_metric() return self @@ -55,6 +57,15 @@ def build_polling_metric(self): dimensions={"Environment": self.environment} ) + def build_event_publisher_metric(self): + """ + Returns a custom metric to record event published by the EventPublisher class + """ + return Metric( + namespace=self.dl_metrics_namespace, + dimensions={"Environment": self.environment} + ) + def build_remaining_mesh_messages_metric(self): """ Returns a custom metric to record remaining messages in the MESH inbox after polling diff --git a/lambdas/mesh-poll/mesh_poll/processor.py b/lambdas/mesh-poll/mesh_poll/processor.py index 0a9e2db0b..156fe6da0 100644 --- a/lambdas/mesh-poll/mesh_poll/processor.py +++ b/lambdas/mesh-poll/mesh_poll/processor.py @@ -29,6 +29,7 @@ def __init__(self, **kwargs): self.__polling_metric = kwargs['polling_metric'] self.__remaining_mesh_messages_metric = kwargs['remaining_mesh_messages_metric'] self.__unfinished_reading_mesh_metric = kwargs['unfinished_reading_mesh_metric'] + self.__event_publisher_metric = kwargs['event_publisher_metric'] environment = 'development' deployment = 'primary' @@ -41,6 +42,7 @@ def __init__(self, **kwargs): self.__event_publisher = EventPublisher( event_bus_arn=self.__config.event_bus_arn, dlq_url=self.__config.event_publisher_dlq_url, + event_metric=self.__event_publisher_metric, logger=self.__log ) diff --git a/utils/py-utils/dl_utils/event_publisher.py b/utils/py-utils/dl_utils/event_publisher.py index 5a4e25fb5..e6f54f8a6 100644 --- a/utils/py-utils/dl_utils/event_publisher.py +++ b/utils/py-utils/dl_utils/event_publisher.py @@ -41,6 +41,7 @@ def __init__( self, event_bus_arn: str, dlq_url: str, + event_metric: Metric, logger: Optional[logging.Logger] = None, events_client: Optional[Any] = None, sqs_client: Optional[Any] = None diff --git a/utils/py-utils/dl_utils/metric_client.py b/utils/py-utils/dl_utils/metric_client.py index dfbdb1ba2..37901c355 100644 --- a/utils/py-utils/dl_utils/metric_client.py +++ b/utils/py-utils/dl_utils/metric_client.py @@ -16,7 +16,9 @@ def __init__(self, **kwargs): self.dimensions = kwargs.get("dimensions", {}) self.unit = kwargs.get("unit", 'Count') - def record(self, value): + def record(self, value, **kwargs): + + metric_name = kwargs.get('name', self.name) """ method for reporting metric """ @@ -30,7 +32,7 @@ def record(self, value): ], "Metrics": [ { - "Name": self.name, + "Name": metric_name, "Unit": self.unit, } ] From b5d182afdd917856d1e2d448afea34de2fc84f68 Mon Sep 17 00:00:00 2001 From: Angel Pastor Date: Fri, 1 May 2026 17:45:05 +0100 Subject: [PATCH 9/9] WIP metrics in python lambdas --- .../__tests__/test_handler.py | 1 + .../mesh_acknowledge/config.py | 21 ++++- .../mesh_acknowledge/handler.py | 1 + lambdas/mesh-download/mesh_download/config.py | 12 +++ .../mesh-download/mesh_download/handler.py | 1 + .../mesh_poll/__tests__/test_processor.py | 49 ++++++----- lambdas/mesh-poll/mesh_poll/handler.py | 3 +- .../__tests__/test_event_publisher.py | 82 ++++++++++++++++++- utils/py-utils/dl_utils/event_publisher.py | 13 ++- utils/py-utils/dl_utils/metric_client.py | 4 +- 10 files changed, 162 insertions(+), 25 deletions(-) diff --git a/lambdas/mesh-acknowledge/mesh_acknowledge/__tests__/test_handler.py b/lambdas/mesh-acknowledge/mesh_acknowledge/__tests__/test_handler.py index 31d1f72cf..1281d8b89 100644 --- a/lambdas/mesh-acknowledge/mesh_acknowledge/__tests__/test_handler.py +++ b/lambdas/mesh-acknowledge/mesh_acknowledge/__tests__/test_handler.py @@ -143,6 +143,7 @@ def test_handler_passes_correct_parameters( event_publisher_cls.assert_called_once_with( event_bus_arn=config.event_publisher_event_bus_arn, dlq_url=config.event_publisher_dlq_url, + event_metric=config.event_publisher_metric, logger=log, ) acknowledger_cls.assert_called_once_with( diff --git a/lambdas/mesh-acknowledge/mesh_acknowledge/config.py b/lambdas/mesh-acknowledge/mesh_acknowledge/config.py index c6fcef081..20c3fac0e 100644 --- a/lambdas/mesh-acknowledge/mesh_acknowledge/config.py +++ b/lambdas/mesh-acknowledge/mesh_acknowledge/config.py @@ -1,7 +1,7 @@ """ Module for configuring MESH Acknowledger application """ -from dl_utils import BaseMeshConfig +from dl_utils import BaseMeshConfig, Metric _REQUIRED_ENV_VAR_MAP = { "ssm_mesh_prefix": "SSM_MESH_PREFIX", @@ -9,6 +9,7 @@ "environment": "ENVIRONMENT", "event_publisher_event_bus_arn": "EVENT_PUBLISHER_EVENT_BUS_ARN", "event_publisher_dlq_url": "EVENT_PUBLISHER_DLQ_URL", + "dl_metrics_namespace": "DL_METRICS_NAMESPACE", "dlq_url": "DLQ_URL", } @@ -21,3 +22,21 @@ class Config(BaseMeshConfig): """ _REQUIRED_ENV_VAR_MAP = _REQUIRED_ENV_VAR_MAP + + def __init__(self, ssm=None): + super().__init__(ssm=ssm) + self.event_publisher_metric = None + + def __enter__(self): + super().__enter__() + self.event_publisher_metric = self.build_event_publisher_metric() + return self + + def build_event_publisher_metric(self): + """ + Returns a custom metric to record events published by the EventPublisher class + """ + return Metric( + namespace=self.dl_metrics_namespace, + dimensions={"Environment": self.environment} + ) diff --git a/lambdas/mesh-acknowledge/mesh_acknowledge/handler.py b/lambdas/mesh-acknowledge/mesh_acknowledge/handler.py index c3a367a58..61933e8ae 100644 --- a/lambdas/mesh-acknowledge/mesh_acknowledge/handler.py +++ b/lambdas/mesh-acknowledge/mesh_acknowledge/handler.py @@ -23,6 +23,7 @@ def handler(message: Dict[str, Any], _context: Any): event_publisher = EventPublisher( event_bus_arn=config.event_publisher_event_bus_arn, dlq_url=config.event_publisher_dlq_url, + event_metric=config.event_publisher_metric, logger=log ) acknowledger = MeshAcknowledger( diff --git a/lambdas/mesh-download/mesh_download/config.py b/lambdas/mesh-download/mesh_download/config.py index f537e5208..c8d0cf350 100644 --- a/lambdas/mesh-download/mesh_download/config.py +++ b/lambdas/mesh-download/mesh_download/config.py @@ -12,6 +12,7 @@ "internal_duplicate_download_metric_name": "INTERNAL_DUPLICATE_DOWNLOAD_METRIC_NAME", "trust_duplicate_download_metric_name": "TRUST_DUPLICATE_DOWNLOAD_METRIC_NAME", "download_metric_namespace": "DOWNLOAD_METRIC_NAMESPACE", + "dl_metrics_namespace": "DL_METRICS_NAMESPACE", "event_publisher_event_bus_arn": "EVENT_PUBLISHER_EVENT_BUS_ARN", "event_publisher_dlq_url": "EVENT_PUBLISHER_DLQ_URL", "pii_bucket": "PII_BUCKET" @@ -32,6 +33,7 @@ def __init__(self, ssm=None, s3_client=None): self.download_metric = None self.internal_duplicate_download_metric = None self.trust_duplicate_download_metric = None + self.event_publisher_metric = None def __enter__(self): super().__enter__() @@ -40,6 +42,7 @@ def __enter__(self): self.download_metric = self.build_download_metric() self.internal_duplicate_download_metric = self.build_internal_duplicate_download_metric() self.trust_duplicate_download_metric = self.build_trust_duplicate_download_metric() + self.event_publisher_metric = self.build_event_publisher_metric() return self @@ -73,6 +76,15 @@ def build_trust_duplicate_download_metric(self): dimensions={"Environment": self.environment} ) + def build_event_publisher_metric(self): + """ + Returns a custom metric to record events published by the EventPublisher class + """ + return Metric( + namespace=self.dl_metrics_namespace, + dimensions={"Environment": self.environment} + ) + @property def transactional_data_bucket(self): """ diff --git a/lambdas/mesh-download/mesh_download/handler.py b/lambdas/mesh-download/mesh_download/handler.py index 67c99c760..846946da1 100644 --- a/lambdas/mesh-download/mesh_download/handler.py +++ b/lambdas/mesh-download/mesh_download/handler.py @@ -36,6 +36,7 @@ def handler(event, context): event_publisher = EventPublisher( event_bus_arn=config.event_publisher_event_bus_arn, dlq_url=config.event_publisher_dlq_url, + event_metric=config.event_publisher_metric, logger=log ) diff --git a/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py b/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py index 6d9c0e70f..f9b54ff84 100644 --- a/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py +++ b/lambdas/mesh-poll/mesh_poll/__tests__/test_processor.py @@ -28,6 +28,7 @@ def setup_mocks(): polling_metric = Mock() remaining_mesh_messages_metric = Mock() unfinished_reading_mesh_metric = Mock() + event_publisher_metric = Mock() return ( config, @@ -36,7 +37,8 @@ def setup_mocks(): log, polling_metric, remaining_mesh_messages_metric, - unfinished_reading_mesh_metric + unfinished_reading_mesh_metric, + event_publisher_metric ) @@ -72,7 +74,7 @@ class TestMeshMessageProcessor: def test_process_messages_iterates_through_inbox(self, mock_event_publisher_class): """Test that processor iterates through all messages in MESH inbox""" - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message1 = setup_message_data("1") message2 = setup_message_data("2") @@ -84,7 +86,8 @@ def test_process_messages_iterates_through_inbox(self, mock_event_publisher_clas log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) mesh_client.iterate_all_messages.return_value = [message1, message2] @@ -102,7 +105,7 @@ def test_process_messages_iterates_through_inbox(self, mock_event_publisher_clas def test_process_messages_stops_near_timeout(self, mock_event_publisher_class): """Test that processor stops processing when near timeout""" - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message1 = setup_message_data("1") mock_event_publisher = Mock() @@ -116,7 +119,8 @@ def test_process_messages_stops_near_timeout(self, mock_event_publisher_class): log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) mesh_client.iterate_all_messages.return_value = [message1] @@ -132,7 +136,7 @@ def test_process_messages_stops_near_timeout(self, mock_event_publisher_class): def test_process_message_with_valid_sender(self, mock_event_publisher_class): """Test processing a single message from valid sender""" - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message = setup_message_data("1") mock_event_publisher = Mock() @@ -147,7 +151,8 @@ def test_process_message_with_valid_sender(self, mock_event_publisher_class): log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) mesh_client.list_messages.return_value = ["messageId1"] @@ -163,7 +168,7 @@ def test_process_message_with_valid_sender(self, mock_event_publisher_class): def test_process_message_with_unknown_sender(self, mock_event_publisher_class): """Test that messages from unknown senders are rejected silently""" - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message = setup_message_data("1") mock_event_publisher = Mock() @@ -181,7 +186,8 @@ def test_process_message_with_unknown_sender(self, mock_event_publisher_class): log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) processor.process_message(message) @@ -195,7 +201,7 @@ def test_process_message_logs_error_on_event_publish_failure(self, mock_event_pu Test that processor logs error when event publishing fails and does not acknowledge message """ - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message = setup_message_data("1") mock_event_publisher = Mock() @@ -214,7 +220,8 @@ def test_process_message_logs_error_on_event_publish_failure(self, mock_event_pu log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) processor.process_message(message) @@ -226,7 +233,7 @@ def test_process_message_logs_error_on_event_publish_failure(self, mock_event_pu def test_all_messages_are_processed_in_a_single_iteration(self, mock_event_publisher_class): """Test that processor processes all messages in a single iteration""" - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message1 = setup_message_data("1") message2 = setup_message_data("2") message3 = setup_message_data("3") @@ -244,7 +251,8 @@ def test_all_messages_are_processed_in_a_single_iteration(self, mock_event_publi log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) mesh_client.iterate_all_messages.return_value = [message1, message2, message3] @@ -263,7 +271,7 @@ def test_all_messages_are_processed_in_a_single_iteration(self, mock_event_publi def test_process_message_rejects_missing_local_id(self, mock_event_publisher_class): """Test that processor publishes MESHInboxMessageInvalid event for missing local_id""" - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message = setup_message_data("1") message.local_id = None @@ -282,7 +290,8 @@ def test_process_message_rejects_missing_local_id(self, mock_event_publisher_cla log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) processor.process_message(message) @@ -292,7 +301,7 @@ def test_process_message_rejects_missing_local_id(self, mock_event_publisher_cla def test_process_message_rejects_empty_local_id(self, mock_event_publisher_class): """Test that processor publishes MESHInboxMessageInvalid event for empty local_id""" - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message = setup_message_data("1") message.local_id = "" @@ -311,7 +320,8 @@ def test_process_message_rejects_empty_local_id(self, mock_event_publisher_class log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) processor.process_message(message) @@ -324,7 +334,7 @@ def test_process_message_rejects_whitespace_only_local_id(self, mock_event_publi Test that processor publishes MESHInboxMessageInvalid event for whitespace-only local_id """ - (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric) = setup_mocks() + (config, sender_lookup, mesh_client, log, polling_metric, remaining_mesh_messages_metric, unfinished_reading_mesh_metric, event_publisher_metric) = setup_mocks() message = setup_message_data("1") message.local_id = " " @@ -343,7 +353,8 @@ def test_process_message_rejects_whitespace_only_local_id(self, mock_event_publi log=log, polling_metric=polling_metric, remaining_mesh_messages_metric=remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=unfinished_reading_mesh_metric + unfinished_reading_mesh_metric=unfinished_reading_mesh_metric, + event_publisher_metric=event_publisher_metric ) processor.process_message(message) diff --git a/lambdas/mesh-poll/mesh_poll/handler.py b/lambdas/mesh-poll/mesh_poll/handler.py index bb0ced4d8..de6b0508e 100644 --- a/lambdas/mesh-poll/mesh_poll/handler.py +++ b/lambdas/mesh-poll/mesh_poll/handler.py @@ -17,6 +17,7 @@ def handler(_, context): log=log, polling_metric=config.polling_metric, remaining_mesh_messages_metric=config.remaining_mesh_messages_metric, - unfinished_reading_mesh_metric=config.unfinished_reading_mesh_metric) + unfinished_reading_mesh_metric=config.unfinished_reading_mesh_metric, + event_publisher_metric=config.event_publisher_metric) processor.process_messages() diff --git a/utils/py-utils/dl_utils/__tests__/test_event_publisher.py b/utils/py-utils/dl_utils/__tests__/test_event_publisher.py index 4ed1aa209..01fae2725 100644 --- a/utils/py-utils/dl_utils/__tests__/test_event_publisher.py +++ b/utils/py-utils/dl_utils/__tests__/test_event_publisher.py @@ -27,10 +27,16 @@ def mock_sqs_client(): @pytest.fixture -def test_config(mock_logger, mock_events_client, mock_sqs_client): +def mock_event_metric(): + return Mock() + + +@pytest.fixture +def test_config(mock_logger, mock_events_client, mock_sqs_client, mock_event_metric): return { 'event_bus_arn': 'arn:aws:events:us-east-1:123456789012:event-bus/test-bus', 'dlq_url': 'https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq', + 'event_metric': mock_event_metric, 'logger': mock_logger, 'events_client': mock_events_client, 'sqs_client': mock_sqs_client, @@ -520,3 +526,77 @@ def test_should_not_retry_on_permanent_client_error( dlq_call_args = mock_sqs_client.send_message_batch.call_args[1] assert dlq_call_args['Entries'][0]['MessageBody'] == json.dumps(valid_cloud_event) assert dlq_call_args['Entries'][0]['MessageAttributes']['DlqReason']['StringValue'] == 'EVENTBRIDGE_FAILURE' + + +class TestEventMetricRecording: + """Tests for CloudWatch metric recording on EventBridge publish.""" + + def test_records_success_metric_when_events_sent_successfully( + self, test_config, mock_events_client, mock_sqs_client, + mock_event_metric, valid_cloud_event, mock_validator): + """Records a success metric using the event type as the metric name.""" + mock_events_client.put_events.return_value = { + 'FailedEntryCount': 0, + 'Entries': [{'EventId': 'event-1'}] + } + + publisher = EventPublisher(**test_config) + publisher.send_events([valid_cloud_event], validator=mock_validator) + + event_type = valid_cloud_event['type'] + mock_event_metric.record.assert_called_once_with(1, name=f"{event_type}_success") + + def test_records_failure_metric_when_eventbridge_rejects_event( + self, test_config, mock_events_client, mock_sqs_client, + mock_event_metric, valid_cloud_event, mock_validator): + """Records a failure metric when EventBridge permanently rejects an event.""" + mock_events_client.put_events.return_value = { + 'FailedEntryCount': 1, + 'Entries': [{'ErrorCode': 'AccessDenied', 'ErrorMessage': 'Access denied'}] + } + mock_sqs_client.send_message_batch.return_value = {'Successful': []} + + publisher = EventPublisher(**test_config) + publisher.send_events([valid_cloud_event], validator=mock_validator) + + event_type = valid_cloud_event['type'] + mock_event_metric.record.assert_called_once_with(1, name=f"{event_type}_failure") + + def test_records_both_success_and_failure_metrics_for_mixed_batch( + self, test_config, mock_events_client, mock_sqs_client, + mock_event_metric, valid_cloud_event, valid_cloud_event2, mock_validator): + """Records separate success and failure metrics for a batch with mixed outcomes.""" + mock_events_client.put_events.return_value = { + 'FailedEntryCount': 1, + 'Entries': [ + {'ErrorCode': 'AccessDenied', 'ErrorMessage': 'Access denied'}, + {'EventId': 'event-2'}, + ] + } + mock_sqs_client.send_message_batch.return_value = {'Successful': []} + + publisher = EventPublisher(**test_config) + publisher.send_events([valid_cloud_event, valid_cloud_event2], validator=mock_validator) + + event_type = valid_cloud_event['type'] + mock_event_metric.record.assert_any_call(1, name=f"{event_type}_success") + mock_event_metric.record.assert_any_call(1, name=f"{event_type}_failure") + + def test_does_not_record_metric_when_event_metric_is_none( + self, mock_logger, mock_events_client, mock_sqs_client, + valid_cloud_event, mock_validator): + """When no event_metric is provided, no metric is recorded.""" + mock_events_client.put_events.return_value = { + 'FailedEntryCount': 0, + 'Entries': [{'EventId': 'event-1'}] + } + + publisher = EventPublisher( + event_bus_arn='arn:aws:events:us-east-1:123456789012:event-bus/test-bus', + dlq_url='https://sqs.us-east-1.amazonaws.com/123456789012/test-dlq', + logger=mock_logger, + events_client=mock_events_client, + sqs_client=mock_sqs_client, + ) + # Should not raise + publisher.send_events([valid_cloud_event], validator=mock_validator) diff --git a/utils/py-utils/dl_utils/event_publisher.py b/utils/py-utils/dl_utils/event_publisher.py index e6f54f8a6..e9f1bbaf1 100644 --- a/utils/py-utils/dl_utils/event_publisher.py +++ b/utils/py-utils/dl_utils/event_publisher.py @@ -13,6 +13,7 @@ import boto3 from botocore.config import Config from botocore.exceptions import ClientError +from .metric_client import Metric DlqReason = Literal['INVALID_EVENT', 'EVENTBRIDGE_FAILURE'] @@ -41,7 +42,7 @@ def __init__( self, event_bus_arn: str, dlq_url: str, - event_metric: Metric, + event_metric: Optional[Metric] = None, logger: Optional[logging.Logger] = None, events_client: Optional[Any] = None, sqs_client: Optional[Any] = None @@ -56,6 +57,7 @@ def __init__( self.event_bus_arn = event_bus_arn self.dlq_url = dlq_url + self.event_metric = event_metric self.logger = logger or logging.getLogger(__name__) self.events_client = events_client or boto3.client( 'events', @@ -211,6 +213,15 @@ def _send_to_event_bridge(self, events: List[Dict[str, Any]]) -> List[Dict[str, batch_failures = self._send_batch_with_retry(batch) + if self.event_metric is not None and batch: + event_type = batch[0].get('type', 'unknown') + success_count = len(batch) - len(batch_failures) + failure_count = len(batch_failures) + if success_count > 0: + self.event_metric.record(success_count, name=f"{event_type}_success") + if failure_count > 0: + self.event_metric.record(failure_count, name=f"{event_type}_failure") + if batch_failures: for event in batch_failures: self.logger.warning( diff --git a/utils/py-utils/dl_utils/metric_client.py b/utils/py-utils/dl_utils/metric_client.py index 37901c355..0c7f5a5cd 100644 --- a/utils/py-utils/dl_utils/metric_client.py +++ b/utils/py-utils/dl_utils/metric_client.py @@ -11,7 +11,7 @@ class Metric: # pylint: disable=too-few-public-methods """ def __init__(self, **kwargs): - self.name = kwargs['name'] + self.name = kwargs.get('name', None) self.namespace = kwargs['namespace'] self.dimensions = kwargs.get("dimensions", {}) self.unit = kwargs.get("unit", 'Count') @@ -39,5 +39,5 @@ def record(self, value, **kwargs): }], }, **self.dimensions, - self.name: value, + metric_name: value, }))