From 17a9a038ca4c7b095cbd948c68bbe31786f42a32 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 11 Nov 2024 09:30:12 +0600 Subject: [PATCH 01/11] feat: `setTimestamp` function --- packages/metrics/src/Metrics.ts | 74 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index abe8fac81b..8984bb0e62 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -1,5 +1,5 @@ import { Console } from 'node:console'; -import { Utility } from '@aws-lambda-powertools/commons'; +import { Utility, isIntegerNumber } from '@aws-lambda-powertools/commons'; import type { GenericLogger, HandlerMethodDecorator, @@ -198,6 +198,11 @@ class Metrics extends Utility implements MetricsInterface { */ private storedMetrics: StoredMetrics = {}; + /** + * Custom timestamp for the metrics + */ + #timestamp?: number; + public constructor(options: MetricsOptions = {}) { super(); @@ -571,6 +576,23 @@ class Metrics extends Utility implements MetricsInterface { this.clearMetadata(); } + /** + * Sets the timestamp for the metric. + * If an integer is provided, it is assumed to be the epoch time in milliseconds. + * If a Date object is provided, it will be converted to epoch time in milliseconds. + * + * @param timestamp - The timestamp to set, which can be a number or a Date object. + */ + public setTimestamp(timestamp: number | Date): void { + if (!this.#validateEmfTimestamp(timestamp)) { + this.#logger.warn( + "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + + 'Ensure the timestamp is within 14 days in the past or up to 2 hours in the future and is also a valid number or Date object.' + ); + } + this.#timestamp = this.#convertTimestampToEmfFormat(timestamp); + } + /** * Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema. * @@ -627,7 +649,7 @@ class Metrics extends Utility implements MetricsInterface { return { _aws: { - Timestamp: new Date().getTime(), + Timestamp: this.#timestamp ?? new Date().getTime(), CloudWatchMetrics: [ { Namespace: this.namespace || DEFAULT_NAMESPACE, @@ -940,6 +962,54 @@ class Metrics extends Utility implements MetricsInterface { } } } + + /** + * Validates a given timestamp based on CloudWatch Timestamp guidelines. + * + * Timestamp must meet CloudWatch requirements, otherwise an InvalidTimestampError will be raised. + * The time stamp can be up to two weeks in the past and up to two hours into the future. + * See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp) + * for valid values. + * + * @param timestamp - Date object or epoch time in milliseconds representing the timestamp to validate. + */ + #validateEmfTimestamp(timestamp: number | Date): boolean { + const EMF_MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds + const EMF_MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000; // 2 hours in milliseconds + + if (!(timestamp instanceof Date) && !isIntegerNumber(timestamp)) { + return false; + } + + const timestampMs = + timestamp instanceof Date ? timestamp.getTime() : timestamp; + + const currentTime = new Date().getTime(); + + const minValidTimestamp = currentTime - EMF_MAX_TIMESTAMP_PAST_AGE; + const maxValidTimestamp = currentTime + EMF_MAX_TIMESTAMP_FUTURE_AGE; + + return timestampMs >= minValidTimestamp && timestampMs <= maxValidTimestamp; + } + + /** + * Converts a given timestamp to EMF (Embedded Metric Format) compatible format. + * + * @param timestamp - The timestamp to convert, which can be either a number (in milliseconds) or a Date object. + * @returns The timestamp in milliseconds. If the input is invalid, returns 0. + */ + #convertTimestampToEmfFormat(timestamp: number | Date): number { + if (isIntegerNumber(timestamp)) { + return timestamp; + } + if (timestamp instanceof Date) { + return timestamp.getTime(); + } + // If this point is reached, input was neither a valid number nor Date + // Return 0 which represents the initial date of epoch time + // This will be skipped by Amazon CloudWatch + return 0; + } } export { Metrics }; From 960ad1b2019a9fd438e193112d1953b921beab7e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 11 Nov 2024 21:18:53 +0600 Subject: [PATCH 02/11] refactor: use `isDate` node util function --- packages/metrics/src/Metrics.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 8984bb0e62..6c496acd39 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -1,4 +1,5 @@ import { Console } from 'node:console'; +import { isDate } from 'node:util/types'; import { Utility, isIntegerNumber } from '@aws-lambda-powertools/commons'; import type { GenericLogger, @@ -977,12 +978,11 @@ class Metrics extends Utility implements MetricsInterface { const EMF_MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds const EMF_MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000; // 2 hours in milliseconds - if (!(timestamp instanceof Date) && !isIntegerNumber(timestamp)) { + if (!isDate(timestamp) && !isIntegerNumber(timestamp)) { return false; } - const timestampMs = - timestamp instanceof Date ? timestamp.getTime() : timestamp; + const timestampMs = isDate(timestamp) ? timestamp.getTime() : timestamp; const currentTime = new Date().getTime(); @@ -1002,7 +1002,7 @@ class Metrics extends Utility implements MetricsInterface { if (isIntegerNumber(timestamp)) { return timestamp; } - if (timestamp instanceof Date) { + if (isDate(timestamp)) { return timestamp.getTime(); } // If this point is reached, input was neither a valid number nor Date From 80353630c2e58a0b19170f18e8e8c5e4a0ee295d Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 11 Nov 2024 21:22:27 +0600 Subject: [PATCH 03/11] test: `setTimestamp` function --- packages/metrics/tests/unit/Metrics.test.ts | 258 +++++++++++++++++++- 1 file changed, 257 insertions(+), 1 deletion(-) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 8b75a21faf..8280df374d 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -32,8 +32,20 @@ jest.mock('node:console', () => ({ })), })); jest.spyOn(console, 'warn').mockImplementation(() => ({})); +const OriginalDate = Date; const mockDate = new Date(1466424490000); -const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); +/** + * If the constructor is called without arguments, it returns a predefined mock date. + * Otherwise, it delegates to the original Date constructor with the provided arguments. + */ +const dateSpy = jest + .spyOn(global, 'Date') + .mockImplementation((...args: ConstructorParameters) => { + if ((args as unknown[]).length === 0) { + return mockDate; + } + return new OriginalDate(...args); + }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); @@ -2242,4 +2254,248 @@ describe('Class: Metrics', () => { expect(metrics['console']).toEqual(console); }); }); + + describe('Method: setTimestamp', () => { + const testCases = [ + { + format: 'milliseconds', + getTimestamp: (timestampMs: number) => timestampMs, + }, + { + format: 'Date object', + getTimestamp: (timestamp: number) => new Date(timestamp), + }, + ]; + + for (const { format, getTimestamp } of testCases) { + describe(`when timestamp is provided as ${format}`, () => { + test('should set the timestamp if provided in the future', () => { + // Prepare + const testMetric = 'test-metric'; + const metrics: Metrics = new Metrics(); + //Add 10 minutes to mockDate object and still remain as date object + const timestampMs = mockDate.getTime() + 10 * 60 * 1000; // Add 10 minutes in milliseconds + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(getTimestamp(timestampMs)); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: timestampMs, + }), + }) + ); + }); + + test('should set the timestamp if provided in the past', () => { + // Prepare + const testMetric = 'test-metric'; + const metrics: Metrics = new Metrics(); + const timestampMs = mockDate.getTime() - 10 * 60 * 1000; // Subtract 10 minutes in milliseconds + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(getTimestamp(timestampMs)); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: timestampMs, + }), + }) + ); + }); + + test('should not log a warning if the timestamp exact two hours in the future and set the timestamp', () => { + // Prepare + const testMetric = 'test-metric'; + const timestampMs = mockDate.getTime() + 2 * 60 * 60 * 1000; // Add 2 hours and 1 ms + const customLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + const metrics: Metrics = new Metrics({ logger: customLogger }); + const consoleWarnSpy = jest.spyOn(customLogger, 'warn'); + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(getTimestamp(timestampMs)); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: timestampMs, + }), + }) + ); + }); + + test('should log a warning if the timestamp is more than two hours in the future but still set the timestamp', () => { + // Prepare + const testMetric = 'test-metric'; + const timestampMs = mockDate.getTime() + 2 * 60 * 60 * 1000 + 1; // Add 2 hours and 1 ms + const customLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + const metrics: Metrics = new Metrics({ logger: customLogger }); + const consoleWarnSpy = jest.spyOn(customLogger, 'warn'); + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(getTimestamp(timestampMs)); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(consoleWarnSpy).toHaveBeenCalledWith( + "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + + 'Ensure the timestamp is within 14 days in the past or up to 2 hours in the future and is also a valid number or Date object.' + ); + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: timestampMs, + }), + }) + ); + }); + + test('should not log a warning if the timestamp is exact 14 days in the past and set the timestamp', () => { + // Prepare + const testMetric = 'test-metric'; + const timestampMs = mockDate.getTime() - 14 * 24 * 60 * 60 * 1000; // Subtract 14 days + const customLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + const metrics: Metrics = new Metrics({ logger: customLogger }); + const consoleWarnSpy = jest.spyOn(customLogger, 'warn'); + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(getTimestamp(timestampMs)); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: timestampMs, + }), + }) + ); + }); + + test('should log a warning if the timestamp is more than 14 days in the past but still set the timestamp', () => { + // Prepare + const testMetric = 'test-metric'; + const timestampMs = mockDate.getTime() - 14 * 24 * 60 * 60 * 1000 - 1; // Subtract 14 days and 1 ms + const customLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + const metrics: Metrics = new Metrics({ logger: customLogger }); + const consoleWarnSpy = jest.spyOn(customLogger, 'warn'); + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(getTimestamp(timestampMs)); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(consoleWarnSpy).toHaveBeenCalledWith( + "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + + 'Ensure the timestamp is within 14 days in the past or up to 2 hours in the future and is also a valid number or Date object.' + ); + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: timestampMs, + }), + }) + ); + }); + }); + } + + test('should log warning and set timestamp to 0 if not a number provided', () => { + // Prepare + const testMetric = 'test-metric'; + const customLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + const metrics: Metrics = new Metrics({ logger: customLogger }); + const consoleWarnSpy = jest.spyOn(customLogger, 'warn'); + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(Number.NaN); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(consoleWarnSpy).toHaveBeenCalledWith( + "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + + 'Ensure the timestamp is within 14 days in the past or up to 2 hours in the future and is also a valid number or Date object.' + ); + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: 0, + }), + }) + ); + }); + + test('should log warning and set timestamp to 0 if not a integer number provided', () => { + // Prepare + const testMetric = 'test-metric'; + const customLogger = { + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + }; + const metrics: Metrics = new Metrics({ logger: customLogger }); + const consoleWarnSpy = jest.spyOn(customLogger, 'warn'); + + // Act + metrics.addMetric(testMetric, MetricUnit.Count, 10); + metrics.setTimestamp(1.1); + const loggedData = metrics.serializeMetrics(); + + // Assess + expect(consoleWarnSpy).toHaveBeenCalledWith( + "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + + 'Ensure the timestamp is within 14 days in the past or up to 2 hours in the future and is also a valid number or Date object.' + ); + expect(loggedData).toEqual( + expect.objectContaining({ + _aws: expect.objectContaining({ + Timestamp: 0, + }), + }) + ); + }); + }); }); From 287bc58c0c5dda506ca387b9218819d1732d5ccf Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 09:28:55 +0600 Subject: [PATCH 04/11] doc: `setTimestamp` function section in `metrics` --- docs/core/metrics.md | 12 ++++++++++++ examples/snippets/metrics/setTimestamp.ts | 15 +++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 examples/snippets/metrics/setTimestamp.ts diff --git a/docs/core/metrics.md b/docs/core/metrics.md index d47a6416a5..30480520ca 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -229,6 +229,18 @@ You can add default dimensions to your metrics by passing them as parameters in If you'd like to remove them at some point, you can use the `clearDefaultDimensions` method. +### Changing default timestamp + +When creating metrics, we use the current timestamp. If you want to change the timestamp of all the metrics you create, utilize the `setTimestamp` function. You can specify a datetime object or an integer representing an epoch timestamp in milliseconds. + +Note that when specifying the timestamp using an integer, it must adhere to the epoch timezone format in milliseconds. + +=== "setTimestamp method" + + ```typescript hl_lines="13" + --8<-- "examples/snippets/metrics/setTimestamp.ts" + ``` + ### Flushing metrics As you finish adding all your metrics, you need to serialize and "flush them" by calling `publishStoredMetrics()`. This will print the metrics to standard output. diff --git a/examples/snippets/metrics/setTimestamp.ts b/examples/snippets/metrics/setTimestamp.ts new file mode 100644 index 0000000000..5f6d6f69a4 --- /dev/null +++ b/examples/snippets/metrics/setTimestamp.ts @@ -0,0 +1,15 @@ +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + +const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', +}); + +export const handler = async ( + _event: unknown, + _context: unknown +): Promise => { + const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago + metrics.setTimestamp(metricTimestamp); + metrics.addMetric('successfulBooking', MetricUnit.Count, 1); +}; From 1f51bb4d516d79b49989e98f44db4bfc064bdcdb Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 09:36:32 +0600 Subject: [PATCH 05/11] refactor: variables for common value --- packages/metrics/tests/unit/Metrics.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index 8280df374d..cbd8dbe1e1 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -2269,11 +2269,13 @@ describe('Class: Metrics', () => { for (const { format, getTimestamp } of testCases) { describe(`when timestamp is provided as ${format}`, () => { + const twoHours = 2 * 60 * 60 * 1000; + const fourteenDays = 14 * 24 * 60 * 60 * 1000; + test('should set the timestamp if provided in the future', () => { // Prepare const testMetric = 'test-metric'; const metrics: Metrics = new Metrics(); - //Add 10 minutes to mockDate object and still remain as date object const timestampMs = mockDate.getTime() + 10 * 60 * 1000; // Add 10 minutes in milliseconds // Act @@ -2315,7 +2317,7 @@ describe('Class: Metrics', () => { test('should not log a warning if the timestamp exact two hours in the future and set the timestamp', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() + 2 * 60 * 60 * 1000; // Add 2 hours and 1 ms + const timestampMs = mockDate.getTime() + twoHours; const customLogger = { warn: jest.fn(), debug: jest.fn(), @@ -2344,7 +2346,7 @@ describe('Class: Metrics', () => { test('should log a warning if the timestamp is more than two hours in the future but still set the timestamp', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() + 2 * 60 * 60 * 1000 + 1; // Add 2 hours and 1 ms + const timestampMs = mockDate.getTime() + twoHours + 1; const customLogger = { warn: jest.fn(), debug: jest.fn(), @@ -2376,7 +2378,7 @@ describe('Class: Metrics', () => { test('should not log a warning if the timestamp is exact 14 days in the past and set the timestamp', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() - 14 * 24 * 60 * 60 * 1000; // Subtract 14 days + const timestampMs = mockDate.getTime() - fourteenDays; const customLogger = { warn: jest.fn(), debug: jest.fn(), @@ -2405,7 +2407,7 @@ describe('Class: Metrics', () => { test('should log a warning if the timestamp is more than 14 days in the past but still set the timestamp', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() - 14 * 24 * 60 * 60 * 1000 - 1; // Subtract 14 days and 1 ms + const timestampMs = mockDate.getTime() - fourteenDays - 1; const customLogger = { warn: jest.fn(), debug: jest.fn(), From e3936c49622bb34a4299bdf1e60e8fd198e33e50 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 09:50:22 +0600 Subject: [PATCH 06/11] refactor: constants for magic numbers --- packages/metrics/src/Metrics.ts | 5 ++--- packages/metrics/src/constants.ts | 12 +++++++++++ packages/metrics/tests/unit/Metrics.test.ts | 23 +++++++++++---------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 6c496acd39..9a43c5e114 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -10,6 +10,8 @@ import { EnvironmentVariablesService } from './config/EnvironmentVariablesServic import { COLD_START_METRIC, DEFAULT_NAMESPACE, + EMF_MAX_TIMESTAMP_FUTURE_AGE, + EMF_MAX_TIMESTAMP_PAST_AGE, MAX_DIMENSION_COUNT, MAX_METRICS_SIZE, MAX_METRIC_VALUES_SIZE, @@ -975,9 +977,6 @@ class Metrics extends Utility implements MetricsInterface { * @param timestamp - Date object or epoch time in milliseconds representing the timestamp to validate. */ #validateEmfTimestamp(timestamp: number | Date): boolean { - const EMF_MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds - const EMF_MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000; // 2 hours in milliseconds - if (!isDate(timestamp) && !isIntegerNumber(timestamp)) { return false; } diff --git a/packages/metrics/src/constants.ts b/packages/metrics/src/constants.ts index 18edeb0516..0228ce5215 100644 --- a/packages/metrics/src/constants.ts +++ b/packages/metrics/src/constants.ts @@ -18,6 +18,16 @@ const MAX_METRIC_VALUES_SIZE = 100; * The maximum number of dimensions that can be added to a metric (0-indexed). */ const MAX_DIMENSION_COUNT = 29; +/** + * The maximum age of a timestamp in milliseconds that can be emitted in a metric. + * This is set to 14 days. + */ +const EMF_MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000; +/** + * The maximum age of a timestamp in milliseconds that can be emitted in a metric. + * This is set to 2 hours. + */ +const EMF_MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000; /** * The unit of the metric. @@ -73,4 +83,6 @@ export { MAX_DIMENSION_COUNT, MetricUnit, MetricResolution, + EMF_MAX_TIMESTAMP_PAST_AGE, + EMF_MAX_TIMESTAMP_FUTURE_AGE, }; diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index cbd8dbe1e1..adf8c00228 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -10,6 +10,8 @@ import { EnvironmentVariablesService } from '../../src/config/EnvironmentVariabl import { COLD_START_METRIC, DEFAULT_NAMESPACE, + EMF_MAX_TIMESTAMP_FUTURE_AGE, + EMF_MAX_TIMESTAMP_PAST_AGE, MAX_DIMENSION_COUNT, MAX_METRICS_SIZE, MAX_METRIC_VALUES_SIZE, @@ -2269,9 +2271,6 @@ describe('Class: Metrics', () => { for (const { format, getTimestamp } of testCases) { describe(`when timestamp is provided as ${format}`, () => { - const twoHours = 2 * 60 * 60 * 1000; - const fourteenDays = 14 * 24 * 60 * 60 * 1000; - test('should set the timestamp if provided in the future', () => { // Prepare const testMetric = 'test-metric'; @@ -2314,10 +2313,10 @@ describe('Class: Metrics', () => { ); }); - test('should not log a warning if the timestamp exact two hours in the future and set the timestamp', () => { + test('should not log a warning if the timestamp is withing valid future range', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() + twoHours; + const timestampMs = mockDate.getTime() + EMF_MAX_TIMESTAMP_FUTURE_AGE; const customLogger = { warn: jest.fn(), debug: jest.fn(), @@ -2343,10 +2342,11 @@ describe('Class: Metrics', () => { ); }); - test('should log a warning if the timestamp is more than two hours in the future but still set the timestamp', () => { + test('should log a warning if the timestamp is more than the future range but still set the timestamp', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() + twoHours + 1; + const timestampMs = + mockDate.getTime() + EMF_MAX_TIMESTAMP_FUTURE_AGE + 1; const customLogger = { warn: jest.fn(), debug: jest.fn(), @@ -2375,10 +2375,10 @@ describe('Class: Metrics', () => { ); }); - test('should not log a warning if the timestamp is exact 14 days in the past and set the timestamp', () => { + test('should not log a warning if the timestamp is within past range and set the timestamp', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() - fourteenDays; + const timestampMs = mockDate.getTime() - EMF_MAX_TIMESTAMP_PAST_AGE; const customLogger = { warn: jest.fn(), debug: jest.fn(), @@ -2404,10 +2404,11 @@ describe('Class: Metrics', () => { ); }); - test('should log a warning if the timestamp is more than 14 days in the past but still set the timestamp', () => { + test('should log a warning if the timestamp is more than past range but still set the timestamp', () => { // Prepare const testMetric = 'test-metric'; - const timestampMs = mockDate.getTime() - fourteenDays - 1; + const timestampMs = + mockDate.getTime() - EMF_MAX_TIMESTAMP_PAST_AGE - 1; const customLogger = { warn: jest.fn(), debug: jest.fn(), From 16f893b5188990bc5f8c04a9ad3c263247807c3f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 10:01:56 +0600 Subject: [PATCH 07/11] style: doc comments updated --- packages/metrics/src/Metrics.ts | 11 ++++++----- packages/metrics/tests/unit/Metrics.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 9a43c5e114..17932d59fa 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -982,7 +982,6 @@ class Metrics extends Utility implements MetricsInterface { } const timestampMs = isDate(timestamp) ? timestamp.getTime() : timestamp; - const currentTime = new Date().getTime(); const minValidTimestamp = currentTime - EMF_MAX_TIMESTAMP_PAST_AGE; @@ -992,7 +991,7 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Converts a given timestamp to EMF (Embedded Metric Format) compatible format. + * Converts a given timestamp to EMF compatible format. * * @param timestamp - The timestamp to convert, which can be either a number (in milliseconds) or a Date object. * @returns The timestamp in milliseconds. If the input is invalid, returns 0. @@ -1004,9 +1003,11 @@ class Metrics extends Utility implements MetricsInterface { if (isDate(timestamp)) { return timestamp.getTime(); } - // If this point is reached, input was neither a valid number nor Date - // Return 0 which represents the initial date of epoch time - // This will be skipped by Amazon CloudWatch + /** + * If this point is reached, it indicates timestamp was neither a valid number nor Date + * Returning zero represents the initial date of epoch time, + * which will be skipped by Amazon CloudWatch. + **/ return 0; } } diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index adf8c00228..b65ebab212 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -2265,7 +2265,7 @@ describe('Class: Metrics', () => { }, { format: 'Date object', - getTimestamp: (timestamp: number) => new Date(timestamp), + getTimestamp: (timestampMs: number) => new Date(timestampMs), }, ]; @@ -2313,7 +2313,7 @@ describe('Class: Metrics', () => { ); }); - test('should not log a warning if the timestamp is withing valid future range', () => { + test('should not log a warning if the timestamp is within valid future range', () => { // Prepare const testMetric = 'test-metric'; const timestampMs = mockDate.getTime() + EMF_MAX_TIMESTAMP_FUTURE_AGE; From 3c5f412845515c0ac9f4dc1e2b468b17a3f322c2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 10:12:35 +0600 Subject: [PATCH 08/11] doc: add comments for `#validateEmfTimestamp` inside `setTimestamp` function --- packages/metrics/src/Metrics.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 17932d59fa..247b5fd16f 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -587,6 +587,13 @@ class Metrics extends Utility implements MetricsInterface { * @param timestamp - The timestamp to set, which can be a number or a Date object. */ public setTimestamp(timestamp: number | Date): void { + /** + * The timestamp must be a Date object or an integer representing an epoch time. + * This should not exceed 14 days in the past or be more than 2 hours in the future. + * Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch. + * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html + * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html + **/ if (!this.#validateEmfTimestamp(timestamp)) { this.#logger.warn( "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + From 973e147a5244eeaa23cba2a3bcfccc02d0242b08 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 22:33:50 +0600 Subject: [PATCH 09/11] feat: add `setTimestamp` to `MetricsInterface` --- packages/metrics/src/types/Metrics.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index f02f9b183a..27b49f5f13 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -475,6 +475,31 @@ interface MetricsInterface { * @param enabled - Whether to throw an error if no metrics are emitted */ setThrowOnEmptyMetrics(enabled: boolean): void; + /** + * Sets the timestamp for the metric. + * + * If an integer is provided, it is assumed to be the epoch time in milliseconds. + * If a Date object is provided, it will be converted to epoch time in milliseconds. + * + * @example + * ```typescript + * import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * }); + * + * export const handler = async () => { + * const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago + * metrics.setTimestamp(metricTimestamp); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * }; + * ``` + * + * @param timestamp - The timestamp to set, which can be a number or a Date object. + */ + setTimestamp(timestamp: number | Date): void; /** * Create a new Metrics instance configured to immediately flush a single metric. * From 300eaf59b0f1084e2bf25f68433d6f567695f42a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 22:48:04 +0600 Subject: [PATCH 10/11] refactor: sync the docstring of `setTimestamp` function --- packages/metrics/src/Metrics.ts | 30 ++++++++++++++++++++------- packages/metrics/src/types/Metrics.ts | 8 ++++++- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 247b5fd16f..2809b5bf13 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -581,19 +581,35 @@ class Metrics extends Utility implements MetricsInterface { /** * Sets the timestamp for the metric. + * * If an integer is provided, it is assumed to be the epoch time in milliseconds. * If a Date object is provided, it will be converted to epoch time in milliseconds. * + * The timestamp must be a Date object or an integer representing an epoch time. + * This should not exceed 14 days in the past or be more than 2 hours in the future. + * Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch. + * + * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html + * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html + * + * @example + * ```typescript + * import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * }); + * + * export const handler = async () => { + * const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago + * metrics.setTimestamp(metricTimestamp); + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * }; + * ``` * @param timestamp - The timestamp to set, which can be a number or a Date object. */ public setTimestamp(timestamp: number | Date): void { - /** - * The timestamp must be a Date object or an integer representing an epoch time. - * This should not exceed 14 days in the past or be more than 2 hours in the future. - * Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch. - * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html - * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html - **/ if (!this.#validateEmfTimestamp(timestamp)) { this.#logger.warn( "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " + diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index 27b49f5f13..da71711501 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -481,6 +481,13 @@ interface MetricsInterface { * If an integer is provided, it is assumed to be the epoch time in milliseconds. * If a Date object is provided, it will be converted to epoch time in milliseconds. * + * The timestamp must be a Date object or an integer representing an epoch time. + * This should not exceed 14 days in the past or be more than 2 hours in the future. + * Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch. + * + * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html + * See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html + * * @example * ```typescript * import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; @@ -496,7 +503,6 @@ interface MetricsInterface { * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); * }; * ``` - * * @param timestamp - The timestamp to set, which can be a number or a Date object. */ setTimestamp(timestamp: number | Date): void; From 7825dc2d71baaf118fb02feb0554ffa6a34aed34 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 12 Nov 2024 22:48:44 +0600 Subject: [PATCH 11/11] fix: remove redundant comment from `validateEmfTimestamp` --- packages/metrics/src/Metrics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 2809b5bf13..94eef05c8b 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -992,7 +992,7 @@ class Metrics extends Utility implements MetricsInterface { /** * Validates a given timestamp based on CloudWatch Timestamp guidelines. * - * Timestamp must meet CloudWatch requirements, otherwise an InvalidTimestampError will be raised. + * Timestamp must meet CloudWatch requirements. * The time stamp can be up to two weeks in the past and up to two hours into the future. * See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp) * for valid values.