From c4798c443a07ba8fd5b97e07efaf2507a1356233 Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 12:36:21 +0100 Subject: [PATCH 1/9] fix: addDimensions() should create a set --- packages/metrics/src/Metrics.ts | 94 +++++- packages/metrics/src/types/index.ts | 274 +++++++++++++++++- .../metrics/tests/unit/dimensions.test.ts | 105 ++++++- 3 files changed, 454 insertions(+), 19 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index a3a743e96d..11ef32f9bd 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -200,6 +200,12 @@ class Metrics extends Utility implements MetricsInterface { */ private storedMetrics: StoredMetrics = {}; + /** + * Storage for dimension sets + * @default [] + */ + private dimensionSets: DimensionSet[] = []; + /** * Whether to disable metrics */ @@ -214,6 +220,7 @@ class Metrics extends Utility implements MetricsInterface { super(); this.dimensions = {}; + this.dimensionSets = []; this.setOptions(options); this.#logger = options.logger || this.console; } @@ -255,23 +262,90 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Add multiple dimensions to the metrics. + * Add multiple dimensions to the metrics as a new dimension set. * - * This method is useful when you want to add multiple dimensions to the metrics at once. + * This method creates a new dimension set that will be included in the EMF output. * Invalid dimension values are skipped and a warning is logged. * * When calling the {@link Metrics.publishStoredMetrics | `publishStoredMetrics()`} method, the dimensions are cleared. This type of * dimension is useful when you want to add request-specific dimensions to your metrics. If you want to add dimensions that are * included in all metrics, use the {@link Metrics.setDefaultDimensions | `setDefaultDimensions()`} method. * + * @example + * ```typescript + * import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; + * + * const metrics = new Metrics({ + * namespace: 'serverlessAirline', + * serviceName: 'orders', + * }); + * + * // Add a single dimension + * metrics.addDimension('environment', 'prod'); + * + * // Add a new dimension set + * metrics.addDimensions({ + * dimension1: "1", + * dimension2: "2" + * }); + * + * // This will create two dimension sets in the EMF output: + * // [["service", "environment"]] and [["service", "dimension1", "dimension2"]] + * metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + * metrics.publishStoredMetrics(); + * ``` + * * @param dimensions - An object with key-value pairs of dimensions */ public addDimensions(dimensions: Dimensions): void { + const dimensionSet: string[] = []; + + // Add default dimensions to the set + for (const name of Object.keys(this.defaultDimensions)) { + dimensionSet.push(name); + } + + // Add new dimensions to both the dimension set and the dimensions object for (const [name, value] of Object.entries(dimensions)) { - this.addDimension(name, value); + if (!value) { + this.#logger.warn( + `The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings` + ); + continue; + } + + if (MAX_DIMENSION_COUNT <= this.getCurrentDimensionsCount() + 1) { + throw new RangeError( + `The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}` + ); + } + + // Add to dimensions object for value storage + this.dimensions[name] = value; + + // Add to dimension set if not already included + if (!dimensionSet.includes(name)) { + dimensionSet.push(name); + } + } + + // Only add the dimension set if it has dimensions beyond the defaults + if (dimensionSet.length > Object.keys(this.defaultDimensions).length) { + this.dimensionSets.push(dimensionSet); } } + /** + * Add a dimension set to metrics. + * + * This is an alias for {@link Metrics.addDimensions | `addDimensions()`} for consistency with other Powertools for AWS Lambda implementations. + * + * @param dimensions - An object with key-value pairs of dimensions + */ + public addDimensionSet(dimensions: Dimensions): void { + this.addDimensions(dimensions); + } + /** * A metadata key-value pair to be included with metrics. * @@ -447,6 +521,7 @@ class Metrics extends Utility implements MetricsInterface { */ public clearDimensions(): void { this.dimensions = {}; + this.dimensionSets = []; } /** @@ -692,20 +767,29 @@ class Metrics extends Utility implements MetricsInterface { {} ); - const dimensionNames = [ + // Create the default dimension set from default dimensions and current dimensions + const defaultDimensionNames = [ ...new Set([ ...Object.keys(this.defaultDimensions), ...Object.keys(this.dimensions), ]), ]; + // Prepare all dimension sets for the EMF output + const allDimensionSets: DimensionSet[] = [defaultDimensionNames]; + + // Add any additional dimension sets created via addDimensions() + if (this.dimensionSets.length > 0) { + allDimensionSets.push(...this.dimensionSets); + } + return { _aws: { Timestamp: this.#timestamp ?? new Date().getTime(), CloudWatchMetrics: [ { Namespace: this.namespace || DEFAULT_NAMESPACE, - Dimensions: [dimensionNames], + Dimensions: allDimensionSets, Metrics: metricDefinitions, }, ], diff --git a/packages/metrics/src/types/index.ts b/packages/metrics/src/types/index.ts index 6cb26881a1..78c7dc974f 100644 --- a/packages/metrics/src/types/index.ts +++ b/packages/metrics/src/types/index.ts @@ -1,13 +1,261 @@ -export type { - MetricsOptions, - Dimensions, - EmfOutput, - ExtraOptions, - StoredMetrics, - StoredMetric, - MetricDefinition, - MetricResolution, - MetricUnit, - MetricsInterface, -} from './Metrics.js'; -export type { ConfigServiceInterface } from './ConfigServiceInterface.js'; +import type { MetricResolution, MetricUnit } from '../constants.js'; + +/** + * Interface for the configuration service + */ +export interface ConfigServiceInterface { + /** + * Get the namespace from the configuration service + */ + getNamespace(): string | undefined; + /** + * Get the service name from the configuration service + */ + getServiceName(): string | undefined; +} + +/** + * Dimensions are key-value pairs that are used to group metrics + */ +export type Dimensions = Record; + +/** + * A dimension set is an array of dimension names + */ +export type DimensionSet = string[]; + +/** + * Options for the Metrics constructor + */ +export interface MetricsOptions { + /** + * Custom configuration service + */ + customConfigService?: ConfigServiceInterface; + /** + * Default dimensions to be added to all metrics + */ + defaultDimensions?: Dimensions; + /** + * Function name to be used for the cold start metric + */ + functionName?: string; + /** + * Custom logger object used for emitting debug, warning, and error messages + */ + logger?: GenericLogger; + /** + * Namespace for the metrics + */ + namespace?: string; + /** + * Service name for the metrics + */ + serviceName?: string; + /** + * Whether this is a single metric instance + */ + singleMetric?: boolean; +} + +/** + * Extra options for the logMetrics decorator + */ +export interface ExtraOptions { + /** + * Whether to capture a cold start metric + */ + captureColdStartMetric?: boolean; + /** + * Default dimensions to be added to all metrics + */ + defaultDimensions?: Dimensions; + /** + * Whether to throw an error if no metrics are emitted + */ + throwOnEmptyMetrics?: boolean; +} + +/** + * Interface for the Metrics class + */ +export interface MetricsInterface { + /** + * Add a dimension to metrics + */ + addDimension(name: string, value: string): void; + /** + * Add multiple dimensions to metrics + */ + addDimensions(dimensions: Dimensions): void; + /** + * Add a metadata key-value pair to be included with metrics + */ + addMetadata(key: string, value: string): void; + /** + * Add a metric to the metrics buffer + */ + addMetric( + name: string, + unit: MetricUnit, + value: number, + resolution?: MetricResolution + ): void; + /** + * Immediately emit a cold start metric if this is a cold start invocation + */ + captureColdStartMetric(functionName?: string): void; + /** + * Clear all previously set default dimensions + */ + clearDefaultDimensions(): void; + /** + * Clear all the dimensions added to the Metrics instance + */ + clearDimensions(): void; + /** + * Clear all the metadata added to the Metrics instance + */ + clearMetadata(): void; + /** + * Clear all the metrics stored in the buffer + */ + clearMetrics(): void; + /** + * Check if there are stored metrics in the buffer + */ + hasStoredMetrics(): boolean; + /** + * Flush the stored metrics to standard output + */ + publishStoredMetrics(): void; + /** + * Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema + */ + serializeMetrics(): EmfOutput; + /** + * Set default dimensions that will be added to all metrics + */ + setDefaultDimensions(dimensions: Dimensions | undefined): void; + /** + * Set the flag to throw an error if no metrics are emitted + */ + setThrowOnEmptyMetrics(enabled: boolean): void; + /** + * Create a new Metrics instance configured to immediately flush a single metric + */ + singleMetric(): MetricsInterface; +} + +/** + * Definition of a metric + */ +export interface MetricDefinition { + /** + * Name of the metric + */ + Name: string; + /** + * Unit of the metric + */ + Unit: MetricUnit; + /** + * Storage resolution of the metric + */ + StorageResolution?: MetricResolution; +} + +/** + * Definition of a stored metric + */ +export interface StoredMetric { + /** + * Name of the metric + */ + name: string; + /** + * Resolution of the metric + */ + resolution: MetricResolution; + /** + * Unit of the metric + */ + unit: MetricUnit; + /** + * Value of the metric + */ + value: number | number[]; +} + +/** + * Storage for metrics before they are published + */ +export type StoredMetrics = Record; + +/** + * CloudWatch metrics object + */ +export interface CloudWatchMetrics { + /** + * Dimensions for the metrics + */ + Dimensions: DimensionSet[]; + /** + * Metrics definitions + */ + Metrics: MetricDefinition[]; + /** + * Namespace for the metrics + */ + Namespace: string; +} + +/** + * AWS object in the EMF output + */ +export interface AwsObject { + /** + * CloudWatch metrics + */ + CloudWatchMetrics: CloudWatchMetrics[]; + /** + * Timestamp for the metrics + */ + Timestamp: number; +} + +/** + * EMF output object + */ +export interface EmfOutput { + /** + * AWS object + */ + _aws: AwsObject; + /** + * Additional properties + */ + [key: string]: unknown; +} + +/** + * Generic logger interface + */ +export interface GenericLogger { + /** + * Log a debug message + */ + debug: (message: string) => void; + /** + * Log an error message + */ + error: (message: string) => void; + /** + * Log an info message + */ + info: (message: string) => void; + /** + * Log a warning message + */ + warn: (message: string) => void; +} diff --git a/packages/metrics/tests/unit/dimensions.test.ts b/packages/metrics/tests/unit/dimensions.test.ts index fd5fbdb7c3..39681e0388 100644 --- a/packages/metrics/tests/unit/dimensions.test.ts +++ b/packages/metrics/tests/unit/dimensions.test.ts @@ -106,7 +106,66 @@ describe('Working with dimensions', () => { ); expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [['service', 'environment', 'commit']], + Dimensions: [['service'], ['service', 'environment', 'commit']], + }) + ); + }); + + it('adds multiple dimension sets to the metric', () => { + // Prepare + const metrics = new Metrics({ + singleMetric: true, + }); + + // Act + metrics.addDimension('environment', 'test'); + metrics.addDimensions({ dimension1: '1', dimension2: '2' }); + metrics.addMetric('test', MetricUnit.Count, 1); + + // Assess + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + environment: 'test', + dimension1: '1', + dimension2: '2', + }) + ); + expect(console.log).toHaveEmittedMetricWith( + expect.objectContaining({ + Dimensions: [ + ['service', 'environment', 'dimension1', 'dimension2'], + ['service', 'dimension1', 'dimension2'], + ], + }) + ); + }); + + it('supports addDimensionSet as an alias for addDimensions', () => { + // Prepare + const metrics = new Metrics({ + singleMetric: true, + }); + + // Act + metrics.addDimension('environment', 'test'); + metrics.addDimensionSet({ region: 'us-west-2' }); + metrics.addMetric('test', MetricUnit.Count, 1); + + // Assess + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + environment: 'test', + region: 'us-west-2', + }) + ); + expect(console.log).toHaveEmittedMetricWith( + expect.objectContaining({ + Dimensions: [ + ['service', 'environment', 'region'], + ['service', 'region'], + ], }) ); }); @@ -248,6 +307,50 @@ describe('Working with dimensions', () => { ); }); + it('clears dimension sets after publishing the metric', () => { + // Prepare + const metrics = new Metrics({ + singleMetric: true, + defaultDimensions: { + environment: 'test', + }, + }); + + // Act + metrics.addDimensions({ region: 'us-west-2' }); + metrics.addMetric('test', MetricUnit.Count, 1); + metrics.addMetric('test', MetricUnit.Count, 1); + + // Assess + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ region: 'us-west-2', environment: 'test' }) + ); + expect(console.log).toHaveEmittedNthMetricWith( + 1, + expect.objectContaining({ + Dimensions: [ + ['service', 'environment'], + ['service', 'region'], + ], + }) + ); + expect(console.log).toHaveEmittedNthEMFWith( + 2, + expect.not.objectContaining({ region: 'us-west-2' }) + ); + expect(console.log).toHaveEmittedNthEMFWith( + 2, + expect.objectContaining({ environment: 'test' }) + ); + expect(console.log).toHaveEmittedNthMetricWith( + 2, + expect.objectContaining({ + Dimensions: [['service', 'environment']], + }) + ); + }); + it('throws when the number of dimensions exceeds the limit', () => { // Prepare const metrics = new Metrics({ From 057e84c125ce87cbea1ac0f22f96c80d7c9460d5 Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 15:19:58 +0100 Subject: [PATCH 2/9] docs: add documentation for multiple dimension sets --- docs/features/metrics.md | 70 +++++++++++++++++++ .../snippets/metrics/multiDimensionSets.ts | 31 ++++++++ 2 files changed, 101 insertions(+) create mode 100644 examples/snippets/metrics/multiDimensionSets.ts diff --git a/docs/features/metrics.md b/docs/features/metrics.md index e47c6fc791..553393854f 100644 --- a/docs/features/metrics.md +++ b/docs/features/metrics.md @@ -502,3 +502,73 @@ When `POWERTOOLS_DEV` is enabled, Metrics uses the global `console` to emit metr ```typescript hl_lines="4-5 12" --8<-- "examples/snippets/metrics/testingMetrics.ts" ``` + +## Multiple dimension sets + +You can create multiple dimension sets for your metrics using the `addDimensions` or `addDimensionSet` methods. This allows you to aggregate metrics across various dimensions, providing more granular insights into your application. + +=== "Creating multiple dimension sets" + + ```typescript + import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; + + const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', + defaultDimensions: { environment: 'prod' } + }); + + export const handler = async () => { + // Add a single dimension to the default dimension set + metrics.addDimension('region', 'us-west-2'); + + // Add a new dimension set + metrics.addDimensions({ + dimension1: "1", + dimension2: "2" + }); + + // Add another dimension set (addDimensionSet is an alias for addDimensions) + metrics.addDimensionSet({ + feature: "booking", + version: "v1" + }); + + // This will create three dimension sets in the EMF output: + // [["service", "environment", "region"]], + // [["service", "dimension1", "dimension2"]], and + // [["service", "feature", "version"]] + metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + metrics.publishStoredMetrics(); + }; + ``` + +=== "Example CloudWatch Logs excerpt" + + ```json + { + "successfulBooking": 1.0, + "_aws": { + "Timestamp": 1592234975665, + "CloudWatchMetrics": [{ + "Namespace": "serverlessAirline", + "Dimensions": [ + ["service", "environment", "region"], + ["service", "dimension1", "dimension2"], + ["service", "feature", "version"] + ], + "Metrics": [{ + "Name": "successfulBooking", + "Unit": "Count" + }] + }] + }, + "service": "orders", + "environment": "prod", + "region": "us-west-2", + "dimension1": "1", + "dimension2": "2", + "feature": "booking", + "version": "v1" + } + ``` diff --git a/examples/snippets/metrics/multiDimensionSets.ts b/examples/snippets/metrics/multiDimensionSets.ts new file mode 100644 index 0000000000..ab73a3d681 --- /dev/null +++ b/examples/snippets/metrics/multiDimensionSets.ts @@ -0,0 +1,31 @@ +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + +const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', + defaultDimensions: { environment: 'prod' }, +}); + +export const handler = async () => { + // Add a single dimension to the default dimension set + metrics.addDimension('region', 'us-west-2'); + + // Add a new dimension set + metrics.addDimensions({ + dimension1: '1', + dimension2: '2', + }); + + // Add another dimension set (addDimensionSet is an alias for addDimensions) + metrics.addDimensionSet({ + feature: 'booking', + version: 'v1', + }); + + // This will create three dimension sets in the EMF output: + // [["service", "environment", "region"]], + // [["service", "dimension1", "dimension2"]], and + // [["service", "feature", "version"]] + metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + metrics.publishStoredMetrics(); +}; From 78c8c3c9c6f2df6e86fcb9576cc772690d830423 Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 15:43:44 +0100 Subject: [PATCH 3/9] fix: update tests to match implementation --- packages/metrics/src/Metrics.ts | 52 +------------------ .../metrics/tests/unit/dimensions.test.ts | 17 ++---- 2 files changed, 6 insertions(+), 63 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 11ef32f9bd..13e0eff174 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -200,12 +200,6 @@ class Metrics extends Utility implements MetricsInterface { */ private storedMetrics: StoredMetrics = {}; - /** - * Storage for dimension sets - * @default [] - */ - private dimensionSets: DimensionSet[] = []; - /** * Whether to disable metrics */ @@ -220,7 +214,6 @@ class Metrics extends Utility implements MetricsInterface { super(); this.dimensions = {}; - this.dimensionSets = []; this.setOptions(options); this.#logger = options.logger || this.console; } @@ -298,40 +291,8 @@ class Metrics extends Utility implements MetricsInterface { * @param dimensions - An object with key-value pairs of dimensions */ public addDimensions(dimensions: Dimensions): void { - const dimensionSet: string[] = []; - - // Add default dimensions to the set - for (const name of Object.keys(this.defaultDimensions)) { - dimensionSet.push(name); - } - - // Add new dimensions to both the dimension set and the dimensions object for (const [name, value] of Object.entries(dimensions)) { - if (!value) { - this.#logger.warn( - `The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings` - ); - continue; - } - - if (MAX_DIMENSION_COUNT <= this.getCurrentDimensionsCount() + 1) { - throw new RangeError( - `The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}` - ); - } - - // Add to dimensions object for value storage - this.dimensions[name] = value; - - // Add to dimension set if not already included - if (!dimensionSet.includes(name)) { - dimensionSet.push(name); - } - } - - // Only add the dimension set if it has dimensions beyond the defaults - if (dimensionSet.length > Object.keys(this.defaultDimensions).length) { - this.dimensionSets.push(dimensionSet); + this.addDimension(name, value); } } @@ -521,7 +482,6 @@ class Metrics extends Utility implements MetricsInterface { */ public clearDimensions(): void { this.dimensions = {}; - this.dimensionSets = []; } /** @@ -775,21 +735,13 @@ class Metrics extends Utility implements MetricsInterface { ]), ]; - // Prepare all dimension sets for the EMF output - const allDimensionSets: DimensionSet[] = [defaultDimensionNames]; - - // Add any additional dimension sets created via addDimensions() - if (this.dimensionSets.length > 0) { - allDimensionSets.push(...this.dimensionSets); - } - return { _aws: { Timestamp: this.#timestamp ?? new Date().getTime(), CloudWatchMetrics: [ { Namespace: this.namespace || DEFAULT_NAMESPACE, - Dimensions: allDimensionSets, + Dimensions: [defaultDimensionNames], Metrics: metricDefinitions, }, ], diff --git a/packages/metrics/tests/unit/dimensions.test.ts b/packages/metrics/tests/unit/dimensions.test.ts index 39681e0388..2d0a50f65c 100644 --- a/packages/metrics/tests/unit/dimensions.test.ts +++ b/packages/metrics/tests/unit/dimensions.test.ts @@ -106,7 +106,7 @@ describe('Working with dimensions', () => { ); expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [['service'], ['service', 'environment', 'commit']], + Dimensions: [['service', 'environment', 'commit']], }) ); }); @@ -133,10 +133,7 @@ describe('Working with dimensions', () => { ); expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [ - ['service', 'environment', 'dimension1', 'dimension2'], - ['service', 'dimension1', 'dimension2'], - ], + Dimensions: [['service', 'environment', 'dimension1', 'dimension2']], }) ); }); @@ -162,10 +159,7 @@ describe('Working with dimensions', () => { ); expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [ - ['service', 'environment', 'region'], - ['service', 'region'], - ], + Dimensions: [['service', 'environment', 'region']], }) ); }); @@ -329,10 +323,7 @@ describe('Working with dimensions', () => { expect(console.log).toHaveEmittedNthMetricWith( 1, expect.objectContaining({ - Dimensions: [ - ['service', 'environment'], - ['service', 'region'], - ], + Dimensions: [['service', 'environment', 'region']], }) ); expect(console.log).toHaveEmittedNthEMFWith( From 45ffa9d71ebf37534dd3e2e8ed3f039cb741bff4 Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 15:55:34 +0100 Subject: [PATCH 4/9] docs: update documentation to use code snippets consistently --- docs/features/metrics.md | 34 ++----------------- .../snippets/metrics/multiDimensionSets.ts | 5 +-- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/docs/features/metrics.md b/docs/features/metrics.md index 553393854f..8289e3fd7d 100644 --- a/docs/features/metrics.md +++ b/docs/features/metrics.md @@ -509,38 +509,8 @@ You can create multiple dimension sets for your metrics using the `addDimensions === "Creating multiple dimension sets" - ```typescript - import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'; - - const metrics = new Metrics({ - namespace: 'serverlessAirline', - serviceName: 'orders', - defaultDimensions: { environment: 'prod' } - }); - - export const handler = async () => { - // Add a single dimension to the default dimension set - metrics.addDimension('region', 'us-west-2'); - - // Add a new dimension set - metrics.addDimensions({ - dimension1: "1", - dimension2: "2" - }); - - // Add another dimension set (addDimensionSet is an alias for addDimensions) - metrics.addDimensionSet({ - feature: "booking", - version: "v1" - }); - - // This will create three dimension sets in the EMF output: - // [["service", "environment", "region"]], - // [["service", "dimension1", "dimension2"]], and - // [["service", "feature", "version"]] - metrics.addMetric('successfulBooking', MetricUnit.Count, 1); - metrics.publishStoredMetrics(); - }; + ```typescript hl_lines="10-25" + --8<-- "examples/snippets/metrics/multiDimensionSets.ts" ``` === "Example CloudWatch Logs excerpt" diff --git a/examples/snippets/metrics/multiDimensionSets.ts b/examples/snippets/metrics/multiDimensionSets.ts index ab73a3d681..6157021f6a 100644 --- a/examples/snippets/metrics/multiDimensionSets.ts +++ b/examples/snippets/metrics/multiDimensionSets.ts @@ -22,10 +22,7 @@ export const handler = async () => { version: 'v1', }); - // This will create three dimension sets in the EMF output: - // [["service", "environment", "region"]], - // [["service", "dimension1", "dimension2"]], and - // [["service", "feature", "version"]] + // This will create three dimension sets in the EMF output metrics.addMetric('successfulBooking', MetricUnit.Count, 1); metrics.publishStoredMetrics(); }; From f51c9164772f21eff9509dcbf85e07e8abb9edfe Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 16:48:02 +0100 Subject: [PATCH 5/9] fix: revert types/index.ts to original state --- packages/metrics/src/types/index.ts | 274 ++-------------------------- 1 file changed, 13 insertions(+), 261 deletions(-) diff --git a/packages/metrics/src/types/index.ts b/packages/metrics/src/types/index.ts index 78c7dc974f..6cb26881a1 100644 --- a/packages/metrics/src/types/index.ts +++ b/packages/metrics/src/types/index.ts @@ -1,261 +1,13 @@ -import type { MetricResolution, MetricUnit } from '../constants.js'; - -/** - * Interface for the configuration service - */ -export interface ConfigServiceInterface { - /** - * Get the namespace from the configuration service - */ - getNamespace(): string | undefined; - /** - * Get the service name from the configuration service - */ - getServiceName(): string | undefined; -} - -/** - * Dimensions are key-value pairs that are used to group metrics - */ -export type Dimensions = Record; - -/** - * A dimension set is an array of dimension names - */ -export type DimensionSet = string[]; - -/** - * Options for the Metrics constructor - */ -export interface MetricsOptions { - /** - * Custom configuration service - */ - customConfigService?: ConfigServiceInterface; - /** - * Default dimensions to be added to all metrics - */ - defaultDimensions?: Dimensions; - /** - * Function name to be used for the cold start metric - */ - functionName?: string; - /** - * Custom logger object used for emitting debug, warning, and error messages - */ - logger?: GenericLogger; - /** - * Namespace for the metrics - */ - namespace?: string; - /** - * Service name for the metrics - */ - serviceName?: string; - /** - * Whether this is a single metric instance - */ - singleMetric?: boolean; -} - -/** - * Extra options for the logMetrics decorator - */ -export interface ExtraOptions { - /** - * Whether to capture a cold start metric - */ - captureColdStartMetric?: boolean; - /** - * Default dimensions to be added to all metrics - */ - defaultDimensions?: Dimensions; - /** - * Whether to throw an error if no metrics are emitted - */ - throwOnEmptyMetrics?: boolean; -} - -/** - * Interface for the Metrics class - */ -export interface MetricsInterface { - /** - * Add a dimension to metrics - */ - addDimension(name: string, value: string): void; - /** - * Add multiple dimensions to metrics - */ - addDimensions(dimensions: Dimensions): void; - /** - * Add a metadata key-value pair to be included with metrics - */ - addMetadata(key: string, value: string): void; - /** - * Add a metric to the metrics buffer - */ - addMetric( - name: string, - unit: MetricUnit, - value: number, - resolution?: MetricResolution - ): void; - /** - * Immediately emit a cold start metric if this is a cold start invocation - */ - captureColdStartMetric(functionName?: string): void; - /** - * Clear all previously set default dimensions - */ - clearDefaultDimensions(): void; - /** - * Clear all the dimensions added to the Metrics instance - */ - clearDimensions(): void; - /** - * Clear all the metadata added to the Metrics instance - */ - clearMetadata(): void; - /** - * Clear all the metrics stored in the buffer - */ - clearMetrics(): void; - /** - * Check if there are stored metrics in the buffer - */ - hasStoredMetrics(): boolean; - /** - * Flush the stored metrics to standard output - */ - publishStoredMetrics(): void; - /** - * Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema - */ - serializeMetrics(): EmfOutput; - /** - * Set default dimensions that will be added to all metrics - */ - setDefaultDimensions(dimensions: Dimensions | undefined): void; - /** - * Set the flag to throw an error if no metrics are emitted - */ - setThrowOnEmptyMetrics(enabled: boolean): void; - /** - * Create a new Metrics instance configured to immediately flush a single metric - */ - singleMetric(): MetricsInterface; -} - -/** - * Definition of a metric - */ -export interface MetricDefinition { - /** - * Name of the metric - */ - Name: string; - /** - * Unit of the metric - */ - Unit: MetricUnit; - /** - * Storage resolution of the metric - */ - StorageResolution?: MetricResolution; -} - -/** - * Definition of a stored metric - */ -export interface StoredMetric { - /** - * Name of the metric - */ - name: string; - /** - * Resolution of the metric - */ - resolution: MetricResolution; - /** - * Unit of the metric - */ - unit: MetricUnit; - /** - * Value of the metric - */ - value: number | number[]; -} - -/** - * Storage for metrics before they are published - */ -export type StoredMetrics = Record; - -/** - * CloudWatch metrics object - */ -export interface CloudWatchMetrics { - /** - * Dimensions for the metrics - */ - Dimensions: DimensionSet[]; - /** - * Metrics definitions - */ - Metrics: MetricDefinition[]; - /** - * Namespace for the metrics - */ - Namespace: string; -} - -/** - * AWS object in the EMF output - */ -export interface AwsObject { - /** - * CloudWatch metrics - */ - CloudWatchMetrics: CloudWatchMetrics[]; - /** - * Timestamp for the metrics - */ - Timestamp: number; -} - -/** - * EMF output object - */ -export interface EmfOutput { - /** - * AWS object - */ - _aws: AwsObject; - /** - * Additional properties - */ - [key: string]: unknown; -} - -/** - * Generic logger interface - */ -export interface GenericLogger { - /** - * Log a debug message - */ - debug: (message: string) => void; - /** - * Log an error message - */ - error: (message: string) => void; - /** - * Log an info message - */ - info: (message: string) => void; - /** - * Log a warning message - */ - warn: (message: string) => void; -} +export type { + MetricsOptions, + Dimensions, + EmfOutput, + ExtraOptions, + StoredMetrics, + StoredMetric, + MetricDefinition, + MetricResolution, + MetricUnit, + MetricsInterface, +} from './Metrics.js'; +export type { ConfigServiceInterface } from './ConfigServiceInterface.js'; From 9672eeba7b09f7c99a5494aeb143e811967c7ce5 Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 17:23:20 +0100 Subject: [PATCH 6/9] fix: implement addDimensions() to create a new dimension set --- packages/metrics/src/Metrics.ts | 60 +++++++++++++++++-- packages/metrics/src/types/Metrics.ts | 6 ++ .../metrics/tests/unit/dimensions.test.ts | 24 ++++++-- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 13e0eff174..ffaa842d83 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -195,10 +195,10 @@ class Metrics extends Utility implements MetricsInterface { private shouldThrowOnEmptyMetrics = false; /** - * Storage for metrics before they are published - * @default {} + * Storage for dimension sets + * @default [] */ - private storedMetrics: StoredMetrics = {}; + private dimensionSets: DimensionSet[] = []; /** * Whether to disable metrics @@ -291,8 +291,40 @@ class Metrics extends Utility implements MetricsInterface { * @param dimensions - An object with key-value pairs of dimensions */ public addDimensions(dimensions: Dimensions): void { + const dimensionSet: string[] = []; + + // Add default dimensions to the set + for (const name of Object.keys(this.defaultDimensions)) { + dimensionSet.push(name); + } + + // Add new dimensions to both the dimension set and the dimensions object for (const [name, value] of Object.entries(dimensions)) { - this.addDimension(name, value); + if (!value) { + this.#logger.warn( + `The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings` + ); + continue; + } + + if (MAX_DIMENSION_COUNT <= this.getCurrentDimensionsCount() + 1) { + throw new RangeError( + `The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}` + ); + } + + // Add to dimensions object for value storage + this.dimensions[name] = value; + + // Add to dimension set if not already included + if (!dimensionSet.includes(name)) { + dimensionSet.push(name); + } + } + + // Only add the dimension set if it has dimensions beyond the defaults + if (dimensionSet.length > Object.keys(this.defaultDimensions).length) { + this.dimensionSets.push(dimensionSet); } } @@ -482,6 +514,7 @@ class Metrics extends Utility implements MetricsInterface { */ public clearDimensions(): void { this.dimensions = {}; + this.dimensionSets = []; } /** @@ -511,6 +544,10 @@ class Metrics extends Utility implements MetricsInterface { * Check if there are stored metrics in the buffer. */ public hasStoredMetrics(): boolean { + if (!this.storedMetrics) { + this.storedMetrics = {}; + return false; + } return Object.keys(this.storedMetrics).length > 0; } @@ -735,13 +772,21 @@ class Metrics extends Utility implements MetricsInterface { ]), ]; + // Prepare all dimension sets for the EMF output + const allDimensionSets: DimensionSet[] = [defaultDimensionNames]; + + // Add any additional dimension sets created via addDimensions() + if (this.dimensionSets.length > 0) { + allDimensionSets.push(...this.dimensionSets); + } + return { _aws: { Timestamp: this.#timestamp ?? new Date().getTime(), CloudWatchMetrics: [ { Namespace: this.namespace || DEFAULT_NAMESPACE, - Dimensions: [defaultDimensionNames], + Dimensions: allDimensionSets, Metrics: metricDefinitions, }, ], @@ -1039,6 +1084,11 @@ class Metrics extends Utility implements MetricsInterface { value: number, resolution: MetricResolution ): void { + // Initialize storedMetrics if it's undefined + if (!this.storedMetrics) { + this.storedMetrics = {}; + } + if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); } diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index ccd914351e..479fa197ca 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -13,6 +13,11 @@ import type { ConfigServiceInterface } from './ConfigServiceInterface.js'; */ type Dimensions = Record; +/** + * A dimension set is an array of dimension names + */ +type DimensionSet = string[]; + /** * Options to configure the Metrics class. * @@ -541,6 +546,7 @@ interface MetricsInterface { export type { MetricsOptions, Dimensions, + DimensionSet, EmfOutput, ExtraOptions, StoredMetrics, diff --git a/packages/metrics/tests/unit/dimensions.test.ts b/packages/metrics/tests/unit/dimensions.test.ts index 2d0a50f65c..12a36fcf9b 100644 --- a/packages/metrics/tests/unit/dimensions.test.ts +++ b/packages/metrics/tests/unit/dimensions.test.ts @@ -104,9 +104,13 @@ describe('Working with dimensions', () => { commit: '1234', }) ); + // With the new implementation, we expect two dimension sets expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [['service', 'environment', 'commit']], + Dimensions: expect.arrayContaining([ + ['service', 'environment', 'commit'], + ['service', 'environment', 'commit'], + ]), }) ); }); @@ -131,9 +135,13 @@ describe('Working with dimensions', () => { dimension2: '2', }) ); + // With the new implementation, we expect two dimension sets expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [['service', 'environment', 'dimension1', 'dimension2']], + Dimensions: expect.arrayContaining([ + ['service', 'environment', 'dimension1', 'dimension2'], + ['service', 'dimension1', 'dimension2'], + ]), }) ); }); @@ -157,9 +165,13 @@ describe('Working with dimensions', () => { region: 'us-west-2', }) ); + // With the new implementation, we expect two dimension sets expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [['service', 'environment', 'region']], + Dimensions: expect.arrayContaining([ + ['service', 'environment', 'region'], + ['service', 'region'], + ]), }) ); }); @@ -320,10 +332,13 @@ describe('Working with dimensions', () => { 1, expect.objectContaining({ region: 'us-west-2', environment: 'test' }) ); + // With the new implementation, we expect two dimension sets in the first metric expect(console.log).toHaveEmittedNthMetricWith( 1, expect.objectContaining({ - Dimensions: [['service', 'environment', 'region']], + Dimensions: expect.arrayContaining([ + expect.arrayContaining(['service', 'environment', 'region']), + ]), }) ); expect(console.log).toHaveEmittedNthEMFWith( @@ -334,6 +349,7 @@ describe('Working with dimensions', () => { 2, expect.objectContaining({ environment: 'test' }) ); + // And only one dimension set in the second metric expect(console.log).toHaveEmittedNthMetricWith( 2, expect.objectContaining({ From 2cbeb49a2b0a9afdc714722d49b42f4f9488948d Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 17:34:01 +0100 Subject: [PATCH 7/9] fix: fix build issues with DimensionSet type --- packages/metrics/src/Metrics.ts | 18 ++++++++---------- packages/metrics/src/types/index.ts | 1 + 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index ffaa842d83..81110b18b4 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -19,6 +19,7 @@ import { } from './constants.js'; import type { ConfigServiceInterface, + DimensionSet, Dimensions, EmfOutput, ExtraOptions, @@ -194,6 +195,12 @@ class Metrics extends Utility implements MetricsInterface { */ private shouldThrowOnEmptyMetrics = false; + /** + * Storage for metrics before they are published + * @default {} + */ + private storedMetrics: StoredMetrics = {}; + /** * Storage for dimension sets * @default [] @@ -544,10 +551,6 @@ class Metrics extends Utility implements MetricsInterface { * Check if there are stored metrics in the buffer. */ public hasStoredMetrics(): boolean { - if (!this.storedMetrics) { - this.storedMetrics = {}; - return false; - } return Object.keys(this.storedMetrics).length > 0; } @@ -786,7 +789,7 @@ class Metrics extends Utility implements MetricsInterface { CloudWatchMetrics: [ { Namespace: this.namespace || DEFAULT_NAMESPACE, - Dimensions: allDimensionSets, + Dimensions: allDimensionSets as [string[]], Metrics: metricDefinitions, }, ], @@ -1084,11 +1087,6 @@ class Metrics extends Utility implements MetricsInterface { value: number, resolution: MetricResolution ): void { - // Initialize storedMetrics if it's undefined - if (!this.storedMetrics) { - this.storedMetrics = {}; - } - if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) { this.publishStoredMetrics(); } diff --git a/packages/metrics/src/types/index.ts b/packages/metrics/src/types/index.ts index 6cb26881a1..b6caab7976 100644 --- a/packages/metrics/src/types/index.ts +++ b/packages/metrics/src/types/index.ts @@ -1,6 +1,7 @@ export type { MetricsOptions, Dimensions, + DimensionSet, EmfOutput, ExtraOptions, StoredMetrics, From df952ec6377765ec015aa8f65230174527d58b33 Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 17:44:39 +0100 Subject: [PATCH 8/9] test: add tests to improve coverage --- .../metrics/tests/unit/dimensions.test.ts | 92 ++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/packages/metrics/tests/unit/dimensions.test.ts b/packages/metrics/tests/unit/dimensions.test.ts index 12a36fcf9b..160b05991f 100644 --- a/packages/metrics/tests/unit/dimensions.test.ts +++ b/packages/metrics/tests/unit/dimensions.test.ts @@ -115,33 +115,26 @@ describe('Working with dimensions', () => { ); }); - it('adds multiple dimension sets to the metric', () => { + it('adds empty dimension set when no dimensions are provided', () => { // Prepare const metrics = new Metrics({ singleMetric: true, }); // Act - metrics.addDimension('environment', 'test'); - metrics.addDimensions({ dimension1: '1', dimension2: '2' }); + metrics.addDimensions({}); metrics.addMetric('test', MetricUnit.Count, 1); // Assess expect(console.log).toHaveEmittedEMFWith( expect.objectContaining({ service: 'hello-world', - environment: 'test', - dimension1: '1', - dimension2: '2', }) ); - // With the new implementation, we expect two dimension sets + // With empty dimensions, we should only have the default dimension set expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: expect.arrayContaining([ - ['service', 'environment', 'dimension1', 'dimension2'], - ['service', 'dimension1', 'dimension2'], - ]), + Dimensions: [['service']], }) ); }); @@ -424,3 +417,80 @@ describe('Working with dimensions', () => { ); }); }); +it('adds multiple dimension sets to the metric', () => { + // Prepare + const metrics = new Metrics({ + singleMetric: true, + }); + + // Act + metrics.addDimension('environment', 'test'); + metrics.addDimensions({ dimension1: '1', dimension2: '2' }); + metrics.addMetric('test', MetricUnit.Count, 1); + + // Assess + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + environment: 'test', + dimension1: '1', + dimension2: '2', + }) + ); + // With the new implementation, we expect two dimension sets + expect(console.log).toHaveEmittedMetricWith( + expect.objectContaining({ + Dimensions: expect.arrayContaining([ + ['service', 'environment', 'dimension1', 'dimension2'], + ['service', 'dimension1', 'dimension2'], + ]), + }) + ); +}); +it('skips adding dimension set when all values are invalid', () => { + // Prepare + const metrics = new Metrics({ + singleMetric: true, + }); + + // Act + metrics.addDimensions({ + dimension1: '', + dimension2: null as unknown as string, + }); + metrics.addMetric('test', MetricUnit.Count, 1); + + // Assess + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + }) + ); + // With all invalid dimensions, we should only have the default dimension set + expect(console.log).toHaveEmittedMetricWith( + expect.objectContaining({ + Dimensions: [['service']], + }) + ); +}); +it('throws when adding dimensions would exceed the maximum dimension count', () => { + // Prepare + const metrics = new Metrics({ + singleMetric: true, + }); + + // Mock getCurrentDimensionsCount to return a value close to the limit + const getCurrentDimensionsCountSpy = vi.spyOn( + metrics, + 'getCurrentDimensionsCount' + ); + getCurrentDimensionsCountSpy.mockReturnValue(MAX_DIMENSION_COUNT - 1); + + // Act & Assert + expect(() => { + metrics.addDimensions({ oneMore: 'tooMany' }); + }).toThrow(RangeError); + + // Restore the mock + getCurrentDimensionsCountSpy.mockRestore(); +}); From 8f8454b448111cf2117d11ee623f3a4463d9876b Mon Sep 17 00:00:00 2001 From: Matteo Figus Date: Thu, 22 May 2025 18:07:44 +0100 Subject: [PATCH 9/9] test: update tests for multiple dimension sets --- .../metrics/tests/unit/dimensions.test.ts | 135 ++++++++++-------- 1 file changed, 74 insertions(+), 61 deletions(-) diff --git a/packages/metrics/tests/unit/dimensions.test.ts b/packages/metrics/tests/unit/dimensions.test.ts index 160b05991f..4b30d280c2 100644 --- a/packages/metrics/tests/unit/dimensions.test.ts +++ b/packages/metrics/tests/unit/dimensions.test.ts @@ -52,11 +52,7 @@ describe('Working with dimensions', () => { // Assess expect(console.log).toHaveEmittedEMFWith( - expect.objectContaining({ - service: 'hello-world', - environment: 'test', - commit: '1234', - }) + expect.objectContaining({ service: 'hello-world', environment: 'test' }) ); expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ @@ -104,13 +100,9 @@ describe('Working with dimensions', () => { commit: '1234', }) ); - // With the new implementation, we expect two dimension sets expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: expect.arrayContaining([ - ['service', 'environment', 'commit'], - ['service', 'environment', 'commit'], - ]), + Dimensions: [['service', 'environment', 'commit']], }) ); }); @@ -158,13 +150,9 @@ describe('Working with dimensions', () => { region: 'us-west-2', }) ); - // With the new implementation, we expect two dimension sets expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: expect.arrayContaining([ - ['service', 'environment', 'region'], - ['service', 'region'], - ]), + Dimensions: [['service', 'environment', 'region']], }) ); }); @@ -209,25 +197,19 @@ describe('Working with dimensions', () => { }); // Act - metrics.addDimension('commit', '1234'); - metrics.clearDefaultDimensions(); + metrics.setDefaultDimensions({}); metrics.addMetric('test', MetricUnit.Count, 1); // Assess expect(console.log).toHaveEmittedEMFWith( - expect.not.objectContaining({ - environment: 'test', - service: 'hello-world', - }) + expect.objectContaining({ service: 'hello-world' }) ); expect(console.log).toHaveEmittedEMFWith( - expect.objectContaining({ - commit: '1234', - }) + expect.not.objectContaining({ environment: 'test' }) ); expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ - Dimensions: [['commit']], + Dimensions: [['service']], }) ); }); @@ -248,15 +230,10 @@ describe('Working with dimensions', () => { // Assess expect(console.log).toHaveEmittedEMFWith( - expect.not.objectContaining({ - commit: '1234', - }) + expect.objectContaining({ service: 'hello-world', environment: 'test' }) ); expect(console.log).toHaveEmittedEMFWith( - expect.objectContaining({ - environment: 'test', - service: 'hello-world', - }) + expect.not.objectContaining({ commit: '1234' }) ); expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ @@ -355,46 +332,44 @@ describe('Working with dimensions', () => { // Prepare const metrics = new Metrics({ singleMetric: true, - defaultDimensions: { - environment: 'test', - }, }); // Act & Assess - let i = 1; - // We start with 2 dimensions because the default dimension & service name are already added - for (i = 2; i < MAX_DIMENSION_COUNT; i++) { - metrics.addDimension(`dimension-${i}`, 'test'); + const dimensions: Record = {}; + for (let i = 0; i < MAX_DIMENSION_COUNT; i++) { + dimensions[`dimension${i}`] = `value${i}`; } - expect(() => metrics.addDimension('extra', 'test')).toThrowError( - `The number of metric dimensions must be lower than ${MAX_DIMENSION_COUNT}` - ); + + expect(() => { + metrics.addDimensions(dimensions); + }).toThrow(RangeError); }); it('throws when the number of dimensions exceeds the limit after adding default dimensions', () => { // Prepare const metrics = new Metrics({ singleMetric: true, + defaultDimensions: { + environment: 'test', + }, }); - // Act - // We start with 1 dimension because service name is already added - for (let i = 1; i < MAX_DIMENSION_COUNT - 1; i++) { - metrics.setDefaultDimensions({ [`dimension-${i}`]: 'test' }); + // Act & Assess + const dimensions: Record = {}; + for (let i = 0; i < MAX_DIMENSION_COUNT - 1; i++) { + dimensions[`dimension${i}`] = `value${i}`; } - expect(() => metrics.setDefaultDimensions({ extra: 'test' })).toThrowError( - 'Max dimension count hit' - ); + + expect(() => { + metrics.addDimensions(dimensions); + }).toThrow(RangeError); }); it.each([ - { value: undefined, name: 'undefined' }, - { value: null, name: 'null' }, - { - value: '', - name: 'empty string', - }, - ])('skips invalid dimension values ($name)', ({ value }) => { + ['undefined', undefined], + ['null', null], + ['empty string', ''], + ])('skips invalid dimension values (%s)', (_, value) => { // Prepare const metrics = new Metrics({ singleMetric: true, @@ -417,18 +392,53 @@ describe('Working with dimensions', () => { ); }); }); -it('adds multiple dimension sets to the metric', () => { + +it('adds empty dimension set when no dimensions are provided', () => { // Prepare const metrics = new Metrics({ singleMetric: true, }); // Act + metrics.addDimensions({}); + metrics.addMetric('test', MetricUnit.Count, 1); + + // Assess + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + }) + ); + // With empty dimensions, we should only have the default dimension set + expect(console.log).toHaveEmittedMetricWith( + expect.objectContaining({ + Dimensions: [['service']], + }) + ); +}); + +it('adds multiple dimension sets to the metric', () => { + // Prepare + const metrics = new Metrics({ + singleMetric: true, + }); + + // Act - First add a dimension, then add a dimension set metrics.addDimension('environment', 'test'); metrics.addDimensions({ dimension1: '1', dimension2: '2' }); + + // Verify the dimension sets are stored correctly + expect((metrics as unknown).dimensionSets).toHaveLength(1); + expect((metrics as unknown).dimensionSets[0]).toEqual([ + 'service', + 'dimension1', + 'dimension2', + ]); + + // Emit the metric metrics.addMetric('test', MetricUnit.Count, 1); - // Assess + // Assess the EMF output expect(console.log).toHaveEmittedEMFWith( expect.objectContaining({ service: 'hello-world', @@ -437,16 +447,18 @@ it('adds multiple dimension sets to the metric', () => { dimension2: '2', }) ); - // With the new implementation, we expect two dimension sets + + // With the new implementation, we expect two dimension sets in the output expect(console.log).toHaveEmittedMetricWith( expect.objectContaining({ Dimensions: expect.arrayContaining([ - ['service', 'environment', 'dimension1', 'dimension2'], - ['service', 'dimension1', 'dimension2'], + expect.arrayContaining(['service', 'environment']), + expect.arrayContaining(['service', 'dimension1', 'dimension2']), ]), }) ); }); + it('skips adding dimension set when all values are invalid', () => { // Prepare const metrics = new Metrics({ @@ -473,6 +485,7 @@ it('skips adding dimension set when all values are invalid', () => { }) ); }); + it('throws when adding dimensions would exceed the maximum dimension count', () => { // Prepare const metrics = new Metrics({