Skip to content

Commit

Permalink
feat(cloudwatch): Stats factory class for metric strings (aws#23172)
Browse files Browse the repository at this point in the history
The `Statistic` enum type was incorrectly publicly exposed (it should only have been visible internally to the package), and was enhanced in PR aws#23074 to have more enum values such as `P10`, `P50`, `P99_9`, etc.

The stringification of this `Statistic` type would only have worked in TypeScript anyway (in JSII languages like Java and Python we cannot rely on the string values of enums), and the fact that enums cannot be parameterized made it so that we used to have a lot of redundant enum values.

Deprecate the `Statistic` type, and introduce a new factory class, `Stats`, whose sole purpose is to produce formatted strings to use as CloudWatch `statistic` values, and advertise the use of this class.

(We probably shouldn't have been using `string` as the type in the first place, but given that we are factories to produce them seems to be the next best thing).


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
rix0rrr authored and Brennan Ho committed Dec 9, 2022
1 parent f6dbc97 commit b16aae3
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 386 deletions.
@@ -1,5 +1,5 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import { Statistic } from '@aws-cdk/aws-cloudwatch';
import { Stats } from '@aws-cdk/aws-cloudwatch';
import { Duration, Resource } from '@aws-cdk/core';
import { ICertificate } from './certificate';

Expand All @@ -26,7 +26,7 @@ export abstract class CertificateBase extends Resource implements ICertificate {
metricName: 'DaysToExpiry',
namespace: 'AWS/CertificateManager',
region: this.region,
statistic: Statistic.MINIMUM,
statistic: Stats.MINIMUM,
});
}
}
17 changes: 10 additions & 7 deletions packages/@aws-cdk/aws-cloudwatch/README.md
Expand Up @@ -136,14 +136,17 @@ to the metric function call:
declare const fn: lambda.Function;

const minuteErrorRate = fn.metricErrors({
statistic: 'avg',
statistic: cloudwatch.Stats.AVERAGE,
period: Duration.minutes(1),
label: 'Lambda failure rate'
});
```

This function also allows changing the metric label or color (which will be
useful when embedding them in graphs, see below).
The `statistic` field accepts a `string`; the `cloudwatch.Stats` object has a
number of predefined factory functions that help you constructs strings that are
appropriate for CloudWatch. The `metricErrors` function also allows changing the
metric label or color, which will be useful when embedding them in graphs (see
below).

> Rates versus Sums
>
Expand Down Expand Up @@ -175,7 +178,7 @@ in the legend. For example, if you use:
declare const fn: lambda.Function;

const minuteErrorRate = fn.metricErrors({
statistic: 'sum',
statistic: cloudwatch.Stats.SUM,
period: Duration.hours(1),

// Show the maximum hourly error count in the legend
Expand Down Expand Up @@ -363,7 +366,7 @@ dashboard.addWidgets(new cloudwatch.GraphWidget({
left: [executionCountMetric],

right: [errorCountMetric.with({
statistic: "average",
statistic: cloudwatch.Stats.AVERAGE,
label: "Error rate",
color: cloudwatch.Color.GREEN,
})]
Expand Down Expand Up @@ -611,7 +614,7 @@ you can use the following widgets to pack widgets together in different ways:

### Column widget

A column widget contains other widgets and they will be laid out in a
A column widget contains other widgets and they will be laid out in a
vertical column. Widgets will be put one after another in order.

```ts
Expand All @@ -626,7 +629,7 @@ You can add a widget after object instantiation with the method

### Row widget

A row widget contains other widgets and they will be laid out in a
A row widget contains other widgets and they will be laid out in a
horizontal row. Widgets will be put one after another in order.
If the total width of the row exceeds the max width of the grid of 24
columns, the row will wrap automatically and adapt its height.
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudwatch/lib/index.ts
Expand Up @@ -12,6 +12,7 @@ export * from './log-query';
export * from './text';
export * from './widget';
export * from './alarm-status-widget';
export * from './stats';

// AWS::CloudWatch CloudFormation Resources:
export * from './cloudwatch.generated';
376 changes: 1 addition & 375 deletions packages/@aws-cdk/aws-cloudwatch/lib/metric-types.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/lib/metric.ts
Expand Up @@ -38,6 +38,8 @@ export interface CommonMetricOptions {
* - "tcNN.NN" | "tc(NN.NN%:NN.NN%)"
* - "tsNN.NN" | "ts(NN.NN%:NN.NN%)"
*
* Use the factory functions on the `Stats` object to construct valid input strings.
*
* @default Average
*/
readonly statistic?: string;
Expand Down
37 changes: 35 additions & 2 deletions packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts
@@ -1,5 +1,3 @@
import { Statistic } from '../metric-types';

export interface SimpleStatistic {
type: 'simple';
statistic: Statistic;
Expand Down Expand Up @@ -66,4 +64,39 @@ export function normalizeStatistic(stat: string): string {
// floating point rounding issues, return as-is but lowercase the p.
return stat.toLowerCase();
}
}

/**
* Enum for simple statistics
*
* (This is a private copy of the type in `metric-types.ts`; this type should always
* been private, the public one has been deprecated and isn't used anywhere).
*
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html
*/
export enum Statistic {
/**
* The count (number) of data points used for the statistical calculation.
*/
SAMPLE_COUNT = 'SampleCount',

/**
* The value of Sum / SampleCount during the specified period.
*/
AVERAGE = 'Average',
/**
* All values submitted for the matching metric added together.
* This statistic can be useful for determining the total volume of a metric.
*/
SUM = 'Sum',
/**
* The lowest value observed during the specified period.
* You can use this value to determine low volumes of activity for your application.
*/
MINIMUM = 'Minimum',
/**
* The highest value observed during the specified period.
* You can use this value to determine high volumes of activity for your application.
*/
MAXIMUM = 'Maximum',
}
215 changes: 215 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/lib/stats.ts
@@ -0,0 +1,215 @@

/**
* Factory functions for standard statistics strings
*/
export abstract class Stats {
/**
* The count (number) of data points used for the statistical calculation.
*/
public static readonly SAMPLE_COUNT = 'SampleCount';

/**
* The value of Sum / SampleCount during the specified period.
*/
public static readonly AVERAGE = 'Average';
/**
* All values submitted for the matching metric added together.
* This statistic can be useful for determining the total volume of a metric.
*/
public static readonly SUM = 'Sum';

/**
* The lowest value observed during the specified period.
* You can use this value to determine low volumes of activity for your application.
*/
public static readonly MINIMUM = 'Minimum';

/**
* The highest value observed during the specified period.
* You can use this value to determine high volumes of activity for your application.
*/
public static readonly MAXIMUM = 'Maximum';

/**
* Interquartile mean (IQM) is the trimmed mean of the interquartile range, or the middle 50% of values.
*
* It is equivalent to `trimmedMean(25, 75)`.
*/
public static readonly IQM = 'IQM';

/**
* Percentile indicates the relative standing of a value in a dataset.
*
* Percentiles help you get a better understanding of the distribution of your metric data.
*
* For example, `p(90)` is the 90th percentile and means that 90% of the data
* within the period is lower than this value and 10% of the data is higher
* than this value.
*/
public static percentile(percentile: number) {
assertPercentage(percentile);
return `p${percentile}`;
}

/**
* A shorter alias for `percentile()`.
*/
public static p(percentile: number) {
return Stats.percentile(percentile);
}

/**
* Trimmed mean (TM) is the mean of all values that are between two specified boundaries.
*
* Values outside of the boundaries are ignored when the mean is calculated.
* You define the boundaries as one or two numbers between 0 and 100, up to 10
* decimal places. The numbers are percentages.
*
* - If two numbers are given, they define the lower and upper bounds in percentages,
* respectively.
* - If one number is given, it defines the upper bound (the lower bound is assumed to
* be 0).
*
* For example, `tm(90)` calculates the average after removing the 10% of data
* points with the highest values; `tm(10, 90)` calculates the average after removing the
* 10% with the lowest and 10% with the highest values.
*/
public static trimmedMean(p1: number, p2?: number) {
return boundaryPercentileStat('tm', 'TM', p1, p2);
}

/**
* A shorter alias for `trimmedMean()`.
*/
public static tm(p1: number, p2?: number) {
return Stats.trimmedMean(p1, p2);
}

/**
* Winsorized mean (WM) is similar to trimmed mean.
*
* However, with winsorized mean, the values that are outside the boundary are
* not ignored, but instead are considered to be equal to the value at the
* edge of the appropriate boundary. After this normalization, the average is
* calculated. You define the boundaries as one or two numbers between 0 and
* 100, up to 10 decimal places.
*
* - If two numbers are given, they define the lower and upper bounds in percentages,
* respectively.
* - If one number is given, it defines the upper bound (the lower bound is assumed to
* be 0).
*
* For example, `tm(90)` calculates the average after removing the 10% of data
* points with the highest values; `tm(10, 90)` calculates the average after removing the
* 10% with the lowest and 10% with the highest values.
*
* For example, `wm(90)` calculates the average while treating the 10% of the
* highest values to be equal to the value at the 90th percentile.
* `wm(10, 90)` calculates the average while treaing the bottom 10% and the
* top 10% of values to be equal to the boundary values.
*/
public static winsorizedMean(p1: number, p2?: number) {
return boundaryPercentileStat('wm', 'WM', p1, p2);
}

/**
* A shorter alias for `winsorizedMean()`.
*/
public static wm(p1: number, p2?: number) {
return Stats.winsorizedMean(p1, p2);
}

/**
* Trimmed count (TC) is the number of data points in the chosen range for a trimmed mean statistic.
*
* - If two numbers are given, they define the lower and upper bounds in percentages,
* respectively.
* - If one number is given, it defines the upper bound (the lower bound is assumed to
* be 0).
*
* For example, `tc(90)` returns the number of data points not including any
* data points that fall in the highest 10% of the values. `tc(10, 90)`
* returns the number of data points not including any data points that fall
* in the lowest 10% of the values and the highest 90% of the values.
*/
public static trimmedCount(p1: number, p2?: number) {
return boundaryPercentileStat('tc', 'TC', p1, p2);
}

/**
* Shorter alias for `trimmedCount()`.
*/
public static tc(p1: number, p2?: number) {
return Stats.trimmedCount(p1, p2);
}

/**
* Trimmed sum (TS) is the sum of the values of data points in a chosen range for a trimmed mean statistic.
* It is equivalent to `(Trimmed Mean) * (Trimmed count)`.
*
* - If two numbers are given, they define the lower and upper bounds in percentages,
* respectively.
* - If one number is given, it defines the upper bound (the lower bound is assumed to
* be 0).
*
* For example, `ts(90)` returns the sum of the data points not including any
* data points that fall in the highest 10% of the values. `ts(10, 90)`
* returns the sum of the data points not including any data points that fall
* in the lowest 10% of the values and the highest 90% of the values.
*/
public static trimmedSum(p1: number, p2?: number) {
return boundaryPercentileStat('ts', 'TS', p1, p2);
}

/**
* Shorter alias for `trimmedSum()`.
*/
public static ts(p1: number, p2?: number) {
return Stats.trimmedSum(p1, p2);
}

/**
* Percentile rank (PR) is the percentage of values that meet a fixed threshold.
*
* - If two numbers are given, they define the lower and upper bounds in absolute values,
* respectively.
* - If one number is given, it defines the upper bound (the lower bound is assumed to
* be 0).
*
* For example, `percentileRank(300)` returns the percentage of data points that have a value of 300 or less.
* `percentileRank(100, 2000)` returns the percentage of data points that have a value between 100 and 2000.
*/
public static percentileRank(v1: number, v2?: number) {
if (v2 !== undefined) {
return `PR(${v1}:${v2})`;
} else {
return `PR(:${v1})`;
}
}

/**
* Shorter alias for `percentileRank()`.
*/
public static pr(v1: number, v2?: number) {
return this.percentileRank(v1, v2);
}
}

function assertPercentage(x?: number) {
if (x !== undefined && (x < 0 || x > 100)) {
throw new Error(`Expecting a percentage, got: ${x}`);
}
}

/**
* Formatting helper because all these stats look the same
*/
function boundaryPercentileStat(oneBoundaryStat: string, twoBoundaryStat: string, p1: number, p2: number | undefined) {
assertPercentage(p1);
assertPercentage(p2);
if (p2 !== undefined) {
return `${twoBoundaryStat}(${p1}%:${p2}%)`;
} else {
return `${oneBoundaryStat}${p1}`;
}
}
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-cloudwatch/test/stats.test.ts
@@ -0,0 +1,25 @@
import * as cloudwatch from '../lib';

test('spot check some constants', () => {
expect(cloudwatch.Stats.AVERAGE).toEqual('Average');
expect(cloudwatch.Stats.IQM).toEqual('IQM');
expect(cloudwatch.Stats.SAMPLE_COUNT).toEqual('SampleCount');
});


test('spot check percentiles', () => {
expect(cloudwatch.Stats.p(99)).toEqual('p99');
expect(cloudwatch.Stats.p(99.9)).toEqual('p99.9');
expect(cloudwatch.Stats.p(99.99)).toEqual('p99.99');
});

test('spot check some trimmed means', () => {
expect(cloudwatch.Stats.tm(99)).toEqual('tm99');
expect(cloudwatch.Stats.tm(99.9)).toEqual('tm99.9');
expect(cloudwatch.Stats.tm(0.01, 99.99)).toEqual('TM(0.01%:99.99%)');
});

test('percentile rank', () => {
expect(cloudwatch.Stats.pr(300)).toEqual('PR(:300)');
expect(cloudwatch.Stats.pr(100, 500)).toEqual('PR(100:500)');
});

0 comments on commit b16aae3

Please sign in to comment.