Repository: awslabs/aws-embedded-metrics-node
Pull request #121 (2022-06-22 15:32:55)

Support 30 dimensions per dimension set
*Issue #, if available:*
N/A


*Description of changes:*
Max dimensions limit updated to 30 and throw exception if the limit is exceeded.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.



Patch:
diff --git a/package-lock.json b/package-lock.json
index 4012b15..d58a6f0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "aws-embedded-metrics",
-  "version": "2.0.6",
+  "version": "3.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "aws-embedded-metrics",
-      "version": "2.0.6",
+      "version": "3.0.0",
       "license": "Apache-2.0",
       "dependencies": {
         "@datastructures-js/heap": "^4.0.2"
diff --git a/package.json b/package.json
index 1173e9e..1f29cbd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "aws-embedded-metrics",
-  "version": "2.0.6",
+  "version": "3.0.0",
   "description": "AWS Embedded Metrics Client Library",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
diff --git a/src/Constants.ts b/src/Constants.ts
index c6c827c..264fdbc 100644
--- a/src/Constants.ts
+++ b/src/Constants.ts
@@ -14,7 +14,7 @@
  */
 
 export enum Constants {
-  MAX_DIMENSIONS = 9,
+  MAX_DIMENSION_SET_SIZE = 30,
   DEFAULT_NAMESPACE = 'aws-embedded-metrics',
   MAX_METRICS_PER_EVENT = 100,
   MAX_VALUES_PER_METRIC = 100,
diff --git a/src/exceptions/DimensionSetExceededError.ts b/src/exceptions/DimensionSetExceededError.ts
new file mode 100644
index 0000000..d9bac2f
--- /dev/null
+++ b/src/exceptions/DimensionSetExceededError.ts
@@ -0,0 +1,8 @@
+export class DimensionSetExceededError extends Error {
+  constructor(msg: string) {
+    super(msg);
+
+    // Set the prototype explicitly.
+    Object.setPrototypeOf(this, DimensionSetExceededError.prototype);
+  }
+}
diff --git a/src/logger/MetricsContext.ts b/src/logger/MetricsContext.ts
index eb23455..1cbbe03 100644
--- a/src/logger/MetricsContext.ts
+++ b/src/logger/MetricsContext.ts
@@ -17,6 +17,8 @@ import Configuration from '../config/Configuration';
 import { LOG } from '../utils/Logger';
 import { MetricValues } from './MetricValues';
 import { Unit } from './Unit';
+import { Constants } from '../Constants';
+import { DimensionSetExceededError } from '../exceptions/DimensionSetExceededError';
 
 interface IProperties {
   [s: string]: unknown;
@@ -104,6 +106,17 @@ export class MetricsContext {
     this.defaultDimensions = dimensions;
   }
 
+  /**
+   * Validates dimension set length is not more than Constants.MAX_DIMENSION_SET_SIZE
+   *
+   * @param dimensionSet
+   */
+  public static validateDimensionSet(dimensionSet: Record<string, string>): void {
+    if (Object.keys(dimensionSet).length > Constants.MAX_DIMENSION_SET_SIZE)
+      throw new DimensionSetExceededError(
+        `Maximum number of dimensions per dimension set allowed are ${Constants.MAX_DIMENSION_SET_SIZE}`)
+  }
+
   /**
    * Adds a new set of dimensions. Any time a new dimensions set
    * is added, the set is first prepended by the default dimensions.
@@ -111,6 +124,8 @@ export class MetricsContext {
    * @param dimensions
    */
   public putDimensions(incomingDimensionSet: Record<string, string>): void {
+    MetricsContext.validateDimensionSet(incomingDimensionSet)
+
     if (this.dimensions.length === 0) {
       this.dimensions.push(incomingDimensionSet);
       return;
@@ -147,6 +162,9 @@ export class MetricsContext {
    */
   public setDimensions(dimensionSets: Array<Record<string, string>>): void {
     this.shouldUseDefaultDimensions = false;
+
+    dimensionSets.forEach(dimensionSet => MetricsContext.validateDimensionSet(dimensionSet))
+
     this.dimensions = dimensionSets;
   }
 
diff --git a/src/serializers/LogSerializer.ts b/src/serializers/LogSerializer.ts
index 17d8219..ca0788d 100644
--- a/src/serializers/LogSerializer.ts
+++ b/src/serializers/LogSerializer.ts
@@ -15,6 +15,7 @@
 import { MaxHeap } from '@datastructures-js/heap';
 
 import { Constants } from '../Constants';
+import { DimensionSetExceededError } from '../exceptions/DimensionSetExceededError';
 import { MetricsContext } from '../logger/MetricsContext';
 import { ISerializer } from './Serializer';
 
@@ -44,7 +45,12 @@ export class LogSerializer implements ISerializer {
       // support more dimensions
       // in the future it may make sense to introduce a higher-order
       // representation for sink-specific validations
-      const keys = Object.keys(d).slice(0, Constants.MAX_DIMENSIONS);
+      const keys = Object.keys(d);
+      if (keys.length > Constants.MAX_DIMENSION_SET_SIZE) {
+        const errMsg = `Maximum number of dimensions allowed are ${Constants.MAX_DIMENSION_SET_SIZE}.` +
+        `Account for default dimensions if not using set_dimensions.`;
+        throw new DimensionSetExceededError(errMsg)
+      }
       dimensionKeys.push(keys);
       dimensionProperties = { ...dimensionProperties, ...d };
     });


Test patch:
diff --git a/src/logger/__tests__/MetricsContext.test.ts b/src/logger/__tests__/MetricsContext.test.ts
index 1d91176..5b09d83 100644
--- a/src/logger/__tests__/MetricsContext.test.ts
+++ b/src/logger/__tests__/MetricsContext.test.ts
@@ -1,5 +1,6 @@
 import * as faker from 'faker';
 import { MetricsContext } from '../MetricsContext';
+import { DimensionSetExceededError } from '../../exceptions/DimensionSetExceededError';
 
 test('can set property', () => {
   // arrange
@@ -16,6 +17,19 @@ test('can set property', () => {
   expect(actualValue).toBe(expectedValue);
 });
 
+test('setDimensions allows 30 dimensions', () => {
+  // arrange
+  const context = MetricsContext.empty();
+  const numOfDimensions = 30
+  const expectedDimensionSet = getDimensionSet(numOfDimensions);
+
+  // act
+  context.setDimensions([expectedDimensionSet]);
+
+  // assert
+  expect(context.getDimensions()[0]).toStrictEqual(expectedDimensionSet);
+});
+
 test('putDimension adds key to dimension and sets the dimension as a property', () => {
   // arrange
   const context = MetricsContext.empty();
@@ -203,3 +217,34 @@ test('createCopyWithContext copies shouldUseDefaultDimensions', () => {
   expect(newContext).not.toBe(context);
   expect(newContext.getDimensions()).toEqual([]);
 });
+
+test('putDimensions checks the dimension set length', () => {
+  // arrange
+  const context = MetricsContext.empty();
+  const numOfDimensions = 33
+
+  expect(() => {
+    context.putDimensions(getDimensionSet(numOfDimensions))
+  }).toThrow(DimensionSetExceededError);
+});
+
+test('setDimensions checks all the dimension sets have less than 30 dimensions', () => {
+  // arrange
+  const context = MetricsContext.empty();
+  const numOfDimensions = 33
+
+  expect(() => {
+    context.setDimensions([getDimensionSet(numOfDimensions)])
+  }).toThrow(DimensionSetExceededError);
+});
+
+const getDimensionSet = (numOfDimensions: number) => {
+  const dimensionSet:Record<string, string> = {}
+
+  for (let i = 0; i < numOfDimensions; i++) {
+    const expectedKey = `${i}`;
+    dimensionSet[expectedKey] = faker.random.word();
+  }
+
+  return dimensionSet;
+}
diff --git a/src/serializers/__tests__/LogSerializer.test.ts b/src/serializers/__tests__/LogSerializer.test.ts
index 8b30cad..cad7b23 100644
--- a/src/serializers/__tests__/LogSerializer.test.ts
+++ b/src/serializers/__tests__/LogSerializer.test.ts
@@ -2,6 +2,7 @@ import * as faker from 'faker';
 import { Constants } from '../../Constants';
 import { MetricsContext } from '../../logger/MetricsContext';
 import { LogSerializer } from '../LogSerializer';
+import { DimensionSetExceededError } from '../../exceptions/DimensionSetExceededError';
 
 test('serializes dimensions', () => {
   // arrange
@@ -22,33 +23,6 @@ test('serializes dimensions', () => {
   assertJsonEquality(resultJson, expected);
 });
 
-test('cannot serialize more than 9 dimensions', () => {
-  // arrange
-  const dimensions: any = {};
-  const dimensionPointers = [];
-  const allowedDimensions = 9;
-  const dimensionsToAdd = 11;
-  for (let i = 0; i < dimensionsToAdd; i++) {
-    const expectedKey = `${i}`;
-    const expectedValue = faker.random.word();
-    dimensions[expectedKey] = expectedValue;
-    dimensionPointers.push(expectedKey);
-  }
-
-  const expectedDimensionPointers = dimensionPointers.slice(0, allowedDimensions);
-
-  const expected: any = { ...getEmptyPayload(), ...dimensions };
-  expected._aws.CloudWatchMetrics[0].Dimensions.push(expectedDimensionPointers);
-
-  const context = getContext();
-  context.putDimensions(dimensions);
-  // act
-  const resultJson = serializer.serialize(context)[0];
-
-  // assert
-  assertJsonEquality(resultJson, expected);
-});
-
 test('serializes properties', () => {
   // arrange
   const expectedKey = faker.random.word();
@@ -187,6 +161,29 @@ test('serializes metrics with more than 100 values each into multiple events', (
   }
 });
 
+test('cannot serialize more than 30 dimensions', () => {
+  // arrange
+  const context = MetricsContext.empty();
+  const defaultDimensionKey = faker.random.word();
+  const defaultDimensionValue = faker.random.word();
+  const numOfCustomDimensions = 30;
+  const dimensionSet: Record<string, string> = {};
+
+  for (let i = 0; i < numOfCustomDimensions; i++) {
+    const expectedKey = `${i}`;
+    dimensionSet[expectedKey] = faker.random.word();
+  }
+
+  // act
+  context.setDefaultDimensions({ [defaultDimensionKey]: defaultDimensionValue });
+  context.putDimensions(dimensionSet);
+
+  // assert
+  expect(() => {
+    serializer.serialize(context)
+  }).toThrow(DimensionSetExceededError);
+});
+
 const assertJsonEquality = (resultJson: string, expectedObj: any) => {
   const actual = JSON.parse(resultJson);
   expect(actual).toStrictEqual(expectedObj);