Skip to content

Commit 0c9c4b4

Browse files
authored
feat(cloudwatch): Stats factory class for metric strings (#23172)
The `Statistic` enum type was incorrectly publicly exposed (it should only have been visible internally to the package), and was enhanced in PR #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*
1 parent f6b353f commit 0c9c4b4

File tree

8 files changed

+291
-386
lines changed

8 files changed

+291
-386
lines changed

packages/@aws-cdk/aws-certificatemanager/lib/certificate-base.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
2-
import { Statistic } from '@aws-cdk/aws-cloudwatch';
2+
import { Stats } from '@aws-cdk/aws-cloudwatch';
33
import { Duration, Resource } from '@aws-cdk/core';
44
import { ICertificate } from './certificate';
55

@@ -26,7 +26,7 @@ export abstract class CertificateBase extends Resource implements ICertificate {
2626
metricName: 'DaysToExpiry',
2727
namespace: 'AWS/CertificateManager',
2828
region: this.region,
29-
statistic: Statistic.MINIMUM,
29+
statistic: Stats.MINIMUM,
3030
});
3131
}
3232
}

packages/@aws-cdk/aws-cloudwatch/README.md

+10-7
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,17 @@ to the metric function call:
136136
declare const fn: lambda.Function;
137137

138138
const minuteErrorRate = fn.metricErrors({
139-
statistic: 'avg',
139+
statistic: cloudwatch.Stats.AVERAGE,
140140
period: Duration.minutes(1),
141141
label: 'Lambda failure rate'
142142
});
143143
```
144144

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

148151
> Rates versus Sums
149152
>
@@ -175,7 +178,7 @@ in the legend. For example, if you use:
175178
declare const fn: lambda.Function;
176179

177180
const minuteErrorRate = fn.metricErrors({
178-
statistic: 'sum',
181+
statistic: cloudwatch.Stats.SUM,
179182
period: Duration.hours(1),
180183

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

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

612615
### Column widget
613616

614-
A column widget contains other widgets and they will be laid out in a
617+
A column widget contains other widgets and they will be laid out in a
615618
vertical column. Widgets will be put one after another in order.
616619

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

627630
### Row widget
628631

629-
A row widget contains other widgets and they will be laid out in a
632+
A row widget contains other widgets and they will be laid out in a
630633
horizontal row. Widgets will be put one after another in order.
631634
If the total width of the row exceeds the max width of the grid of 24
632635
columns, the row will wrap automatically and adapt its height.

packages/@aws-cdk/aws-cloudwatch/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './log-query';
1212
export * from './text';
1313
export * from './widget';
1414
export * from './alarm-status-widget';
15+
export * from './stats';
1516

1617
// AWS::CloudWatch CloudFormation Resources:
1718
export * from './cloudwatch.generated';

packages/@aws-cdk/aws-cloudwatch/lib/metric-types.ts

+1-375
Large diffs are not rendered by default.

packages/@aws-cdk/aws-cloudwatch/lib/metric.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export interface CommonMetricOptions {
3838
* - "tcNN.NN" | "tc(NN.NN%:NN.NN%)"
3939
* - "tsNN.NN" | "ts(NN.NN%:NN.NN%)"
4040
*
41+
* Use the factory functions on the `Stats` object to construct valid input strings.
42+
*
4143
* @default Average
4244
*/
4345
readonly statistic?: string;

packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Statistic } from '../metric-types';
2-
31
export interface SimpleStatistic {
42
type: 'simple';
53
statistic: Statistic;
@@ -66,4 +64,39 @@ export function normalizeStatistic(stat: string): string {
6664
// floating point rounding issues, return as-is but lowercase the p.
6765
return stat.toLowerCase();
6866
}
67+
}
68+
69+
/**
70+
* Enum for simple statistics
71+
*
72+
* (This is a private copy of the type in `metric-types.ts`; this type should always
73+
* been private, the public one has been deprecated and isn't used anywhere).
74+
*
75+
* @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html
76+
*/
77+
export enum Statistic {
78+
/**
79+
* The count (number) of data points used for the statistical calculation.
80+
*/
81+
SAMPLE_COUNT = 'SampleCount',
82+
83+
/**
84+
* The value of Sum / SampleCount during the specified period.
85+
*/
86+
AVERAGE = 'Average',
87+
/**
88+
* All values submitted for the matching metric added together.
89+
* This statistic can be useful for determining the total volume of a metric.
90+
*/
91+
SUM = 'Sum',
92+
/**
93+
* The lowest value observed during the specified period.
94+
* You can use this value to determine low volumes of activity for your application.
95+
*/
96+
MINIMUM = 'Minimum',
97+
/**
98+
* The highest value observed during the specified period.
99+
* You can use this value to determine high volumes of activity for your application.
100+
*/
101+
MAXIMUM = 'Maximum',
69102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
2+
/**
3+
* Factory functions for standard statistics strings
4+
*/
5+
export abstract class Stats {
6+
/**
7+
* The count (number) of data points used for the statistical calculation.
8+
*/
9+
public static readonly SAMPLE_COUNT = 'SampleCount';
10+
11+
/**
12+
* The value of Sum / SampleCount during the specified period.
13+
*/
14+
public static readonly AVERAGE = 'Average';
15+
/**
16+
* All values submitted for the matching metric added together.
17+
* This statistic can be useful for determining the total volume of a metric.
18+
*/
19+
public static readonly SUM = 'Sum';
20+
21+
/**
22+
* The lowest value observed during the specified period.
23+
* You can use this value to determine low volumes of activity for your application.
24+
*/
25+
public static readonly MINIMUM = 'Minimum';
26+
27+
/**
28+
* The highest value observed during the specified period.
29+
* You can use this value to determine high volumes of activity for your application.
30+
*/
31+
public static readonly MAXIMUM = 'Maximum';
32+
33+
/**
34+
* Interquartile mean (IQM) is the trimmed mean of the interquartile range, or the middle 50% of values.
35+
*
36+
* It is equivalent to `trimmedMean(25, 75)`.
37+
*/
38+
public static readonly IQM = 'IQM';
39+
40+
/**
41+
* Percentile indicates the relative standing of a value in a dataset.
42+
*
43+
* Percentiles help you get a better understanding of the distribution of your metric data.
44+
*
45+
* For example, `p(90)` is the 90th percentile and means that 90% of the data
46+
* within the period is lower than this value and 10% of the data is higher
47+
* than this value.
48+
*/
49+
public static percentile(percentile: number) {
50+
assertPercentage(percentile);
51+
return `p${percentile}`;
52+
}
53+
54+
/**
55+
* A shorter alias for `percentile()`.
56+
*/
57+
public static p(percentile: number) {
58+
return Stats.percentile(percentile);
59+
}
60+
61+
/**
62+
* Trimmed mean (TM) is the mean of all values that are between two specified boundaries.
63+
*
64+
* Values outside of the boundaries are ignored when the mean is calculated.
65+
* You define the boundaries as one or two numbers between 0 and 100, up to 10
66+
* decimal places. The numbers are percentages.
67+
*
68+
* - If two numbers are given, they define the lower and upper bounds in percentages,
69+
* respectively.
70+
* - If one number is given, it defines the upper bound (the lower bound is assumed to
71+
* be 0).
72+
*
73+
* For example, `tm(90)` calculates the average after removing the 10% of data
74+
* points with the highest values; `tm(10, 90)` calculates the average after removing the
75+
* 10% with the lowest and 10% with the highest values.
76+
*/
77+
public static trimmedMean(p1: number, p2?: number) {
78+
return boundaryPercentileStat('tm', 'TM', p1, p2);
79+
}
80+
81+
/**
82+
* A shorter alias for `trimmedMean()`.
83+
*/
84+
public static tm(p1: number, p2?: number) {
85+
return Stats.trimmedMean(p1, p2);
86+
}
87+
88+
/**
89+
* Winsorized mean (WM) is similar to trimmed mean.
90+
*
91+
* However, with winsorized mean, the values that are outside the boundary are
92+
* not ignored, but instead are considered to be equal to the value at the
93+
* edge of the appropriate boundary. After this normalization, the average is
94+
* calculated. You define the boundaries as one or two numbers between 0 and
95+
* 100, up to 10 decimal places.
96+
*
97+
* - If two numbers are given, they define the lower and upper bounds in percentages,
98+
* respectively.
99+
* - If one number is given, it defines the upper bound (the lower bound is assumed to
100+
* be 0).
101+
*
102+
* For example, `tm(90)` calculates the average after removing the 10% of data
103+
* points with the highest values; `tm(10, 90)` calculates the average after removing the
104+
* 10% with the lowest and 10% with the highest values.
105+
*
106+
* For example, `wm(90)` calculates the average while treating the 10% of the
107+
* highest values to be equal to the value at the 90th percentile.
108+
* `wm(10, 90)` calculates the average while treaing the bottom 10% and the
109+
* top 10% of values to be equal to the boundary values.
110+
*/
111+
public static winsorizedMean(p1: number, p2?: number) {
112+
return boundaryPercentileStat('wm', 'WM', p1, p2);
113+
}
114+
115+
/**
116+
* A shorter alias for `winsorizedMean()`.
117+
*/
118+
public static wm(p1: number, p2?: number) {
119+
return Stats.winsorizedMean(p1, p2);
120+
}
121+
122+
/**
123+
* Trimmed count (TC) is the number of data points in the chosen range for a trimmed mean statistic.
124+
*
125+
* - If two numbers are given, they define the lower and upper bounds in percentages,
126+
* respectively.
127+
* - If one number is given, it defines the upper bound (the lower bound is assumed to
128+
* be 0).
129+
*
130+
* For example, `tc(90)` returns the number of data points not including any
131+
* data points that fall in the highest 10% of the values. `tc(10, 90)`
132+
* returns the number of data points not including any data points that fall
133+
* in the lowest 10% of the values and the highest 90% of the values.
134+
*/
135+
public static trimmedCount(p1: number, p2?: number) {
136+
return boundaryPercentileStat('tc', 'TC', p1, p2);
137+
}
138+
139+
/**
140+
* Shorter alias for `trimmedCount()`.
141+
*/
142+
public static tc(p1: number, p2?: number) {
143+
return Stats.trimmedCount(p1, p2);
144+
}
145+
146+
/**
147+
* Trimmed sum (TS) is the sum of the values of data points in a chosen range for a trimmed mean statistic.
148+
* It is equivalent to `(Trimmed Mean) * (Trimmed count)`.
149+
*
150+
* - If two numbers are given, they define the lower and upper bounds in percentages,
151+
* respectively.
152+
* - If one number is given, it defines the upper bound (the lower bound is assumed to
153+
* be 0).
154+
*
155+
* For example, `ts(90)` returns the sum of the data points not including any
156+
* data points that fall in the highest 10% of the values. `ts(10, 90)`
157+
* returns the sum of the data points not including any data points that fall
158+
* in the lowest 10% of the values and the highest 90% of the values.
159+
*/
160+
public static trimmedSum(p1: number, p2?: number) {
161+
return boundaryPercentileStat('ts', 'TS', p1, p2);
162+
}
163+
164+
/**
165+
* Shorter alias for `trimmedSum()`.
166+
*/
167+
public static ts(p1: number, p2?: number) {
168+
return Stats.trimmedSum(p1, p2);
169+
}
170+
171+
/**
172+
* Percentile rank (PR) is the percentage of values that meet a fixed threshold.
173+
*
174+
* - If two numbers are given, they define the lower and upper bounds in absolute values,
175+
* respectively.
176+
* - If one number is given, it defines the upper bound (the lower bound is assumed to
177+
* be 0).
178+
*
179+
* For example, `percentileRank(300)` returns the percentage of data points that have a value of 300 or less.
180+
* `percentileRank(100, 2000)` returns the percentage of data points that have a value between 100 and 2000.
181+
*/
182+
public static percentileRank(v1: number, v2?: number) {
183+
if (v2 !== undefined) {
184+
return `PR(${v1}:${v2})`;
185+
} else {
186+
return `PR(:${v1})`;
187+
}
188+
}
189+
190+
/**
191+
* Shorter alias for `percentileRank()`.
192+
*/
193+
public static pr(v1: number, v2?: number) {
194+
return this.percentileRank(v1, v2);
195+
}
196+
}
197+
198+
function assertPercentage(x?: number) {
199+
if (x !== undefined && (x < 0 || x > 100)) {
200+
throw new Error(`Expecting a percentage, got: ${x}`);
201+
}
202+
}
203+
204+
/**
205+
* Formatting helper because all these stats look the same
206+
*/
207+
function boundaryPercentileStat(oneBoundaryStat: string, twoBoundaryStat: string, p1: number, p2: number | undefined) {
208+
assertPercentage(p1);
209+
assertPercentage(p2);
210+
if (p2 !== undefined) {
211+
return `${twoBoundaryStat}(${p1}%:${p2}%)`;
212+
} else {
213+
return `${oneBoundaryStat}${p1}`;
214+
}
215+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as cloudwatch from '../lib';
2+
3+
test('spot check some constants', () => {
4+
expect(cloudwatch.Stats.AVERAGE).toEqual('Average');
5+
expect(cloudwatch.Stats.IQM).toEqual('IQM');
6+
expect(cloudwatch.Stats.SAMPLE_COUNT).toEqual('SampleCount');
7+
});
8+
9+
10+
test('spot check percentiles', () => {
11+
expect(cloudwatch.Stats.p(99)).toEqual('p99');
12+
expect(cloudwatch.Stats.p(99.9)).toEqual('p99.9');
13+
expect(cloudwatch.Stats.p(99.99)).toEqual('p99.99');
14+
});
15+
16+
test('spot check some trimmed means', () => {
17+
expect(cloudwatch.Stats.tm(99)).toEqual('tm99');
18+
expect(cloudwatch.Stats.tm(99.9)).toEqual('tm99.9');
19+
expect(cloudwatch.Stats.tm(0.01, 99.99)).toEqual('TM(0.01%:99.99%)');
20+
});
21+
22+
test('percentile rank', () => {
23+
expect(cloudwatch.Stats.pr(300)).toEqual('PR(:300)');
24+
expect(cloudwatch.Stats.pr(100, 500)).toEqual('PR(100:500)');
25+
});

0 commit comments

Comments
 (0)