From e65f17be03a1164ab01682b49ae53b844cfcb548 Mon Sep 17 00:00:00 2001 From: Mark Slowey Date: Thu, 12 Feb 2026 15:39:05 +0000 Subject: [PATCH 1/3] console.log the metric to be picked up --- lambdas/authorizer/src/__tests__/index.test.ts | 17 +++++++---------- lambdas/authorizer/src/authorizer.ts | 8 +++----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts index 4020b55e..cecfbe10 100644 --- a/lambdas/authorizer/src/__tests__/index.test.ts +++ b/lambdas/authorizer/src/__tests__/index.test.ts @@ -56,14 +56,18 @@ describe("Authorizer Lambda Function", () => { }); describe("Certificate expiry check", () => { + let consoleLogSpy: jest.SpyInstance; + beforeEach(() => { jest .useFakeTimers({ doNotFake: ["nextTick"] }) .setSystemTime(new Date("2025-11-03T14:19:00Z")); + consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); }); afterEach(() => { jest.useRealTimers(); + consoleLogSpy.mockRestore(); }); it("Should not log CloudWatch metric when certificate is null", async () => { @@ -73,10 +77,7 @@ describe("Authorizer Lambda Function", () => { handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - const mockedInfo = mockedDeps.logger.info as jest.Mock; - expect(mockedInfo.mock.calls).not.toContainEqual( - expect.stringContaining("CloudWatchMetrics"), - ); + expect(consoleLogSpy).not.toHaveBeenCalled(); }); it("Should log CloudWatch metric when the certificate expiry threshold is reached", async () => { @@ -88,8 +89,7 @@ describe("Authorizer Lambda Function", () => { handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - const mockedInfo = mockedDeps.logger.info as jest.Mock; - expect(mockedInfo.mock.calls.map((call) => call[0])).toContain( + expect(consoleLogSpy).toHaveBeenCalledWith( JSON.stringify({ _aws: { Timestamp: 1_762_179_540_000, @@ -123,10 +123,7 @@ describe("Authorizer Lambda Function", () => { handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - const mockedInfo = mockedDeps.logger.info as jest.Mock; - expect(mockedInfo.mock.calls).not.toContainEqual( - expect.stringContaining("CloudWatchMetrics"), - ); + expect(consoleLogSpy).not.toHaveBeenCalled(); }); }); diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index d5a0bd55..87eb65b4 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -146,10 +146,8 @@ async function checkCertificateExpiry( const expiry = getCertificateExpiryInDays(certificate); if (expiry <= deps.env.CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS) { - deps.logger.info( - JSON.stringify( - buildCloudWatchMetric(deps.env.CLOUDWATCH_NAMESPACE, certificate), - ), - ); + let metric = buildCloudWatchMetric(deps.env.CLOUDWATCH_NAMESPACE, certificate); + deps.logger.warn(metric, `APIM Certificated expiry in ${expiry} days`); + console.log(JSON.stringify(metric)); } } From c1425c5193d50008cd08a9523b91b1c1167bfa42 Mon Sep 17 00:00:00 2001 From: Mark Slowey Date: Thu, 12 Feb 2026 16:13:28 +0000 Subject: [PATCH 2/3] lint --- lambdas/authorizer/src/authorizer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index 87eb65b4..37b2224d 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -146,7 +146,10 @@ async function checkCertificateExpiry( const expiry = getCertificateExpiryInDays(certificate); if (expiry <= deps.env.CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS) { - let metric = buildCloudWatchMetric(deps.env.CLOUDWATCH_NAMESPACE, certificate); + const metric = buildCloudWatchMetric( + deps.env.CLOUDWATCH_NAMESPACE, + certificate, + ); deps.logger.warn(metric, `APIM Certificated expiry in ${expiry} days`); console.log(JSON.stringify(metric)); } From 98dd7cebc1f96760fd104959cd05d29d9e20f97f Mon Sep 17 00:00:00 2001 From: Mark Slowey Date: Fri, 13 Feb 2026 12:03:11 +0000 Subject: [PATCH 3/3] move to use metricScope --- .../authorizer/src/__tests__/index.test.ts | 73 ++++++++++++------- lambdas/authorizer/src/authorizer.ts | 44 +++-------- 2 files changed, 54 insertions(+), 63 deletions(-) diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts index cecfbe10..58041e86 100644 --- a/lambdas/authorizer/src/__tests__/index.test.ts +++ b/lambdas/authorizer/src/__tests__/index.test.ts @@ -4,11 +4,30 @@ import { Callback, Context, } from "aws-lambda"; +import { metricScope } from "aws-embedded-metrics"; import pino from "pino"; import { Deps } from "../deps"; import { EnvVars } from "../env"; import createAuthorizerHandler from "../authorizer"; +jest.mock("aws-embedded-metrics", () => { + const metricsMock = { + setNamespace: jest.fn(), + putMetric: jest.fn(), + }; + + return { + metricScope: jest.fn((handler: (metrics: typeof metricsMock) => unknown) => { + const wrapped = handler(metricsMock); + if (typeof wrapped === "function") { + return wrapped(); + } + return undefined; + }), + __metricsMock: metricsMock, + }; +}); + const mockedDeps: jest.Mocked = { logger: { info: jest.fn(), @@ -56,18 +75,25 @@ describe("Authorizer Lambda Function", () => { }); describe("Certificate expiry check", () => { - let consoleLogSpy: jest.SpyInstance; - beforeEach(() => { jest .useFakeTimers({ doNotFake: ["nextTick"] }) .setSystemTime(new Date("2025-11-03T14:19:00Z")); - consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); + (metricScope as jest.Mock).mockClear(); + const metricsMock = (jest.requireMock( + "aws-embedded-metrics", + ) as { + __metricsMock: { + setNamespace: jest.Mock; + putMetric: jest.Mock; + }; + }).__metricsMock; + metricsMock.setNamespace.mockClear(); + metricsMock.putMetric.mockClear(); }); afterEach(() => { jest.useRealTimers(); - consoleLogSpy.mockRestore(); }); it("Should not log CloudWatch metric when certificate is null", async () => { @@ -77,7 +103,7 @@ describe("Authorizer Lambda Function", () => { handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - expect(consoleLogSpy).not.toHaveBeenCalled(); + expect(metricScope).not.toHaveBeenCalled(); }); it("Should log CloudWatch metric when the certificate expiry threshold is reached", async () => { @@ -89,28 +115,19 @@ describe("Authorizer Lambda Function", () => { handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - expect(consoleLogSpy).toHaveBeenCalledWith( - JSON.stringify({ - _aws: { - Timestamp: 1_762_179_540_000, - CloudWatchMetrics: [ - { - Namespace: "cloudwatch-namespace", - Dimensions: ["SUBJECT_DN", "NOT_AFTER"], - Metrics: [ - { - Name: "apim-client-certificate-near-expiry", - Unit: "Count", - Value: 1, - }, - ], - }, - ], - }, - SUBJECT_DN: "CN=test-subject", - NOT_AFTER: "2025-11-17T14:19:00Z", - "apim-client-certificate-near-expiry": 1, - }), + const metricsMock = (jest.requireMock("aws-embedded-metrics") as { + __metricsMock: { + setNamespace: jest.Mock; + putMetric: jest.Mock; + }; + }).__metricsMock; + + expect(metricScope).toHaveBeenCalledTimes(1); + expect(metricsMock.setNamespace).toHaveBeenCalledWith("authorizer"); + expect(metricsMock.putMetric).toHaveBeenCalledWith( + "apim-client-certificate-near-expiry", + 14, + "Count", ); }); @@ -123,7 +140,7 @@ describe("Authorizer Lambda Function", () => { handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - expect(consoleLogSpy).not.toHaveBeenCalled(); + expect(metricScope).not.toHaveBeenCalled(); }); }); diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index 37b2224d..0f7c5eb8 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -6,7 +6,8 @@ import { APIGatewayRequestAuthorizerHandler, Callback, Context, -} from "aws-lambda"; +} from "aws-lambda" +import { MetricsLogger, metricScope } from "aws-embedded-metrics"; import { Supplier } from "@internal/datastore"; import { Deps } from "./deps"; @@ -100,33 +101,6 @@ function getCertificateExpiryInDays( return (expiry - now) / (1000 * 60 * 60 * 24); } -function buildCloudWatchMetric( - namespace: string, - certificate: APIGatewayEventClientCertificate, -) { - return { - _aws: { - Timestamp: Date.now(), - CloudWatchMetrics: [ - { - Namespace: namespace, - Dimensions: ["SUBJECT_DN", "NOT_AFTER"], - Metrics: [ - { - Name: "apim-client-certificate-near-expiry", - Unit: "Count", - Value: 1, - }, - ], - }, - ], - }, - SUBJECT_DN: certificate.subjectDN, - NOT_AFTER: certificate.validity.notAfter, - "apim-client-certificate-near-expiry": 1, - }; -} - async function checkCertificateExpiry( certificate: APIGatewayEventClientCertificate | null, deps: Deps, @@ -146,11 +120,11 @@ async function checkCertificateExpiry( const expiry = getCertificateExpiryInDays(certificate); if (expiry <= deps.env.CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS) { - const metric = buildCloudWatchMetric( - deps.env.CLOUDWATCH_NAMESPACE, - certificate, - ); - deps.logger.warn(metric, `APIM Certificated expiry in ${expiry} days`); - console.log(JSON.stringify(metric)); - } + metricScope( + (metrics: MetricsLogger) => async () => { + deps.logger.warn(`APIM Certificated expiry in ${expiry} days`); + metrics.setNamespace(process.env.AWS_LAMBDA_FUNCTION_NAME || 'authorizer'); + metrics.putMetric("apim-client-certificate-near-expiry", expiry, "Count"); + }); + }; }