From 4150c2b24744ca0443ea3d0ad55cda7127777121 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Thu, 16 Apr 2026 14:01:42 -0400 Subject: [PATCH 1/4] KMS-661: Add Monitoring and Metrics for event publishing (KMS) --- bin/deploy-bamboo.sh | 1 + bin/localstack/start.sh | 11 +- cdk/app/lib/KmsStack.ts | 10 + cdk/app/lib/helper/IamSetup.ts | 8 + .../lib/helper/KeywordSyncMonitoringSetup.ts | 64 ++ cdk/bin/main.ts | 5 + package-lock.json | 897 ++++++++++-------- package.json | 1 + .../src/publisher/__tests__/handler.test.js | 114 +++ serverless/src/publisher/handler.js | 89 +- .../__tests__/emitPublisherMetrics.test.js | 174 ++++ serverless/src/shared/emitPublisherMetrics.js | 137 +++ 12 files changed, 1099 insertions(+), 412 deletions(-) create mode 100644 cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts create mode 100644 serverless/src/shared/__tests__/emitPublisherMetrics.test.js create mode 100644 serverless/src/shared/emitPublisherMetrics.js diff --git a/bin/deploy-bamboo.sh b/bin/deploy-bamboo.sh index e6c97a6e..a9002b34 100755 --- a/bin/deploy-bamboo.sh +++ b/bin/deploy-bamboo.sh @@ -64,6 +64,7 @@ dockerRun() { --env "EDL_PASSWORD=$bamboo_EDL_PASSWORD" \ --env "CMR_BASE_URL=$bamboo_CMR_BASE_URL" \ --env "BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE=${bamboo_BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE:-false}" \ + --env "KEYWORD_SYNC_ALARM_EMAILS=${bamboo_KEYWORD_SYNC_ALARM_EMAILS:-}" \ --env "CORS_ORIGIN=$bamboo_CORS_ORIGIN" \ --env "RDF4J_INSTANCE_TYPE=$bamboo_RDF4J_INSTANCE_TYPE" \ --env "RDF4J_CONTAINER_MEMORY_LIMIT=$bamboo_RDF4J_CONTAINER_MEMORY_LIMIT" \ diff --git a/bin/localstack/start.sh b/bin/localstack/start.sh index 28094d2f..d4dc734f 100755 --- a/bin/localstack/start.sh +++ b/bin/localstack/start.sh @@ -6,7 +6,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=bin/env/local_env.sh source "${SCRIPT_DIR}/../env/local_env.sh" -REQUIRED_SERVICES="sns,sqs,events" +# CloudWatch is enabled here so keyword sync metrics can be verified locally +# alongside the existing SNS/SQS/EventBridge workflow. +REQUIRED_SERVICES="sns,sqs,events,cloudwatch" if ! docker network inspect "${KMS_DOCKER_NETWORK}" >/dev/null 2>&1; then docker network create "${KMS_DOCKER_NETWORK}" >/dev/null @@ -24,7 +26,8 @@ if [[ -n "${existing_id}" ]]; then || true )" - if [[ ",${configured_services}," != *",events,"* ]]; then + if [[ ",${configured_services}," != *",events,"* ]] \ + || [[ ",${configured_services}," != *",cloudwatch,"* ]]; then docker rm -f "${LOCALSTACK_CONTAINER_NAME}" >/dev/null echo "Recreating LocalStack container '${LOCALSTACK_CONTAINER_NAME}' to enable services: ${REQUIRED_SERVICES}" existing_id="" @@ -54,5 +57,5 @@ docker run -d \ "${LOCALSTACK_IMAGE}" >/dev/null echo "Started LocalStack container '${LOCALSTACK_CONTAINER_NAME}' on ${LOCALSTACK_PORT}->4566" -echo "SNS/SQS/EventBridge endpoint for SAM/Lambda containers: ${AWS_ENDPOINT_URL}" -echo "SNS/SQS/EventBridge endpoint from host: http://localhost:${LOCALSTACK_PORT}" +echo "SNS/SQS/EventBridge/CloudWatch endpoint for SAM/Lambda containers: ${AWS_ENDPOINT_URL}" +echo "SNS/SQS/EventBridge/CloudWatch endpoint from host: http://localhost:${LOCALSTACK_PORT}" diff --git a/cdk/app/lib/KmsStack.ts b/cdk/app/lib/KmsStack.ts index 2165ae6e..8c67d5f1 100644 --- a/cdk/app/lib/KmsStack.ts +++ b/cdk/app/lib/KmsStack.ts @@ -8,6 +8,7 @@ import { Construct } from 'constructs' import { ApiResources } from './helper/ApiResources' import { IamSetup } from './helper/IamSetup' +import { KeywordSyncMonitoringSetup } from './helper/KeywordSyncMonitoringSetup' import { LambdaFunctions } from './helper/KmsLambdaFunctions' import { LogForwardingSetup } from './helper/LogForwardingSetup' import { VpcSetup } from './helper/VpcSetup' @@ -18,6 +19,7 @@ import { VpcSetup } from './helper/VpcSetup' */ export interface KmsStackProps extends cdk.StackProps { existingApiId: string | undefined + keywordSyncAlarmEmails?: string[] prefix: string rootResourceId: string | undefined stage: string @@ -83,6 +85,7 @@ export class KmsStack extends cdk.Stack { const { environment, existingApiId, + keywordSyncAlarmEmails, logDestinationArn, prefix, rootResourceId, @@ -147,6 +150,7 @@ export class KmsStack extends cdk.Stack { apiResources.configureCors(this, prefix) // Use a concrete ARN string during local synth so SAM can inject it into the Lambda env. const keywordEventsTopicArn = useLocalstack ? localTopicArn : this.keywordEventsTopic.topicArn + // SNS keyword event publishing needs the topic ARN at runtime. const lambdaEnvironment = { ...environment, KEYWORD_EVENTS_TOPIC_ARN: keywordEventsTopicArn @@ -165,6 +169,12 @@ export class KmsStack extends cdk.Stack { useLocalstack }) + new KeywordSyncMonitoringSetup(this, { + prefix, + stage: this.stage, + notificationEmails: keywordSyncAlarmEmails || [] + }) + const lambdas = this.lambdaFunctions.getAllLambdas() const methods = this.lambdaFunctions.getAllMethods() diff --git a/cdk/app/lib/helper/IamSetup.ts b/cdk/app/lib/helper/IamSetup.ts index b03ae07a..0ae5f1a2 100644 --- a/cdk/app/lib/helper/IamSetup.ts +++ b/cdk/app/lib/helper/IamSetup.ts @@ -115,6 +115,9 @@ export class IamSetup { /** * Adds base KMS Lambda policy to the Lambda role. + * + * These permissions cover shared runtime needs across the application, + * including CloudWatch custom metric emission for keyword sync monitoring. * @private */ private addKMSLambdaBasePolicy() { @@ -123,6 +126,11 @@ export class IamSetup { resources: ['*'] })) + this.lambdaRole.addToPolicy(new iam.PolicyStatement({ + actions: ['cloudwatch:PutMetricData'], + resources: ['*'] + })) + this.lambdaRole.addToPolicy(new iam.PolicyStatement({ actions: ['lambda:InvokeFunction'], resources: ['*'] diff --git a/cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts b/cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts new file mode 100644 index 00000000..764b71ca --- /dev/null +++ b/cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts @@ -0,0 +1,64 @@ +import * as cdk from 'aws-cdk-lib' +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch' +import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions' +import * as sns from 'aws-cdk-lib/aws-sns' +import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions' +import { Construct } from 'constructs' + +const KEYWORD_SYNC_METRIC_NAMESPACE = 'KMS/KeywordSync' +const KEYWORD_EVENT_PUBLISH_FAILURES_METRIC = 'KeywordEventPublishFailures' + +export interface KeywordSyncMonitoringSetupProps { + prefix: string + stage: string + notificationEmails?: string[] +} + +/** + * Sets up CloudWatch monitoring resources for keyword sync publishing. + */ +export class KeywordSyncMonitoringSetup { + public readonly publishFailuresAlarm: cloudwatch.Alarm + + /** + * Creates the keyword sync monitoring resources. + * + * @param {Construct} scope - Construct scope for the monitoring resources. + * @param {KeywordSyncMonitoringSetupProps} props - Monitoring configuration. + */ + constructor(scope: Construct, props: KeywordSyncMonitoringSetupProps) { + const { notificationEmails = [], prefix, stage } = props + + const keywordEventPublishFailuresMetric = new cloudwatch.Metric({ + namespace: KEYWORD_SYNC_METRIC_NAMESPACE, + metricName: KEYWORD_EVENT_PUBLISH_FAILURES_METRIC, + statistic: 'Sum', + period: cdk.Duration.minutes(5) + }) + + this.publishFailuresAlarm = new cloudwatch.Alarm(scope, 'KeywordSyncPublishFailuresAlarm', { + alarmName: `${prefix}-${stage}-keyword-event-publish-failures`, + alarmDescription: 'Alerts when publisher keyword event publishes fail after retries.', + metric: keywordEventPublishFailuresMetric, + threshold: 1, + evaluationPeriods: 1, + datapointsToAlarm: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING + }) + + if (notificationEmails.length === 0) { + return + } + + const alarmTopic = new sns.Topic(scope, 'KeywordSyncAlarmTopic', { + topicName: `${prefix}-${stage}-keyword-sync-alerts` + }) + + notificationEmails.forEach((email) => { + alarmTopic.addSubscription(new subscriptions.EmailSubscription(email)) + }) + + this.publishFailuresAlarm.addAlarmAction(new cloudwatchActions.SnsAction(alarmTopic)) + } +} diff --git a/cdk/bin/main.ts b/cdk/bin/main.ts index 2a078b3f..15f0f3c0 100644 --- a/cdk/bin/main.ts +++ b/cdk/bin/main.ts @@ -44,6 +44,10 @@ async function main() { const existingApiId = process.env.EXISTING_API_ID const rootResourceId = process.env.ROOT_RESOURCE_ID let logDestinationArn = process.env.LOG_DESTINATION_ARN + const keywordSyncAlarmEmails = (process.env.KEYWORD_SYNC_ALARM_EMAILS || '') + .split(',') + .map((email) => email.trim()) + .filter(Boolean) const app = new cdk.App({ context: { @@ -165,6 +169,7 @@ async function main() { stage, existingApiId, rootResourceId, + keywordSyncAlarmEmails, logDestinationArn: logDestinationArn!, environment: { RDF4J_SERVICE_URL: useLocalstack diff --git a/package-lock.json b/package-lock.json index 6cb3fce6..b8a2b4a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "kms", "version": "0.0.1", "dependencies": { + "@aws-sdk/client-cloudwatch": "^3.1030.0", "@aws-sdk/client-eventbridge": "^3.997.0", "@aws-sdk/client-lambda": "^3.775.0", "@aws-sdk/client-s3": "^3.540.0", @@ -44,8 +45,8 @@ "@typescript-eslint/parser": "^8.58.0", "@vitejs/plugin-react": "^4.1.0", "@vitest/coverage-istanbul": "^1.4.0", - "aws-cdk": "^2.1118.0", - "aws-cdk-lib": "^2.248.0", + "aws-cdk": "2.1118.0", + "aws-cdk-lib": "2.248.0", "aws-sdk-client-mock": "^4.0.0", "constructs": "^10.4.3", "esbuild": "^0.19.12", @@ -347,6 +348,74 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-cloudwatch": { + "version": "3.1030.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.1030.0.tgz", + "integrity": "sha512-LEPjGvcwAVsfZhVP0kMir9CBwRM0cFjIkSiyJ4tHPkpqIenrYTPEMwn54GfeF/k1IFMGvmCDwsOB3Ht58Oo8OA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-node": "^3.972.30", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-compression": "^4.3.43", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@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.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.15", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", + "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-endpoints": "^3.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-eventbridge": { "version": "3.997.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-eventbridge/-/client-eventbridge-3.997.0.tgz", @@ -767,22 +836,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.26.tgz", - "integrity": "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==", + "version": "3.973.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", + "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.16", - "@smithy/core": "^3.23.13", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/xml-builder": "^3.972.17", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -803,15 +872,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.24.tgz", - "integrity": "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", + "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -819,20 +888,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.26.tgz", - "integrity": "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==", + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", + "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.21", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" }, "engines": { @@ -840,14 +909,14 @@ } }, "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-stream": { - "version": "4.5.21", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.21.tgz", - "integrity": "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==", + "version": "4.5.22", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", + "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/types": "^4.13.1", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -859,24 +928,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.28.tgz", - "integrity": "sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", + "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/credential-provider-env": "^3.972.24", - "@aws-sdk/credential-provider-http": "^3.972.26", - "@aws-sdk/credential-provider-login": "^3.972.28", - "@aws-sdk/credential-provider-process": "^3.972.24", - "@aws-sdk/credential-provider-sso": "^3.972.28", - "@aws-sdk/credential-provider-web-identity": "^3.972.28", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-login": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -884,18 +953,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.28.tgz", - "integrity": "sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", + "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -903,22 +972,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.29.tgz", - "integrity": "sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", + "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.24", - "@aws-sdk/credential-provider-http": "^3.972.26", - "@aws-sdk/credential-provider-ini": "^3.972.28", - "@aws-sdk/credential-provider-process": "^3.972.24", - "@aws-sdk/credential-provider-sso": "^3.972.28", - "@aws-sdk/credential-provider-web-identity": "^3.972.28", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-ini": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -926,16 +995,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.24.tgz", - "integrity": "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==", + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", + "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -943,18 +1012,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.28.tgz", - "integrity": "sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", + "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/token-providers": "3.1021.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/token-providers": "3.1026.0", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -962,17 +1031,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.28.tgz", - "integrity": "sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", + "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -1053,14 +1122,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz", + "integrity": "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -1081,13 +1150,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz", + "integrity": "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -1095,15 +1164,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz", - "integrity": "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz", + "integrity": "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -1183,18 +1252,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.28.tgz", - "integrity": "sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz", + "integrity": "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.13", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-retry": "^4.2.13", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1202,15 +1271,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", + "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" }, "engines": { @@ -1218,47 +1287,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.18.tgz", - "integrity": "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==", + "version": "3.996.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", + "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.28", - "@aws-sdk/region-config-resolver": "^3.972.10", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.14", - "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.13", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/middleware-retry": "^4.4.46", - "@smithy/middleware-serde": "^4.2.16", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", "@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.44", - "@smithy/util-defaults-mode-node": "^4.2.48", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.13", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1267,15 +1336,15 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", + "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" }, "engines": { @@ -1283,15 +1352,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.10.tgz", - "integrity": "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz", + "integrity": "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.13", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/config-resolver": "^4.4.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -1315,17 +1384,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1021.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1021.0.tgz", - "integrity": "sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==", + "version": "3.1026.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", + "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -1333,12 +1402,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", - "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", + "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -1384,27 +1453,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", - "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz", + "integrity": "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.14.tgz", - "integrity": "sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==", + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz", + "integrity": "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.28", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -1421,12 +1490,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", - "integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", + "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, @@ -4475,18 +4544,6 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.10.tgz", - "integrity": "sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g==", - "dependencies": { - "@smithy/types": "^4.13.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/chunked-blob-reader": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", @@ -4511,16 +4568,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", - "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", + "version": "4.4.15", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.15.tgz", + "integrity": "sha512-BJdMBY5YO9iHh+lPLYdHv6LbX+J8IcPCYMl1IJdBt2KDWNHwONHrPVHk3ttYBqJd9wxv84wlbN0f7GlQzcQtNQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-endpoints": "^3.4.0", + "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -4528,18 +4585,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.13", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.13.tgz", - "integrity": "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==", + "version": "3.23.14", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", + "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.21", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -4549,14 +4606,14 @@ } }, "node_modules/@smithy/core/node_modules/@smithy/util-stream": { - "version": "4.5.21", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.21.tgz", - "integrity": "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==", + "version": "4.5.22", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", + "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/types": "^4.13.1", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -4568,15 +4625,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", - "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", + "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -4649,14 +4706,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", - "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", + "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -4679,12 +4736,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", - "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.13.tgz", + "integrity": "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -4707,12 +4764,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", - "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz", + "integrity": "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4745,14 +4802,35 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/middleware-compression": { + "version": "4.3.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.3.43.tgz", + "integrity": "sha512-MphcLSNTvBN9G2/ko7NBV2psEfsQRZviXmf612ZwvbSY7dJZNroc2+WPHBf+I9KO2SFl4VFz11rTTueihwWjlQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@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.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", - "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz", + "integrity": "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4760,18 +4838,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.28", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.28.tgz", - "integrity": "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==", + "version": "4.4.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz", + "integrity": "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/middleware-serde": "^4.2.16", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-middleware": "^4.2.12", + "@smithy/core": "^3.23.14", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -4779,18 +4857,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.46", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.46.tgz", - "integrity": "sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.1.tgz", + "integrity": "sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.13", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/service-error-classification": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.1", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -4799,14 +4878,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.16.tgz", - "integrity": "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==", + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz", + "integrity": "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4814,12 +4893,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", - "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz", + "integrity": "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4827,14 +4906,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", - "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz", + "integrity": "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4842,14 +4921,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.1.tgz", - "integrity": "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", + "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4857,12 +4936,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", - "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.13.tgz", + "integrity": "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4870,12 +4949,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", - "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.13.tgz", + "integrity": "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4883,12 +4962,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", - "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz", + "integrity": "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -4897,12 +4976,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", - "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz", + "integrity": "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4910,24 +4989,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", - "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz", + "integrity": "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1" + "@smithy/types": "^4.14.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", - "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz", + "integrity": "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -4935,16 +5014,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", - "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", + "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.13", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -4954,17 +5033,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.8", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.8.tgz", - "integrity": "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==", + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.9.tgz", + "integrity": "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.21", + "@smithy/core": "^3.23.14", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" }, "engines": { @@ -4972,14 +5051,14 @@ } }, "node_modules/@smithy/smithy-client/node_modules/@smithy/util-stream": { - "version": "4.5.21", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.21.tgz", - "integrity": "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==", + "version": "4.5.22", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", + "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/types": "^4.13.1", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -4991,9 +5070,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.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", + "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5003,13 +5082,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", - "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.13.tgz", + "integrity": "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/querystring-parser": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -5080,14 +5159,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.44", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.44.tgz", - "integrity": "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==", + "version": "4.3.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz", + "integrity": "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -5095,17 +5174,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.48", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.48.tgz", - "integrity": "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==", + "version": "4.2.50", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.50.tgz", + "integrity": "sha512-xpjncL5XozFA3No7WypTsPU1du0fFS8flIyO+Wh2nhCy7bpEapvU7BR55Bg+wrfw+1cRA+8G8UsTjaxgzrMzXg==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.13", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@smithy/config-resolver": "^4.4.15", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -5113,13 +5192,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", - "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.0.tgz", + "integrity": "sha512-QQHGPKkw6NPcU6TJ1rNEEa201srPtZiX4k61xL163vvs9sTqW/XKz+UEuJ00uvPqoN+5Rs4Ka1UJ7+Mp03IXJw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -5139,12 +5218,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", - "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.13.tgz", + "integrity": "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -5152,13 +5231,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.13.tgz", - "integrity": "sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.1.tgz", + "integrity": "sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/service-error-classification": "^4.2.13", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -5380,12 +5459,12 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", - "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.15.tgz", + "integrity": "sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.14.0", "tslib": "^2.6.2" }, "engines": { @@ -9340,6 +9419,12 @@ "reusify": "^1.0.4" } }, + "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": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", diff --git a/package.json b/package.json index bba3d566..22eb5def 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "create-rdf-files": "vite-node --config vite.config.js setup/scripts/createRdfFiles.js" }, "dependencies": { + "@aws-sdk/client-cloudwatch": "^3.1030.0", "@aws-sdk/client-eventbridge": "^3.997.0", "@aws-sdk/client-lambda": "^3.775.0", "@aws-sdk/client-s3": "^3.540.0", diff --git a/serverless/src/publisher/__tests__/handler.test.js b/serverless/src/publisher/__tests__/handler.test.js index 17abf458..825c64c5 100644 --- a/serverless/src/publisher/__tests__/handler.test.js +++ b/serverless/src/publisher/__tests__/handler.test.js @@ -8,6 +8,7 @@ import { import { CsvComparator } from '@/shared/csvComparator' import { downloadConcepts } from '@/shared/downloadConcepts' +import { emitPublisherMetrics, PUBLISHER_METRIC_NAMES } from '@/shared/emitPublisherMetrics' import { getConceptSchemeDetails } from '@/shared/getConceptSchemeDetails' import { logger } from '@/shared/logger' import { getPublishUpdateQuery } from '@/shared/operations/updates/getPublishUpdateQuery' @@ -44,6 +45,7 @@ const waitForCondition = async (assertion, attempts = 20) => { // Mock the imported functions vi.mock('@/shared/csvComparator') vi.mock('@/shared/downloadConcepts') +vi.mock('@/shared/emitPublisherMetrics') vi.mock('@/shared/getConceptSchemeDetails') vi.mock('@/shared/operations/updates/getPublishUpdateQuery') vi.mock('@/shared/publishKeywordEvent') @@ -59,6 +61,7 @@ describe('publisher handler', () => { beforeEach(() => { vi.resetAllMocks() delete process.env.BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE + emitPublisherMetrics.mockResolvedValue() sendEventBridgeMock.mockResolvedValue({ FailedEntryCount: 0 }) publishKeywordEvent.mockResolvedValue({ messageId: 'message-1', @@ -966,11 +969,45 @@ describe('publisher handler', () => { versionName: 'v1.0.0', publishDate: mockEvent.detail.publishDate, published: true, + keywordChangesDetected: 1, + keywordEventsGenerated: 1, keywordEventsPublished: 1, + keywordEventPublishFailures: 0, cachePrimeEventEmitted: true, keywordEventsCount: 1, postPublishFailures: [] }) + + expect(emitPublisherMetrics).toHaveBeenNthCalledWith(1, { + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, + value: 1 + } + ] + }) + + expect(emitPublisherMetrics).toHaveBeenNthCalledWith(2, { + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + value: 1 + } + ] + }) + + expect(emitPublisherMetrics).toHaveBeenNthCalledWith(3, { + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, + value: 1 + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENT_PUBLISH_FAILURES, + value: 0 + } + ] + }) }) test('should fail the invocation before publish when keyword change generation fails', async () => { @@ -1037,6 +1074,8 @@ describe('publisher handler', () => { expect(sparqlRequest).toHaveBeenCalledTimes(1) expect(publishKeywordEvent).toHaveBeenCalledTimes(1) expect(result.status).toBe('success') + expect(result.keywordChangesDetected).toBe(1) + expect(result.keywordEventsGenerated).toBe(1) expect(result.keywordEventsPublished).toBe(1) expect(logger.warn).toHaveBeenCalledWith( '[publisher] Keyword changes detection failed for 1 scheme(s): platforms: Download failed. ' @@ -1139,11 +1178,27 @@ describe('publisher handler', () => { versionName: 'v1.0.0', publishDate: mockEvent.detail.publishDate, published: true, + keywordChangesDetected: 0, + keywordEventsGenerated: 0, keywordEventsPublished: 0, + keywordEventPublishFailures: 0, cachePrimeEventEmitted: true, keywordEventsCount: 0, postPublishFailures: [] }) + + expect(emitPublisherMetrics).toHaveBeenNthCalledWith(3, { + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, + value: 0 + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENT_PUBLISH_FAILURES, + value: 0 + } + ] + }) }) test('should not publish keyword events when the SPARQL publish update fails', async () => { @@ -1281,7 +1336,10 @@ describe('publisher handler', () => { versionName: 'v1.0.0', publishDate: mockEvent.detail.publishDate, published: true, + keywordChangesDetected: 0, + keywordEventsGenerated: 0, keywordEventsPublished: 0, + keywordEventPublishFailures: 0, cachePrimeEventEmitted: false, keywordEventsCount: 0, postPublishFailures: ['Publish completed, but failed to emit cache-prime event'] @@ -1327,7 +1385,10 @@ describe('publisher handler', () => { versionName: 'v1.0.0', publishDate: mockEvent.detail.publishDate, published: true, + keywordChangesDetected: 1, + keywordEventsGenerated: 1, keywordEventsPublished: 1, + keywordEventPublishFailures: 0, cachePrimeEventEmitted: false, keywordEventsCount: 1, postPublishFailures: ['Publish completed, but failed to emit cache-prime event'] @@ -1480,7 +1541,10 @@ describe('publisher handler', () => { versionName: 'v1.0.0', publishDate: mockEvent.detail.publishDate, published: true, + keywordChangesDetected: 2, + keywordEventsGenerated: 2, keywordEventsPublished: 1, + keywordEventPublishFailures: 1, cachePrimeEventEmitted: true, keywordEventsCount: 2, postPublishFailures: ['Publish completed, but 1 keyword event publishes failed after retries'] @@ -1506,6 +1570,19 @@ describe('publisher handler', () => { expect(logger.info).toHaveBeenCalledWith( '[publisher] Keyword event publish summary attempted=2 published=1 failed=1' ) + + expect(emitPublisherMetrics).toHaveBeenLastCalledWith({ + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, + value: 1 + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENT_PUBLISH_FAILURES, + value: 1 + } + ] + }) }) test('should log the number of published keyword events when SNS publish succeeds for all generated events', async () => { @@ -1543,12 +1620,49 @@ describe('publisher handler', () => { expect(publishKeywordEvent).toHaveBeenCalledTimes(2) expect(result.status).toBe('success') + expect(result.keywordChangesDetected).toBe(2) + expect(result.keywordEventsGenerated).toBe(2) expect(result.keywordEventsPublished).toBe(2) + expect(result.keywordEventPublishFailures).toBe(0) expect(logger.info).toHaveBeenCalledWith( '[publisher] Keyword event publish summary attempted=2 published=2 failed=0' ) }) + test('should log metric emission failures without failing publish', async () => { + const mockSchemes = [{ notation: 'sciencekeywords' }] + getConceptSchemeDetails.mockResolvedValue(mockSchemes) + downloadConcepts.mockResolvedValue('csv content') + emitPublisherMetrics.mockRejectedValue(new Error('CloudWatch unavailable')) + + const mockComparison = { + addedKeywords: new Map([['uuid1', { + oldPath: undefined, + newPath: 'PATH' + }]]), + removedKeywords: new Map(), + changedKeywords: new Map() + } + + const mockComparator = { + compare: vi.fn().mockReturnValue(mockComparison), + getSummary: vi.fn().mockReturnValue({ + addedCount: 1, + removedCount: 0, + changedCount: 0 + }) + } + + CsvComparator.mockImplementation(() => mockComparator) + + const result = await publisher(mockEvent) + + expect(result.status).toBe('success') + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('[publisher] Failed to emit keyword sync metrics for keyword change detection.') + ) + }) + test('should not publish keyword events to SNS when the SPARQL publish update fails', async () => { const mockSchemes = [{ notation: 'sciencekeywords' }] getConceptSchemeDetails.mockResolvedValue(mockSchemes) diff --git a/serverless/src/publisher/handler.js b/serverless/src/publisher/handler.js index 56f66e5d..49e13424 100644 --- a/serverless/src/publisher/handler.js +++ b/serverless/src/publisher/handler.js @@ -2,6 +2,7 @@ import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge import { CsvComparator } from '@/shared/csvComparator' import { downloadConcepts } from '@/shared/downloadConcepts' +import { emitPublisherMetrics, PUBLISHER_METRIC_NAMES } from '@/shared/emitPublisherMetrics' import { getConceptSchemeDetails } from '@/shared/getConceptSchemeDetails' import { logger } from '@/shared/logger' import { getPublishUpdateQuery } from '@/shared/operations/updates/getPublishUpdateQuery' @@ -14,10 +15,54 @@ const KEYWORD_EVENT_PUBLISH_RETRIES = 3 const publisherEventClient = new EventBridgeClient({}) +/** + * Indicates whether keyword diff failures should block publish completion. + * + * This toggle lets us roll out the metrics/eventing path without making + * keyword comparison failures fatal in environments where the downstream flow + * is still being validated. + * + * @returns {boolean} True when publish should fail on keyword diff errors. + */ const shouldBlockPublishOnKeywordDiffFailure = () => ( process.env.BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE === 'true' ) +/** + * Counts the total number of added, removed, and changed keywords across all schemes. + * + * @param {Map} keywordChangesMap - Per-scheme comparison results. + * @returns {number} Total keyword changes detected for the publish run. + */ +const countKeywordChanges = (keywordChangesMap) => Array.from(keywordChangesMap.values()).reduce( + (total, changes) => total + + changes.addedKeywords.size + + changes.removedKeywords.size + + changes.changedKeywords.size, + 0 +) + +/** + * Emits publisher metrics without failing the overall publish flow on metric errors. + * + * Metrics are observability-only for this path, so failed emission is logged for + * debugging but does not block publish completion. + * + * @param {Array<{metricName: string, value: number}>} metrics - Metrics to emit. + * @param {string} context - Human-readable context used in failure logs. + * @returns {Promise} + */ +const emitPublisherMetricsSafely = async (metrics, context) => { + try { + await emitPublisherMetrics({ metrics }) + } catch (metricError) { + logger.error( + `[publisher] Failed to emit keyword sync metrics for ${context}. ` + + `Error: ${metricError.message}` + ) + } +} + /** * Transform keyword changes map into a list of keyword events conforming to the schema. * @@ -343,8 +388,7 @@ const emitCachingEvent = async ({ versionName, publishDate, keywordEvents }) => * attemptedCount: number, * publishedCount: number, * failedEvents: Array<{ keywordEvent: Object, error: string, attempts: number }> - * }>} - * SNS publish summary for the completed batch. + * }>} SNS publish summary for the completed batch. */ const publishKeywordEvents = async (keywordEvents) => keywordEvents.reduce( async (summaryPromise, keywordEvent) => { @@ -441,16 +485,38 @@ export const publisher = async (event) => { logger.info(`[publisher] Starting analysis for version=${versionName}`) const keywordChanges = await getKeywordChanges() + const keywordChangesDetected = countKeywordChanges(keywordChanges) // Log summary of all changes const totalSchemes = keywordChanges.size logger.info(`[publisher] Analysis completed. Processed ${totalSchemes} concept schemes.`) + await emitPublisherMetricsSafely( + [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, + value: keywordChangesDetected + } + ], + 'keyword change detection' + ) + const keywordEvents = createKeywordEvents(keywordChanges) + const keywordEventsGenerated = keywordEvents.length logger.info(`[publisher] Created ${keywordEvents.length} keyword events`) logger.info('[publisher] Keyword Events:', keywordEvents) + await emitPublisherMetricsSafely( + [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + value: keywordEventsGenerated + } + ], + 'keyword event generation' + ) + // Execute the publish operation logger.info(`[publisher] Executing publish update for version=${versionName}`) const publishQuery = getPublishUpdateQuery(versionName, publishDate) @@ -469,11 +535,13 @@ export const publisher = async (event) => { logger.info(`[publisher] Publish update completed for version=${versionName}`) let keywordEventsPublished = 0 + let keywordEventPublishFailures = 0 const postPublishFailures = [] if (keywordEvents.length > 0) { const publishSummary = await publishKeywordEvents(keywordEvents) keywordEventsPublished = publishSummary.publishedCount + keywordEventPublishFailures = publishSummary.failedEvents.length if (publishSummary.failedEvents.length > 0) { postPublishFailures.push( @@ -507,6 +575,20 @@ export const publisher = async (event) => { logger.info('[publisher] No keyword events generated, skipping SNS publish') } + await emitPublisherMetricsSafely( + [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, + value: keywordEventsPublished + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENT_PUBLISH_FAILURES, + value: keywordEventPublishFailures + } + ], + 'keyword event publish summary' + ) + // Emit event for cache-prime to consume let cachePrimeEventEmitted = false @@ -531,7 +613,10 @@ export const publisher = async (event) => { versionName, publishDate, published: true, + keywordChangesDetected, + keywordEventsGenerated, keywordEventsPublished, + keywordEventPublishFailures, cachePrimeEventEmitted, keywordEventsCount: keywordEvents.length, postPublishFailures diff --git a/serverless/src/shared/__tests__/emitPublisherMetrics.test.js b/serverless/src/shared/__tests__/emitPublisherMetrics.test.js new file mode 100644 index 00000000..b499be93 --- /dev/null +++ b/serverless/src/shared/__tests__/emitPublisherMetrics.test.js @@ -0,0 +1,174 @@ +import { + beforeEach, + describe, + expect, + test, + vi +} from 'vitest' + +import { logger } from '@/shared/logger' + +import { + emitPublisherMetrics, + PUBLISHER_METRIC_NAMES, + PUBLISHER_METRIC_NAMESPACE +} from '../emitPublisherMetrics' + +const { + CloudWatchClientMock, + fetchMock, + sendCloudWatchMock, + PutMetricDataCommandMock +} = vi.hoisted(() => ({ + CloudWatchClientMock: vi.fn(() => ({ + send: sendCloudWatchMock + })), + fetchMock: vi.fn(), + sendCloudWatchMock: vi.fn(), + PutMetricDataCommandMock: vi.fn((input) => input) +})) + +vi.mock('@aws-sdk/client-cloudwatch', () => ({ + CloudWatchClient: CloudWatchClientMock, + PutMetricDataCommand: PutMetricDataCommandMock +})) + +describe('emitPublisherMetrics', () => { + beforeEach(() => { + vi.resetAllMocks() + CloudWatchClientMock.mockImplementation(() => ({ + send: sendCloudWatchMock + })) + + vi.spyOn(logger, 'info').mockImplementation(() => {}) + vi.stubGlobal('fetch', fetchMock) + delete process.env.AWS_ENDPOINT_URL + sendCloudWatchMock.mockResolvedValue({}) + }) + + test('should publish the provided publisher metrics with the current stage dimension', async () => { + await emitPublisherMetrics({ + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, + value: 3 + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, + value: 2 + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENT_PUBLISH_FAILURES, + value: 1 + } + ] + }) + + expect(PutMetricDataCommandMock).toHaveBeenCalledWith({ + Namespace: PUBLISHER_METRIC_NAMESPACE, + MetricData: [ + { + MetricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, + Unit: 'Count', + Value: 3 + }, + { + MetricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, + Unit: 'Count', + Value: 2 + }, + { + MetricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENT_PUBLISH_FAILURES, + Unit: 'Count', + Value: 1 + } + ] + }) + + expect(sendCloudWatchMock).toHaveBeenCalledTimes(1) + expect(fetchMock).not.toHaveBeenCalled() + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('KeywordChangesDetected:3') + ) + }) + + test('should publish metrics without dimensions', async () => { + await emitPublisherMetrics({ + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + value: 0 + } + ] + }) + + expect(PutMetricDataCommandMock).toHaveBeenCalledWith({ + Namespace: PUBLISHER_METRIC_NAMESPACE, + MetricData: [ + { + MetricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + Unit: 'Count', + Value: 0 + } + ] + }) + + expect(fetchMock).not.toHaveBeenCalled() + }) + + test('should emit metrics to LocalStack CloudWatch with the query api when AWS_ENDPOINT_URL is set', async () => { + process.env.AWS_ENDPOINT_URL = 'http://localhost:4566' + fetchMock.mockResolvedValue({ + ok: true + }) + + await emitPublisherMetrics({ + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + value: 1 + } + ] + }) + + expect(sendCloudWatchMock).not.toHaveBeenCalled() + expect(fetchMock).toHaveBeenCalledWith('http://localhost:4566', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' + }, + body: expect.any(String) + }) + + const requestBody = new URLSearchParams(fetchMock.mock.calls[0][1].body) + + expect(requestBody.get('Action')).toBe('PutMetricData') + expect(requestBody.get('Version')).toBe('2010-08-01') + expect(requestBody.get('Namespace')).toBe(PUBLISHER_METRIC_NAMESPACE) + expect(requestBody.get('MetricData.member.1.MetricName')).toBe( + PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED + ) + + expect(requestBody.get('MetricData.member.1.Value')).toBe('1') + expect(requestBody.get('MetricData.member.1.Dimensions.member.1.Name')).toBeNull() + expect(requestBody.get('MetricData.member.1.Dimensions.member.1.Value')).toBeNull() + }) + + test('should throw when the LocalStack CloudWatch query request fails', async () => { + process.env.AWS_ENDPOINT_URL = 'http://localhost:4566' + fetchMock.mockResolvedValue({ + ok: false, + status: 500, + text: vi.fn().mockResolvedValue('') + }) + + await expect(emitPublisherMetrics({ + metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENT_PUBLISH_FAILURES, + value: 1 + } + ] + })).rejects.toThrow('Failed to emit keyword sync metrics to LocalStack') + }) +}) diff --git a/serverless/src/shared/emitPublisherMetrics.js b/serverless/src/shared/emitPublisherMetrics.js new file mode 100644 index 00000000..bc36773d --- /dev/null +++ b/serverless/src/shared/emitPublisherMetrics.js @@ -0,0 +1,137 @@ +import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch' + +import { logger } from './logger' + +/** + * Shared metric namespace used for keyword sync monitoring. + * @type {string} + */ +export const PUBLISHER_METRIC_NAMESPACE = 'KMS/KeywordSync' + +/** + * Publisher metric names emitted during keyword analysis and event publishing. + * @type {{[key: string]: string}} + */ +export const PUBLISHER_METRIC_NAMES = { + KEYWORD_CHANGES_DETECTED: 'KeywordChangesDetected', + KEYWORD_EVENTS_GENERATED: 'KeywordEventsGenerated', + KEYWORD_EVENTS_PUBLISHED: 'KeywordEventsPublished', + KEYWORD_EVENT_PUBLISH_FAILURES: 'KeywordEventPublishFailures' +} + +/** + * Creates a CloudWatch client for standard AWS metric emission. + * + * @returns {CloudWatchClient} Configured CloudWatch client instance. + */ +const createCloudWatchClient = () => new CloudWatchClient({}) + +const cloudWatchClient = createCloudWatchClient() + +/** + * Converts publisher metric name/value pairs into CloudWatch MetricData entries. + * + * @param {Object} params - Metric serialization details. + * @param {Array<{metricName: string, value: number}>} params.metrics - Metrics to emit. + * @returns {Array} CloudWatch MetricData payload. + */ +const buildMetricData = ({ metrics }) => metrics.map(({ metricName, value }) => ({ + MetricName: metricName, + Unit: 'Count', + Value: value +})) + +/** + * Builds a CloudWatch Query API request body for metric emission. + * + * LocalStack's CloudWatch emulation currently responds more reliably to the + * classic Query API shape than the SDK's JSON/protocol path, so local metric + * tests use this form while deployed AWS continues to use the SDK client. + * + * @param {Object} params - Query serialization details. + * @param {Array<{metricName: string, value: number}>} params.metrics - Metrics to emit. + * @returns {string} URL-encoded request body for PutMetricData. + */ +const buildCloudWatchQueryRequestBody = ({ metrics }) => { + const params = new URLSearchParams({ + Action: 'PutMetricData', + Version: '2010-08-01', + Namespace: PUBLISHER_METRIC_NAMESPACE + }) + + metrics.forEach(({ metricName, value }, index) => { + const metricIndex = index + 1 + + params.set(`MetricData.member.${metricIndex}.MetricName`, metricName) + params.set(`MetricData.member.${metricIndex}.Unit`, 'Count') + params.set(`MetricData.member.${metricIndex}.Value`, String(value)) + }) + + return params.toString() +} + +/** + * Emits publisher metrics through the CloudWatch Query API. + * + * This path is used for local LocalStack verification, where the CloudWatch + * endpoint is provided through AWS_ENDPOINT_URL. + * + * @param {Object} params - Query API emission details. + * @param {string} params.endpoint - Local CloudWatch-compatible endpoint. + * @param {Array<{metricName: string, value: number}>} params.metrics - Metrics to emit. + * @returns {Promise} + */ +const emitPublisherMetricsWithQueryApi = async ({ endpoint, metrics }) => { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' + }, + body: buildCloudWatchQueryRequestBody({ + metrics + }) + }) + + if (!response.ok) { + const responseBody = await response.text() + + throw new Error( + 'Failed to emit keyword sync metrics to LocalStack. ' + + `Status: ${response.status}. Response: ${responseBody}` + ) + } +} + +/** + * Emits one or more publisher metrics to CloudWatch. + * + * @param {Object} params - Metric emission details. + * @param {Array<{metricName: string, value: number}>} params.metrics - Metrics to emit. + * @returns {Promise} + */ +export const emitPublisherMetrics = async ({ metrics }) => { + const endpoint = process.env.AWS_ENDPOINT_URL + const metricData = buildMetricData({ + metrics + }) + + if (endpoint) { + await emitPublisherMetricsWithQueryApi({ + endpoint, + metrics + }) + } else { + await cloudWatchClient.send(new PutMetricDataCommand({ + Namespace: PUBLISHER_METRIC_NAMESPACE, + MetricData: metricData + })) + } + + logger.info( + '[publisher] Emitted publisher metrics ' + + `namespace=${PUBLISHER_METRIC_NAMESPACE} ` + + `metrics=${metrics.map(({ metricName, value }) => `${metricName}:${value}`).join(',')}` + ) +} + +export default emitPublisherMetrics From af0ed6172044e1cf3ddd269957e9ae59e7a6b2f7 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Fri, 17 Apr 2026 09:27:17 -0400 Subject: [PATCH 2/4] KMS-661: Added script to read local metrics --- scripts/local/show_keyword_sync_metrics.js | 192 +++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100755 scripts/local/show_keyword_sync_metrics.js diff --git a/scripts/local/show_keyword_sync_metrics.js b/scripts/local/show_keyword_sync_metrics.js new file mode 100755 index 00000000..bf6466fb --- /dev/null +++ b/scripts/local/show_keyword_sync_metrics.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +const CLOUDWATCH_API_VERSION = '2010-08-01' +const DEFAULT_ENDPOINT = 'http://localhost:4566' +const DEFAULT_NAMESPACE = 'KMS/KeywordSync' +const DEFAULT_LOOKBACK_MINUTES = 60 +const DEFAULT_PERIOD_SECONDS = 60 +const DEFAULT_STATISTIC = 'Sum' + +const createParser = async () => { + const { XMLParser } = await import('fast-xml-parser') + + return new XMLParser({ + ignoreAttributes: false, + parseAttributeValue: false, + parseTagValue: false, + trimValues: true + }) +} + +const toArray = (value) => { + if (!value) { + return [] + } + + return Array.isArray(value) ? value : [value] +} + +const formatNumber = (value) => { + const numericValue = Number(value) + + if (!Number.isFinite(numericValue)) { + return String(value) + } + + return Number.isInteger(numericValue) ? String(numericValue) : numericValue.toFixed(2) +} + +const postCloudWatchQuery = async ({ endpoint, params }) => { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' + }, + body: new URLSearchParams(params).toString() + }) + + if (!response.ok) { + const responseBody = await response.text() + + throw new Error( + `CloudWatch query failed. Status=${response.status}. Response=${responseBody}` + ) + } + + return response.text() +} + +const listMetricNames = async ({ endpoint, namespace, parser }) => { + const responseXml = await postCloudWatchQuery({ + endpoint, + params: { + Action: 'ListMetrics', + Version: CLOUDWATCH_API_VERSION, + Namespace: namespace + } + }) + + const result = parser.parse(responseXml) + const metrics = toArray(result?.ListMetricsResponse?.ListMetricsResult?.Metrics?.member) + + return [...new Set(metrics.map((metric) => metric?.MetricName).filter(Boolean))].sort() +} + +const getMetricDatapoints = async ({ + endpoint, + namespace, + metricName, + startTime, + endTime, + periodSeconds, + statistic, + parser +}) => { + const responseXml = await postCloudWatchQuery({ + endpoint, + params: { + Action: 'GetMetricStatistics', + Version: CLOUDWATCH_API_VERSION, + Namespace: namespace, + MetricName: metricName, + StartTime: startTime, + EndTime: endTime, + Period: String(periodSeconds), + 'Statistics.member.1': statistic + } + }) + + const result = parser.parse(responseXml) + const datapoints = toArray( + result?.GetMetricStatisticsResponse?.GetMetricStatisticsResult?.Datapoints?.member + ) + + return datapoints + .map((datapoint) => ({ + timestamp: datapoint?.Timestamp, + value: datapoint?.[statistic] + })) + .filter((datapoint) => datapoint.timestamp && datapoint.value !== undefined) + .sort((left, right) => new Date(right.timestamp) - new Date(left.timestamp)) +} + +const main = async () => { + const parser = await createParser() + const endpoint = process.env.LOCALSTACK_ENDPOINT + || process.env.AWS_ENDPOINT_URL + || DEFAULT_ENDPOINT + const namespace = process.env.METRIC_NAMESPACE || DEFAULT_NAMESPACE + const lookbackMinutes = Number(process.env.LOOKBACK_MINUTES || DEFAULT_LOOKBACK_MINUTES) + const periodSeconds = Number(process.env.PERIOD_SECONDS || DEFAULT_PERIOD_SECONDS) + const statistic = process.env.METRIC_STATISTIC || DEFAULT_STATISTIC + + const endTime = new Date() + const startTime = new Date(endTime.getTime() - (lookbackMinutes * 60 * 1000)) + + const startIso = startTime.toISOString() + const endIso = endTime.toISOString() + + console.log(`Endpoint: ${endpoint}`) + console.log(`Namespace: ${namespace}`) + console.log(`Window: ${startIso} -> ${endIso}`) + console.log(`Statistic: ${statistic}`) + console.log(`Period: ${periodSeconds}s`) + console.log('') + + const metricNames = await listMetricNames({ + endpoint, + namespace, + parser + }) + + if (metricNames.length === 0) { + console.log(`No metrics found in namespace ${namespace}`) + + return + } + + const metricSummaries = await Promise.all(metricNames.map(async (metricName) => ({ + metricName, + datapoints: await getMetricDatapoints({ + endpoint, + namespace, + metricName, + startTime: startIso, + endTime: endIso, + periodSeconds, + statistic, + parser + }) + }))) + + metricSummaries.forEach(({ metricName, datapoints }) => { + console.log(metricName) + + if (datapoints.length === 0) { + console.log(' latest: no datapoints in selected window') + console.log('') + } else { + const [latestDatapoint] = datapoints + + console.log( + ` latest: ${formatNumber(latestDatapoint.value)} @ ${latestDatapoint.timestamp}` + ) + + console.log(' datapoints:') + + datapoints + .slice() + .reverse() + .forEach((datapoint) => { + console.log(` - ${datapoint.timestamp}: ${formatNumber(datapoint.value)}`) + }) + + console.log('') + } + }) +} + +main().catch((error) => { + console.error(error.message) + process.exit(1) +}) From d662c535f47d856870709a510599139345e1d113 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Tue, 21 Apr 2026 17:14:17 -0400 Subject: [PATCH 3/4] KMS-661: PR feedback. --- serverless/src/publisher/handler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/serverless/src/publisher/handler.js b/serverless/src/publisher/handler.js index 49e13424..970cf816 100644 --- a/serverless/src/publisher/handler.js +++ b/serverless/src/publisher/handler.js @@ -35,10 +35,10 @@ const shouldBlockPublishOnKeywordDiffFailure = () => ( * @returns {number} Total keyword changes detected for the publish run. */ const countKeywordChanges = (keywordChangesMap) => Array.from(keywordChangesMap.values()).reduce( - (total, changes) => total - + changes.addedKeywords.size - + changes.removedKeywords.size - + changes.changedKeywords.size, + (total, { addedKeywords, removedKeywords, changedKeywords }) => total + + addedKeywords.size + + removedKeywords.size + + changedKeywords.size, 0 ) From f28986a37421c598f36c07f5c09398b2c836a3e9 Mon Sep 17 00:00:00 2001 From: "Christopher D. Gokey" Date: Tue, 21 Apr 2026 17:26:05 -0400 Subject: [PATCH 4/4] KMS-660: More PR feedback --- .../lib/helper/KeywordSyncMonitoringSetup.ts | 3 +- .../src/publisher/__tests__/handler.test.js | 38 +++++++++++-------- serverless/src/publisher/handler.js | 30 +++++---------- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts b/cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts index 764b71ca..76dec330 100644 --- a/cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts +++ b/cdk/app/lib/helper/KeywordSyncMonitoringSetup.ts @@ -7,6 +7,7 @@ import { Construct } from 'constructs' const KEYWORD_SYNC_METRIC_NAMESPACE = 'KMS/KeywordSync' const KEYWORD_EVENT_PUBLISH_FAILURES_METRIC = 'KeywordEventPublishFailures' +const PUBLISH_FAILURE_ALARM_PERIOD = cdk.Duration.days(1) export interface KeywordSyncMonitoringSetupProps { prefix: string @@ -33,7 +34,7 @@ export class KeywordSyncMonitoringSetup { namespace: KEYWORD_SYNC_METRIC_NAMESPACE, metricName: KEYWORD_EVENT_PUBLISH_FAILURES_METRIC, statistic: 'Sum', - period: cdk.Duration.minutes(5) + period: PUBLISH_FAILURE_ALARM_PERIOD }) this.publishFailuresAlarm = new cloudwatch.Alarm(scope, 'KeywordSyncPublishFailuresAlarm', { diff --git a/serverless/src/publisher/__tests__/handler.test.js b/serverless/src/publisher/__tests__/handler.test.js index 825c64c5..329da2c6 100644 --- a/serverless/src/publisher/__tests__/handler.test.js +++ b/serverless/src/publisher/__tests__/handler.test.js @@ -978,26 +978,17 @@ describe('publisher handler', () => { postPublishFailures: [] }) - expect(emitPublisherMetrics).toHaveBeenNthCalledWith(1, { + expect(emitPublisherMetrics).toHaveBeenCalledTimes(1) + expect(emitPublisherMetrics).toHaveBeenCalledWith({ metrics: [ { metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, value: 1 - } - ] - }) - - expect(emitPublisherMetrics).toHaveBeenNthCalledWith(2, { - metrics: [ + }, { metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, value: 1 - } - ] - }) - - expect(emitPublisherMetrics).toHaveBeenNthCalledWith(3, { - metrics: [ + }, { metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, value: 1 @@ -1187,8 +1178,17 @@ describe('publisher handler', () => { postPublishFailures: [] }) - expect(emitPublisherMetrics).toHaveBeenNthCalledWith(3, { + expect(emitPublisherMetrics).toHaveBeenCalledTimes(1) + expect(emitPublisherMetrics).toHaveBeenCalledWith({ metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, + value: 0 + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + value: 0 + }, { metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, value: 0 @@ -1573,6 +1573,14 @@ describe('publisher handler', () => { expect(emitPublisherMetrics).toHaveBeenLastCalledWith({ metrics: [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, + value: 2 + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + value: 2 + }, { metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, value: 1 @@ -1659,7 +1667,7 @@ describe('publisher handler', () => { expect(result.status).toBe('success') expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining('[publisher] Failed to emit keyword sync metrics for keyword change detection.') + expect.stringContaining('[publisher] Failed to emit keyword sync metrics for keyword sync summary.') ) }) diff --git a/serverless/src/publisher/handler.js b/serverless/src/publisher/handler.js index 970cf816..1b8a5929 100644 --- a/serverless/src/publisher/handler.js +++ b/serverless/src/publisher/handler.js @@ -491,32 +491,12 @@ export const publisher = async (event) => { const totalSchemes = keywordChanges.size logger.info(`[publisher] Analysis completed. Processed ${totalSchemes} concept schemes.`) - await emitPublisherMetricsSafely( - [ - { - metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, - value: keywordChangesDetected - } - ], - 'keyword change detection' - ) - const keywordEvents = createKeywordEvents(keywordChanges) const keywordEventsGenerated = keywordEvents.length logger.info(`[publisher] Created ${keywordEvents.length} keyword events`) logger.info('[publisher] Keyword Events:', keywordEvents) - await emitPublisherMetricsSafely( - [ - { - metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, - value: keywordEventsGenerated - } - ], - 'keyword event generation' - ) - // Execute the publish operation logger.info(`[publisher] Executing publish update for version=${versionName}`) const publishQuery = getPublishUpdateQuery(versionName, publishDate) @@ -577,6 +557,14 @@ export const publisher = async (event) => { await emitPublisherMetricsSafely( [ + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_CHANGES_DETECTED, + value: keywordChangesDetected + }, + { + metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_GENERATED, + value: keywordEventsGenerated + }, { metricName: PUBLISHER_METRIC_NAMES.KEYWORD_EVENTS_PUBLISHED, value: keywordEventsPublished @@ -586,7 +574,7 @@ export const publisher = async (event) => { value: keywordEventPublishFailures } ], - 'keyword event publish summary' + 'keyword sync summary' ) // Emit event for cache-prime to consume