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

feat(metrics): rename method purgeStoredMetrics to publishStoredMetrics #377

Merged
merged 3 commits into from
Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions docs/core/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ export class MyFunction {

#### Manually

You can manually flush the metrics with `purgeStoredMetrics` as follows:
You can manually flush the metrics with `publishStoredMetrics` as follows:

!!! warning
Metrics, dimensions and namespace validation still applies.
Expand All @@ -354,7 +354,7 @@ const metrics = new Metrics();
const lambdaHandler: Handler = async () => {
metrics.addMetric('test-metric', MetricUnits.Count, 10);
const metricsObject = metrics.serializeMetrics();
metrics.purgeStoredMetrics();
metrics.publishStoredMetrics();
console.log(JSON.stringify(metricsObject));
};
```
Expand Down
2 changes: 1 addition & 1 deletion packages/metrics/examples/decorator/manual-flushing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const metrics = new Metrics();
const lambdaHandler: Handler = async () => {

metrics.addMetric('test-metric', MetricUnits.Count, 10);
metrics.purgeStoredMetrics();
metrics.publishStoredMetrics();
//Metrics will be published and cleared

return {
Expand Down
2 changes: 1 addition & 1 deletion packages/metrics/examples/manual-flushing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const metrics = new Metrics();

const lambdaHandler = async (): Promise<void> => {
metrics.addMetric('test-metric', MetricUnits.Count, 10);
metrics.purgeStoredMetrics();
metrics.publishStoredMetrics();
};

const handlerWithMiddleware = middy(lambdaHandler)
Expand Down
17 changes: 9 additions & 8 deletions packages/metrics/src/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const DEFAULT_NAMESPACE = 'default_namespace';
* export const handler = async (_event: any, _context: any) => {
* metrics.captureColdStart();
* metrics.addMetric('test-metric', MetricUnits.Count, 10);
* metrics.purgeStoredMetrics();
* metrics.publishStoredMetrics();
* };
* ```
*/
Expand Down Expand Up @@ -146,7 +146,7 @@ class Metrics implements MetricsInterface {
*/
public addMetric(name: string, unit: MetricUnit, value: number): void {
this.storeMetric(name, unit, value);
if (this.isSingleMetric) this.purgeStoredMetrics();
if (this.isSingleMetric) this.publishStoredMetrics();
}

/**
Expand Down Expand Up @@ -245,14 +245,15 @@ class Metrics implements MetricsInterface {

return result;
} finally {
this.purgeStoredMetrics();
this.publishStoredMetrics();
}
};
};
}

/**
* Synchronous function to actually publish your metrics. (Not needed if using logMetrics decorator).
* It will create a new EMF blob and log it to standard output to be then ingested by Cloudwatch logs and processed automatically for metrics creation.
*
* @example
*
Expand All @@ -263,11 +264,11 @@ class Metrics implements MetricsInterface {
*
* export const handler = async (_event: any, _context: any) => {
* metrics.addMetric('test-metric', MetricUnits.Count, 10);
* metrics.purgeStoredMetrics();
* metrics.publishStoredMetrics();
* };
* ```
*/
public purgeStoredMetrics(): void {
public publishStoredMetrics(): void {
const target = this.serializeMetrics();
console.log(JSON.stringify(target));
this.storedMetrics = {};
Expand All @@ -286,7 +287,7 @@ class Metrics implements MetricsInterface {
*
* export const handler = async (event: any, context: Context) => {
* metrics.raiseOnEmptyMetrics();
* metrics.purgeStoredMetrics(); // will throw since no metrics added.
* metrics.publishStoredMetrics(); // will throw since no metrics added.
* }
* ```
*/
Expand Down Expand Up @@ -357,7 +358,7 @@ class Metrics implements MetricsInterface {
/**
* CloudWatch EMF uses the same dimensions across all your metrics. Use singleMetric if you have a metric that should have different dimensions.
*
* You don't need to call purgeStoredMetrics() after calling addMetric for a singleMetrics, they will be flushed directly.
* You don't need to call publishStoredMetrics() after calling addMetric for a singleMetrics, they will be flushed directly.
*
* @example
*
Expand Down Expand Up @@ -428,7 +429,7 @@ class Metrics implements MetricsInterface {

private storeMetric(name: string, unit: MetricUnit, value: number): void {
if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) {
this.purgeStoredMetrics();
this.publishStoredMetrics();
}
this.storedMetrics[name] = {
unit,
Expand Down
2 changes: 1 addition & 1 deletion packages/metrics/src/MetricsInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface MetricsInterface {
clearMetrics(): void
clearDefaultDimensions(): void
logMetrics(options?: MetricsOptions): HandlerMethodDecorator
purgeStoredMetrics(): void
publishStoredMetrics(): void
serializeMetrics(): EmfOutput
setDefaultDimensions(dimensions: Dimensions | undefined): void
singleMetric(): Metrics
Expand Down
2 changes: 1 addition & 1 deletion packages/metrics/src/middleware/middy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const logMetrics = (target: Metrics | Metrics[], options: ExtraOptions = {}): mi

const logMetricsAfterOrError = async (): Promise<void> => {
metricsInstances.forEach((metrics: Metrics) => {
metrics.purgeStoredMetrics();
metrics.publishStoredMetrics();
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ const namespace = process.env.EXPECTED_NAMESPACE ?? 'CDKExample';
const serviceName = process.env.EXPECTED_SERVICE_NAME ?? 'MyFunctionWithStandardHandler';
const metricName = process.env.EXPECTED_METRIC_NAME ?? 'MyMetric';
const metricUnit = (process.env.EXPECTED_METRIC_UNIT as MetricUnits) ?? MetricUnits.Count;
const metricValue = process.env.EXPECTED_METRIC_VALUE ?? 1;
const metricValue = process.env.EXPECTED_METRIC_VALUE ?? '1';
const defaultDimensions = process.env.EXPECTED_DEFAULT_DIMENSIONS ?? '{"MyDimension":"MyValue"}';
const extraDimension = process.env.EXPECTED_EXTRA_DIMENSION ?? '{"MyExtraDimension":"MyExtraValue"}';
const singleMetricDimension = process.env.EXPECTED_SINGLE_METRIC_DIMENSION ?? '{"MySingleMetricDim":"MySingleValue"}';
const singleMetricName = process.env.EXPECTED_SINGLE_METRIC_NAME ?? 'MySingleMetric';
const singleMetricUnit = (process.env.EXPECTED_SINGLE_METRIC_UNIT as MetricUnits) ?? MetricUnits.Percent;
const singleMetricValue = process.env.EXPECTED_SINGLE_METRIC_VALUE ?? 2;
const singleMetricValue = process.env.EXPECTED_SINGLE_METRIC_VALUE ?? '2';

const metrics = new Metrics({ namespace: namespace, service: serviceName });

export const handler = async (_event: unknown, _context: Context): Promise<void> => {
metrics.captureColdStartMetric();
metrics.raiseOnEmptyMetrics();
metrics.setDefaultDimensions(JSON.parse(defaultDimensions));
metrics.addMetric(metricName, metricUnit, metricValue as number);
metrics.addMetric(metricName, metricUnit, parseInt(metricValue));
metrics.addDimension(
Object.entries(JSON.parse(extraDimension))[0][0],
Object.entries(JSON.parse(extraDimension))[0][1] as string,
Expand All @@ -30,8 +30,8 @@ export const handler = async (_event: unknown, _context: Context): Promise<void>
Object.entries(JSON.parse(singleMetricDimension))[0][0],
Object.entries(JSON.parse(singleMetricDimension))[0][1] as string,
);
metricWithItsOwnDimensions.addMetric(singleMetricName, singleMetricUnit, singleMetricValue as number);
metricWithItsOwnDimensions.addMetric(singleMetricName, singleMetricUnit, parseInt(singleMetricValue));

metrics.purgeStoredMetrics();
metrics.publishStoredMetrics();
metrics.raiseOnEmptyMetrics();
};
164 changes: 112 additions & 52 deletions packages/metrics/tests/e2e/standardFunctions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// SPDX-License-Identifier: MIT-0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A following PR will focus on refactoring e2e test to reuse tracing one

/**
* Test metrics decorator
* Test metrics standard functions
*
* @group e2e/metrics/decorator
* @group e2e/metrics/standardFunctions
*/

import { randomUUID } from 'crypto';
Expand All @@ -19,43 +19,44 @@ const cloudwatchClient = new AWS.CloudWatch();
const lambdaClient = new AWS.Lambda();

const integTestApp = new App();
const stack = new Stack(integTestApp, 'ExampleIntegTest');
const stack = new Stack(integTestApp, 'MetricsE2EStandardFunctionsStack');

describe('coldstart', () => {
it('can be deploy succcessfully', async () => {
// GIVEN
const startTime = new Date();
const expectedNamespace = randomUUID(); // to easily find metrics back at assert phase
const expectedServiceName = 'MyFunctionWithStandardHandler';
const expectedMetricName = 'MyMetric';
const expectedMetricUnit = MetricUnits.Count;
const expectedMetricValue = '1';
const expectedDefaultDimensions = { MyDimension: 'MyValue' };
const expectedExtraDimension = { MyExtraDimension: 'MyExtraValue' };
const expectedSingleMetricDimension = { MySingleMetricDim: 'MySingleValue' };
const expectedSingleMetricName = 'MySingleMetric';
const expectedSingleMetricUnit = MetricUnits.Percent;
const expectedSingleMetricValue = '2';
const functionName = 'MyFunctionWithStandardHandler';
new lambda.NodejsFunction(stack, 'MyFunction', {
functionName: functionName,
environment: {
EXPECTED_NAMESPACE: expectedNamespace,
EXPECTED_SERVICE_NAME: expectedServiceName,
EXPECTED_METRIC_NAME: expectedMetricName,
EXPECTED_METRIC_UNIT: expectedMetricUnit,
EXPECTED_METRIC_VALUE: expectedMetricValue,
EXPECTED_DEFAULT_DIMENSIONS: JSON.stringify(expectedDefaultDimensions),
EXPECTED_EXTRA_DIMENSION: JSON.stringify(expectedExtraDimension),
EXPECTED_SINGLE_METRIC_DIMENSION: JSON.stringify(expectedSingleMetricDimension),
EXPECTED_SINGLE_METRIC_NAME: expectedSingleMetricName,
EXPECTED_SINGLE_METRIC_UNIT: expectedSingleMetricUnit,
EXPECTED_SINGLE_METRIC_VALUE: expectedSingleMetricValue,
},
});
// GIVEN
const startTime = new Date();
const expectedNamespace = randomUUID(); // to easily find metrics back at assert phase
const expectedServiceName = 'MyFunctionWithStandardHandler';
const expectedMetricName = 'MyMetric';
const expectedMetricUnit = MetricUnits.Count;
const expectedMetricValue = '1';
const expectedDefaultDimensions = { MyDimension: 'MyValue' };
const expectedExtraDimension = { MyExtraDimension: 'MyExtraValue' };
const expectedSingleMetricDimension = { MySingleMetricDim: 'MySingleValue' };
const expectedSingleMetricName = 'MySingleMetric';
const expectedSingleMetricUnit = MetricUnits.Percent;
const expectedSingleMetricValue = '2';
const functionName = 'MyFunctionWithStandardHandler';
new lambda.NodejsFunction(stack, 'MyFunction', {
functionName: functionName,
tracing: Tracing.ACTIVE,
environment: {
EXPECTED_NAMESPACE: expectedNamespace,
EXPECTED_SERVICE_NAME: expectedServiceName,
EXPECTED_METRIC_NAME: expectedMetricName,
EXPECTED_METRIC_UNIT: expectedMetricUnit,
EXPECTED_METRIC_VALUE: expectedMetricValue,
EXPECTED_DEFAULT_DIMENSIONS: JSON.stringify(expectedDefaultDimensions),
EXPECTED_EXTRA_DIMENSION: JSON.stringify(expectedExtraDimension),
EXPECTED_SINGLE_METRIC_DIMENSION: JSON.stringify(expectedSingleMetricDimension),
EXPECTED_SINGLE_METRIC_NAME: expectedSingleMetricName,
EXPECTED_SINGLE_METRIC_UNIT: expectedSingleMetricUnit,
EXPECTED_SINGLE_METRIC_VALUE: expectedSingleMetricValue,
},
});

const stackArtifact = integTestApp.synth().getStackByName(stack.stackName);
const stackArtifact = integTestApp.synth().getStackByName(stack.stackName);

describe('happy cases', () => {
beforeAll(async () => {
const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({
profile: process.env.AWS_PROFILE,
});
Expand All @@ -66,7 +67,11 @@ describe('coldstart', () => {
await cloudFormation.deployStack({
stack: stackArtifact,
});
// and invoked
}, 200000);

it('capture ColdStart Metric', async () => {
// WHEN
// invoked
await lambdaClient
.invoke({
FunctionName: functionName,
Expand Down Expand Up @@ -106,27 +111,82 @@ describe('coldstart', () => {
MetricName: 'ColdStart',
Statistics: ['Sum'],
},
undefined,
undefined
)
.promise();

// Despite lambda has been called twice, coldstart metric sum should only be 1
const singleDataPoint = coldStartMetricStat.Datapoints ? coldStartMetricStat.Datapoints[0] : {};
expect(singleDataPoint.Sum).toBe(1);
}, 9000000);
});
}, 15000);
Copy link
Contributor

Choose a reason for hiding this comment

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

(minor) timeout is in ms. Will 15 sec be sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes the test itself is quite fast. but since we sleep for 10s to get metrics ingested it still takes around 11sec


afterAll(async () => {
if (!process.env.DISABLE_TEARDOWN) {
const stackArtifact = integTestApp.synth().getStackByName(stack.stackName);
it('produce added Metric with the default and extra one dimensions', async () => {
// GIVEN
const invocationCount = 2;

const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({
profile: process.env.AWS_PROFILE,
});
const cloudFormation = new CloudFormationDeployments({ sdkProvider });
// WHEN
// invoked
for (let i = 0; i < invocationCount; i++) {
await lambdaClient
.invoke({
FunctionName: functionName,
})
.promise();
}

await cloudFormation.destroyStack({
stack: stackArtifact,
});
}
}, 9000000);
// THEN
// sleep to allow metrics to be collected
await new Promise((resolve) => setTimeout(resolve, 10000));

// Check metric dimensions
const metrics = await cloudwatchClient
.listMetrics({
Namespace: expectedNamespace,
MetricName: expectedMetricName,
})
.promise();
expect(metrics.Metrics?.length).toBe(1);
const metric = metrics.Metrics?.[0];
const expectedDimensions = [
{ Name: 'service', Value: expectedServiceName },
{ Name: Object.keys(expectedDefaultDimensions)[0], Value: expectedDefaultDimensions.MyDimension },
{ Name: Object.keys(expectedExtraDimension)[0], Value: expectedExtraDimension.MyExtraDimension },
];
expect(metric?.Dimensions).toStrictEqual(expectedDimensions);

// Check coldstart metric value
const metricStat = await cloudwatchClient
.getMetricStatistics(
{
Namespace: expectedNamespace,
StartTime: new Date(startTime.getTime() - 60 * 1000), // minus 1 minute,
Dimensions: expectedDimensions,
EndTime: new Date(new Date().getTime() + 60 * 1000),
Period: 60,
MetricName: expectedMetricName,
Statistics: ['Sum'],
},
undefined
)
.promise();

// Since lambda has been called twice in this test and potentially more in others, metric sum should be at least of expectedMetricValue * invocationCount
const singleDataPoint = metricStat.Datapoints ? metricStat.Datapoints[0] : {};
expect(singleDataPoint.Sum).toBeGreaterThanOrEqual(parseInt(expectedMetricValue) * invocationCount);
}, 15000);

afterAll(async () => {
if (!process.env.DISABLE_TEARDOWN) {
const stackArtifact = integTestApp.synth().getStackByName(stack.stackName);

const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({
profile: process.env.AWS_PROFILE,
});
const cloudFormation = new CloudFormationDeployments({ sdkProvider });

await cloudFormation.destroyStack({
stack: stackArtifact,
});
}
}, 200000);
});
4 changes: 2 additions & 2 deletions packages/metrics/tests/unit/Metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ describe('Class: Metrics', () => {
const handler = async (_event: DummyEvent, _context: Context): Promise<void> => {
metrics.raiseOnEmptyMetrics();
// Logic goes here
metrics.purgeStoredMetrics();
metrics.publishStoredMetrics();
};

try {
Expand Down Expand Up @@ -474,7 +474,7 @@ describe('Class: Metrics', () => {
_callback: Callback<TResult>,
): void | Promise<TResult> {
metrics.addMetric('test_name_1', MetricUnits.Count, 1);
metrics.purgeStoredMetrics();
metrics.publishStoredMetrics();
}
}

Expand Down