Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Propose new API for aggregating metrics #54

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/logger/MetricsLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import { IEnvironment } from '../environment/IEnvironment';
import { MetricsContext } from './MetricsContext';
import { Unit } from './Unit';

type Metrics = { [key: string]: number | number[]; };
Copy link

@simonespa simonespa Sep 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type declaration doesn't allow the definition of the "unit" (e.g. count, milliseconds, etc.).

I was hoping to set something like:

 {
    Name: 'ResponseTime',
    Value: 59,
    Unit: Unit.Milliseconds
}

Also, multiple metrics may apply to the same dimensions, shouldn't we define this as an array of metric items as discussed in #46?

Example:

m.putMetricWithDimensions({
   metrics: [
      {
         Name: 'ResponseTime',
         Value: 322,
         Unit: Unit.Milliseconds
      },
      {
         Name: 'RequestCount',
         Value: 1,
         Unit: Unit.Count
      }
   ],
   namespace: '/example/namespace',
   dimensions: [{ PageType: pageType }],
   stripDefaultDimensions: true
});

Copy link
Member Author

@jaredcnance jaredcnance Sep 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agree on unit. The idea here was that the key would be used as the metric name which naturally supports multiple distinct metrics. I don’t recall exactly why I proposed it this way, probably because it was less verbose and easier to read but once you add units to the mix it breaks down. Happy to go with this.

type MetricWithDimensions = {
metrics: Metrics,
namespace?: string | undefined,
dimensions?: Array<Record<string, string>> | undefined,
stripDefaultDimensions?: boolean | undefined;
};

/**
* An async metrics logger.
* Use this interface to publish logs to CloudWatch Logs
Expand Down Expand Up @@ -114,6 +122,10 @@ export class MetricsLogger {
return this;
}

public putMetricWithDimensions(metricWithDimensions: MetricWithDimensions): MetricsLogger {
return this;
}

/**
* Creates a new logger using the same contextual data as
* the previous logger. This allows you to flush the instances
Expand Down
146 changes: 146 additions & 0 deletions src/logger/__tests__/MetricsLogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IEnvironment } from '../../environment/IEnvironment';
import { ISink } from '../../sinks/Sink';
import { MetricsContext } from '../MetricsContext';
import { MetricsLogger } from '../MetricsLogger';
import { Constants } from '../../Constants';

const createSink = () => new TestSink();
const createEnvironment = (sink: ISink) => {
Expand All @@ -21,6 +22,16 @@ const createEnvironment = (sink: ISink) => {
};
const createLogger = (env: EnvironmentProvider) => new MetricsLogger(env);

const DEFAULT_DIMENSIONS = { Foo: 'Bar' };
const createLoggerWithDefaultDimensions = (): MetricsLogger => {
const context = MetricsContext.empty();
context.setDefaultDimensions(DEFAULT_DIMENSIONS);

const sink = createSink();
const env = createEnvironment(sink);
return new MetricsLogger(() => Promise.resolve(env), context);
}

let sink: TestSink;
let environment: IEnvironment;
let logger: MetricsLogger;
Expand Down Expand Up @@ -297,6 +308,141 @@ test('context is preserved across flush() calls', async () => {
}
});

test('putMetricWithDimensions metric only', async () => {
// arrange
const logger = createLoggerWithDefaultDimensions();

// act
logger.putMetricWithDimensions({
metrics: { "MyMetric": 100 }
});

await logger.flush();

// assert
expect(sink.events).toHaveLength(1);
const evt = sink.events[0];
expect(evt.metrics.size).toBe(1);
expect(evt.metrics.get("MyMetric")).toBe(100);
// everything else should be defaults
expect(evt.namespace).toBe(Constants.DEFAULT_NAMESPACE);
expect(evt.getDimensions()[0]).toBe(DEFAULT_DIMENSIONS);
});

test('putMetricWithDimensions single metric with namespace', async () => {
// arrange
const logger = createLoggerWithDefaultDimensions();

// act
logger.putMetricWithDimensions({
metrics: { "MyMetric": 100 },
namespace: "My-Namespace"
});

// act
await logger.flush();

// assert
expect(sink.events).toHaveLength(1);
const evt = sink.events[0];
expect(evt.metrics.size).toBe(1);
expect(evt.metrics.get("MyMetric")).toBe(100);
expect(evt.namespace).toBe("My-Namespace");
expect(evt.getDimensions()[0]).toBe(DEFAULT_DIMENSIONS);
});


test('putMetricWithDimensions with single dimensions and default namespace', async () => {
// arrange
const logger = createLoggerWithDefaultDimensions();
const client = 'client';

// act
logger.putMetricWithDimensions({
metrics: { Metric1: 100 },
dimensions: [{ client }]
});

await logger.flush();

// assert
expect(sink.events).toHaveLength(1);
const evt = sink.events[0];
expect(evt.metrics.size).toBe(1);
expect(evt.metrics.get("MyMetric")).toBe(100);
expect(evt.namespace).toBe(Constants.DEFAULT_NAMESPACE);
expect(evt.getDimensions()).toBe([{ ...DEFAULT_DIMENSIONS, client }]);
});

test('putMetricWithDimensions along multiple dimensions', async () => {
// arrange
const logger = createLoggerWithDefaultDimensions();
const client = 'client';
const pageType = 'pageType';

// act
logger.putMetricWithDimensions({
metrics: {
Metric1: 100,
},
namespace: "My Namespace",
dimensions: [
yaozhaoy marked this conversation as resolved.
Show resolved Hide resolved
{ client },
{ pageType },
{ client, pageType },
]
});

await logger.flush();

// assert
expect(sink.events).toHaveLength(1);
const evt = sink.events[0];
expect(evt.metrics.size).toBe(1);
expect(evt.metrics.get("MyMetric")).toBe(100);
expect(evt.namespace).toBe("My-Namespace");
expect(evt.getDimensions()[0]).toBe([
{ ...DEFAULT_DIMENSIONS, client },
{ ...DEFAULT_DIMENSIONS, pageType },
{ ...DEFAULT_DIMENSIONS, client, pageType },
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part I'm not certain of. One question I have is what are users most likely to do? Do users prefer the default dimensions are included most of the time or are users overwriting them more often than not.

Copy link

@simonespa simonespa Sep 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tough one 😄

I don't use the default dimensions and having true by default will make me very happy. Nonetheless, this is a personal preference and I guess we should reason in terms of user expectations.

One way to see it could be to keep the same behaviour that putMetric and putDimensions have perhaps? Which means, the default should be false. Also, given the wording of the property is stripDefaultDimensions it entails that by default is disabled and if you want to activate it you must specify true. The opposite would have been keepDefaultDimensions with default set to false, and activation to true.

For people that don't use the default dimensions, I would expect to either set stripDefaultDimensions explicitly to true every time I call putMetricWithDimensions, or call setDimensions() with empty parameter once at the beginning and then regardless of what the value of stripDefaultDimensions is, the default shouldn't be set because they have been removed for the whole session.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jaredcnance any thoughts on this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now I’m leaning towards the proposal as is. I think it may make sense to offer a global configuration to disable default dimensions entirely. I suspect you either want them or you don’t and you may want that applied uniformly.

]);
});

test('putMetricWithDimensions without default dimensions', async () => {
// arrange
const logger = createLoggerWithDefaultDimensions();
const client = 'client';
const pageType = 'pageType';

// act
logger.putMetricWithDimensions({
metrics: {
Metric1: 100
},
namespace: "My-Namespace",
dimensions: [
{ client },
{ pageType },
{ client, pageType },
],
stripDefaultDimensions: true
});

await logger.flush();

// assert
expect(sink.events).toHaveLength(1);
const evt = sink.events[0];
expect(evt.metrics.size).toBe(1);
expect(evt.metrics.get("MyMetric")).toBe(100);
expect(evt.namespace).toBe("My-Namespace");
expect(evt.getDimensions()[0]).toBe([
{ client },
{ pageType },
{ client, pageType },
]);
});

const expectDimension = (key: string, value: string) => {
expect(sink.events).toHaveLength(1);
const dimensionSets = sink.events[0].getDimensions();
Expand Down