From 87f2e17eecf8a5f2e1ab48041d13b154ad5b904e Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Mon, 17 Dec 2018 16:08:36 -0800 Subject: [PATCH 01/10] Add metric math dsl sketch --- packages/@aws-cdk/aws-cloudwatch/lib/math.ts | 187 ++++++++++++++++++ .../@aws-cdk/aws-cloudwatch/test/test.math.ts | 53 +++++ 2 files changed, 240 insertions(+) create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/math.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/test/test.math.ts diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts new file mode 100644 index 0000000000000..810b2fe9e43ec --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts @@ -0,0 +1,187 @@ +import { Metric } from "./metric"; +// import { Alarm, AlarmProps } from "./alarm"; + +// https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html + +/* +S represents a scalar number, such as 2, -5, or 50.25. + +TS is a time series (a series of values for a single CloudWatch metric over time). +For example, the CPUUtilization metric for instance i-1234567890abcdef0 over the last three days. + +TS[] is an array of time series, such as the time series for multiple metrics. +*/ + +export class ExpressionContext { + private readonly metrics: { [key: string]: Metric } = {}; + private counter = 1; + + public metric(metric: Metric): string { + const id = 'm' + this.counter.toString(); + this.counter += 1; + this.metrics[id] = metric; + return id; + } + + // public toAlarmProps(): AlarmProps { + // return { + // metric + // } + // } +} + +export abstract class Expression { + public abstract render(context: ExpressionContext): string; + + public plus(expression: Expression | number): Plus { + return new Plus(this, expression); + } + public minus(expression: Expression | number): Minus { + return new Minus(this, expression); + } + public multiply(expression: Expression | number): Multiply { + return new Multiply(this, expression); + } + public divide(expression: Expression | number): Divide { + return new Divide(this, expression); + } + public pow(expression: Expression | number): Exponent { + return new Exponent(this, expression); + } +} + +export class Scalar extends Expression { + constructor(private readonly value: number) { + super(); + } + public render(_context: ExpressionContext): string { + return this.value.toString(); + } +} +export class Literal extends Expression { + constructor(private readonly value: string) { + super(); + } + public render(_context: ExpressionContext): string { + return `"${this.value}"`; + } +} + +export class TimeSeries extends Expression { + constructor(private readonly id: string) { + super(); + } + + public render(_context: ExpressionContext): string { + return this.id; + } +} + +export class TimeSeriesArray extends Expression { + constructor(private readonly array: Expression[]) { + super(); + } + + public render(context: ExpressionContext): string { + return `[${this.array.map(a => a.render(context)).join(',')}]`; + } +} + +export abstract class Operator extends Expression { + protected abstract readonly operator: string; + constructor(private readonly lhs: Expression | number, private readonly rhs: Expression | number) { + super(); + } + + public render(context: ExpressionContext): string { + const lhs = typeof this.lhs === 'number' ? new Scalar(this.lhs) : this.lhs; + const rhs = typeof this.rhs === 'number' ? new Scalar(this.rhs) : this.rhs; + return `${lhs.render(context)} ${this.operator} ${rhs.render(context)}`; + } +} + +export class Plus extends Operator { + protected readonly operator: string = '+'; +} +export class Minus extends Operator { + protected readonly operator: string = '+'; +} +export class Multiply extends Operator { + protected readonly operator: string = '*'; +} +export class Divide extends Operator { + protected readonly operator: string = '/'; +} +export class Exponent extends Operator { + protected readonly operator: string = '^'; +} + +export abstract class Function extends Expression { + protected abstract readonly name: string; + + constructor(private readonly expressions: Expression[]) { + super(); + } + + public render(context: ExpressionContext): string { + return `${this.name}(${this.expressions.map(ex => ex.render(context)).join(',')})`; + } +} +export abstract class Function1 extends Function { + constructor(...expression: Expression[]) { + if (expression.length > 1) { + super([new TimeSeriesArray(expression)]); + } else { + super(expression); + } + } +} +export abstract class Function2 extends Function { + constructor(expression1: Expression, expression2: Expression) { + super([expression1, expression2]); + } +} + +export class Abs extends Function1 { + protected readonly name: string = 'ABS'; +} +export class Average extends Function1 { + protected readonly name: string = 'AVG'; +} +export class Ceil extends Function1 { + protected readonly name: string = 'CEIL'; +} +export class Fill extends Function2 { + protected readonly name: string = 'CEIL'; +} +export class Floor extends Function1 { + protected readonly name: string = 'FLOOR'; +} +export class Max extends Function1 { + protected readonly name: string = 'MAX'; +} +export class MetricCount extends Function1 { + protected readonly name: string = 'METRIC_COUNT'; +} +export class Metrics extends Function { + protected readonly name: string = 'METRICS'; + + constructor(filter?: string) { + super(filter !== undefined ? [new Literal(filter)] : []); + } +} +export class Min extends Function1 { + protected readonly name: string = 'MIN'; +} +export class Period extends Function1 { + protected readonly name: string = 'PERIOD'; +} +export class Rate extends Function1 { + protected readonly name: string = 'RATE'; +} +export class StdDev extends Function1 { + protected readonly name: string = 'STDDEV'; +} +export class Sum extends Function1 { + protected readonly name: string = 'SUM'; +} diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts new file mode 100644 index 0000000000000..c1bd4eede533b --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts @@ -0,0 +1,53 @@ +import { Test } from 'nodeunit'; +import cloudwatch = require('../lib'); + +export = { + 'm1 * 100'(test: Test) { + const expression = new cloudwatch.TimeSeries('m1').multiply(100).render(new cloudwatch.ExpressionContext()); + + test.equals(expression, 'm1 * 100'); + test.done(); + }, + + 'SUM(m1)'(test: Test) { + const m1 = new cloudwatch.Metric({ + metricName: 'NumberOfPublishedMessages', + namespace: 'AWS/SNS', + }); + const expression = new cloudwatch.Sum(m1).render(new cloudwatch.ExpressionContext()); + + test.equals(expression, 'SUM(m1)'); + test.done(); + }, + + 'SUM([m1,m2])'(test: Test) { + const m1 = new cloudwatch.Metric({ + metricName: 'NumberOfPublishedMessages', + namespace: 'AWS/SNS', + }); + const m2 = new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'Custom', + }); + const expression = new cloudwatch.Sum(m1, m2).render(new cloudwatch.ExpressionContext()); + + test.equals(expression, 'SUM([m1,m2])'); + test.done(); + }, + + 'SUM([m1,m2]) * 100'(test: Test) { + const m1 = new cloudwatch.Metric({ + metricName: 'NumberOfPublishedMessages', + namespace: 'AWS/SNS', + }); + const m2 = new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'Custom', + }); + const expression = new cloudwatch.Sum(m1, m2).multiply(100) + .render(new cloudwatch.ExpressionContext()); + + test.equals(expression, 'SUM([m1,m2]) * 100'); + test.done(); + } +}; \ No newline at end of file From 2dcf861c507c76d074f80de0b92b56474d9b5f88 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Mon, 17 Dec 2018 16:12:28 -0800 Subject: [PATCH 02/10] Add metric and index changes --- packages/@aws-cdk/aws-cloudwatch/lib/index.ts | 1 + packages/@aws-cdk/aws-cloudwatch/lib/metric.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts index 5940a823ca866..897e0c2437df3 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts @@ -2,6 +2,7 @@ export * from './alarm'; export * from './dashboard'; export * from './graph'; export * from './layout'; +export * from './math'; export * from './metric'; export * from './text'; export * from './widget'; diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts b/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts index 6d09191c0a854..b013bb402b481 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts @@ -1,6 +1,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Alarm, ComparisonOperator, TreatMissingData } from './alarm'; +import { Expression, ExpressionContext } from './math'; import { normalizeStatistic } from './util.statistic'; export type DimensionHash = {[dim: string]: any}; @@ -81,7 +82,7 @@ export interface MetricProps { * Metric is an abstraction that makes it easy to specify metrics for use in both * alarms and graphs. */ -export class Metric { +export class Metric extends Expression { /** * Grant permissions to the given identity to write metrics. * @@ -105,6 +106,8 @@ export class Metric { public readonly color?: string; constructor(props: MetricProps) { + super(); + if (props.periodSec !== undefined && props.periodSec !== 1 && props.periodSec !== 5 && props.periodSec !== 10 && props.periodSec !== 30 && props.periodSec % 60 !== 0) { @@ -142,6 +145,10 @@ export class Metric { }); } + public render(context: ExpressionContext): string { + return context.metric(this); + } + /** * Make a new Alarm for this metric * From 7cfb9f128f52c8afb134388cfbc6e19973685291 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Mon, 17 Dec 2018 16:36:38 -0800 Subject: [PATCH 03/10] Render cfn properties and expose a demonstrative unit test --- packages/@aws-cdk/aws-cloudwatch/lib/math.ts | 32 +++++++-- .../@aws-cdk/aws-cloudwatch/test/test.math.ts | 70 +++++++++---------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts index 810b2fe9e43ec..4a8c93c5148be 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts @@ -23,14 +23,36 @@ export class ExpressionContext { return id; } - // public toAlarmProps(): AlarmProps { - // return { - // metric - // } - // } + public toMetrics(): any { + const metrics: any[] = []; + Object.keys(this.metrics).forEach(id => { + const metric = this.metrics[id]; + metrics.push({ + id, + metric: { + metricName: metric.metricName, + namespace: metric.namespace, + dimensions: metric.dimensions + }, + period: metric.periodSec, + stat: metric.statistic, + unit: metric.unit + }); + }); + return metrics; + } } export abstract class Expression { + public compile(): any { + const context = new ExpressionContext(); + const expression = this.render(context); + return { + expression, + metrics: context.toMetrics() + }; + } + public abstract render(context: ExpressionContext): string; public plus(expression: Expression | number): Plus { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts index c1bd4eede533b..7166e2cf96510 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts @@ -2,39 +2,6 @@ import { Test } from 'nodeunit'; import cloudwatch = require('../lib'); export = { - 'm1 * 100'(test: Test) { - const expression = new cloudwatch.TimeSeries('m1').multiply(100).render(new cloudwatch.ExpressionContext()); - - test.equals(expression, 'm1 * 100'); - test.done(); - }, - - 'SUM(m1)'(test: Test) { - const m1 = new cloudwatch.Metric({ - metricName: 'NumberOfPublishedMessages', - namespace: 'AWS/SNS', - }); - const expression = new cloudwatch.Sum(m1).render(new cloudwatch.ExpressionContext()); - - test.equals(expression, 'SUM(m1)'); - test.done(); - }, - - 'SUM([m1,m2])'(test: Test) { - const m1 = new cloudwatch.Metric({ - metricName: 'NumberOfPublishedMessages', - namespace: 'AWS/SNS', - }); - const m2 = new cloudwatch.Metric({ - metricName: 'Errors', - namespace: 'Custom', - }); - const expression = new cloudwatch.Sum(m1, m2).render(new cloudwatch.ExpressionContext()); - - test.equals(expression, 'SUM([m1,m2])'); - test.done(); - }, - 'SUM([m1,m2]) * 100'(test: Test) { const m1 = new cloudwatch.Metric({ metricName: 'NumberOfPublishedMessages', @@ -43,11 +10,40 @@ export = { const m2 = new cloudwatch.Metric({ metricName: 'Errors', namespace: 'Custom', + dimensions: { + a: '1', + b: '2' + } + }); + const math = new cloudwatch.Sum(m1, m2).multiply(100); + + test.deepEqual(math.compile(), { + expression: 'SUM([m1,m2]) * 100', + metrics: [{ + id: 'm1', + metric: { + metricName: 'NumberOfPublishedMessages', + namespace: 'AWS/SNS', + dimensions: undefined + }, + period: 300, + stat: 'Average', + unit: undefined + }, { + id: 'm2', + metric: { + metricName: 'Errors', + namespace: 'Custom', + dimensions: { + a: '1', + b: '2' + } + }, + period: 300, + stat: 'Average', + unit: undefined + }] }); - const expression = new cloudwatch.Sum(m1, m2).multiply(100) - .render(new cloudwatch.ExpressionContext()); - - test.equals(expression, 'SUM([m1,m2]) * 100'); test.done(); } }; \ No newline at end of file From d3d76ac6b2d4973372000f51ea4fe428e563b2be Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Tue, 18 Dec 2018 21:02:56 -0800 Subject: [PATCH 04/10] Update metric math design with an overview of their DSL --- design/metric-math.md | 113 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 design/metric-math.md diff --git a/design/metric-math.md b/design/metric-math.md new file mode 100644 index 0000000000000..ae862a977f7f7 --- /dev/null +++ b/design/metric-math.md @@ -0,0 +1,113 @@ +# Overview +There are two options for specifying the time-series data of a CloudWatch alarm: +* [Single, specific metric](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-alarm-metric.html) - user provides the metric name, namespace, dimensions, units, etc. +* [Metric-math expressions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) - user provides a list of metrics (as described above) and mathematical expressions to compute a (single) aggregate time-series metric. + +Our L2 `Alarm` construct currently only supports a specific metric - the simple case. This design proposes an extension of this construct to support metric math. + +# Expressions +CloudWatch metric-math is a domain-specific language (DSL) of data types, operators and functions for specifying mathematical aggregations of CloudWatch metric data. + +## Data Types +There are three data types in the language: +* Scalar (or `S`) - represents a scalar number such as `2`, `-5`, `50.25` +* Time-Series (or `TS`) - represents a single time-series metric such as CPU Utilization. Our `Metric` L2 construct can be considered a `TS` value. +* Time-Series array (or `TS[]`) - a collection of time-series metrics (multiple lines on a graph). +``` +// '::' denotes 'is of type' +100 :: S +m1 :: TS +[m1, m2] :: TS[] +``` + +## Arithmetic Operations and Functions +Arithmetic expressions may then be applied to these data types to compute refined time-series aggregates. There are: basic arithmetic operations such as `+`, `-`, `*`, `/` and `^`, and functions such as `SUM`, `AVG` and `STDDEV`. See the [official documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) for a complete explanation. +``` +AVG(m1) :: S +AVG([m1, m2]) :: TS +AVG([m1, m2]) + 1 :: TS +SUM([m1, m2]) :: TS +METRICS() :: TS[] +``` + +## Polymorphism +Both operators and functions are polymorphic with respect to input and output types - the result type depends on the input type, like an overloaded function. This is important, because if we aim to reproduce a representation of this mathematical system in code, then we should also aim to capture and enforce those rules. + +For example, the `AVG` function returns a `S` if passed a single `TS`, while it returns a `TS` if passed a collection, `TS[]`: + +``` +AVG(m1) :: S +AVG([m1, m2]) :: TS +``` + +The consequence of this is you can not alarm on `AVG(TS)` because its type is `S`. Ideally the type system would enforce this constraint at compile time, or at least synth/run time. + +## Intermediate Computations + +Metrics and expressons in a list of `MetricDataQuery` objects do not have to return data; so a complex expression may be reduced into smaller, simpler (and potentially re-usable) parts. In fact, a valid alarm definition *requires* that only one expression returns data. This is to say, the result type of an alarm definition must be `TS`. + +# Alarm + +Let's look at a simple example of computing the average number of published messages to an SNS topic. Instead of specifying the metric directly, we'll use an expression to compute the average of the `Sum` stat: + +```yaml +MyAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + Metrics: + - Id: m1 + MetricStat: + Metric: + Namespace: AWS/SNS + MetricName: NumberOfPublishedMessages + Dimensions: + - Name: TopicName + Value: MyTopic + Period: 300 + Stat: Sum + ReturnData: false + - Id: s1 + Expression: AVG(m1) + ReturnData: true +``` + +Our first metric, `m1`, is not an expression. It brings the topic's metric into the scope of the expression with the ID, `m1`. Think of it like assigning a local variable. Also note how the value of `ReturnData` is `false` - as previously mentioned, only one entry in an alarm definition is permitted to return data. By specifying `false`, we are indicating that this value is an 'intermediate' value only used as part of the larger computation. + +The second metric, `s1`, is a mathematical expression which averages the `m1` time-series metric + + +```javascript +[{ + Id: 'm1', // we're doing math on ths MetricStat + Metric: { + MetricName: 'NumberOfPublishedMessages', + Namespace: 'AWS/SNS', + Dimensions: [{ + Name: 'hello', + Value: 'world' + }] + }, + Period: 300, + Stat: 'Average', + ReturnData: false // just used for math +}, { + Id: 'm2', // math expression based on the 'm1' MetricStat + Expression: 'm1 / 2', + ReturnData: false // just used for math +}, { + Id: 'm3', // the final result is also an expression + Expression: 'SUM([m1,m2]) * 100', + ReturnData: true // use this time-series metric for the alarm +}] +``` + +Note how only one of the metrics (`m3`) has `ReturnData` set to `true` - this is a strict requirement for the [`PutMetricAlarm`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html) API, but not for the [`GetMetricData`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricData.html). + +https://docs.aws.amazon.com/sdkfornet/v3/apidocs/Index.html +``` +Gets and sets the property ReturnData. + +When used in GetMetricData, this option indicates whether to return the timestamps and raw data values of this metric. If you are performing this call just to do math expressions and do not also need the raw data returned, you can specify False. If you omit this, the default of True is used. + +When used in PutMetricAlarm, specify True for the one expression result to use as the alarm. For all other metrics and expressions in the same PutMetricAlarm operation, specify ReturnData as False. +``` From 486663c663da51630e224b51aa22a441aadf0334 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Tue, 18 Dec 2018 21:07:02 -0800 Subject: [PATCH 05/10] Updates to design - missed a bunch .. --- design/metric-math.md | 61 +++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/design/metric-math.md b/design/metric-math.md index ae862a977f7f7..c2c91041f0506 100644 --- a/design/metric-math.md +++ b/design/metric-math.md @@ -1,18 +1,13 @@ # Overview -There are two options for specifying the time-series data of a CloudWatch alarm: -* [Single, specific metric](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-alarm-metric.html) - user provides the metric name, namespace, dimensions, units, etc. -* [Metric-math expressions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) - user provides a list of metrics (as described above) and mathematical expressions to compute a (single) aggregate time-series metric. +Our L2 `Alarm` construct currently only supports specifying a specific metric to monitor - the simple case. This design discusses options for extending this construct to support metric math, a domain-specific language (DSL) of data types, operators and functions for specifying mathematical aggregations of CloudWatch metric data. -Our L2 `Alarm` construct currently only supports a specific metric - the simple case. This design proposes an extension of this construct to support metric math. - -# Expressions -CloudWatch metric-math is a domain-specific language (DSL) of data types, operators and functions for specifying mathematical aggregations of CloudWatch metric data. +See the [official documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) for a complete explanation. ## Data Types There are three data types in the language: -* Scalar (or `S`) - represents a scalar number such as `2`, `-5`, `50.25` -* Time-Series (or `TS`) - represents a single time-series metric such as CPU Utilization. Our `Metric` L2 construct can be considered a `TS` value. -* Time-Series array (or `TS[]`) - a collection of time-series metrics (multiple lines on a graph). +* Scalar (`S`) - represents a scalar number such as `2`, `-5`, `50.25` +* Time-Series (`TS`) - represents a single time-series metric such as CPU Utilization (single line on a graph). Our `Metric` L2 construct is an example of a `TS` value. +* Time-Series array (`TS[]`) - a collection of time-series metrics (multiple lines on a graph). ``` // '::' denotes 'is of type' 100 :: S @@ -21,7 +16,7 @@ m1 :: TS ``` ## Arithmetic Operations and Functions -Arithmetic expressions may then be applied to these data types to compute refined time-series aggregates. There are: basic arithmetic operations such as `+`, `-`, `*`, `/` and `^`, and functions such as `SUM`, `AVG` and `STDDEV`. See the [official documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) for a complete explanation. +Arithmetic expressions may then be applied to these data types to compute aggregations. There are: basic arithmetic operations such as `+`, `-`, `*`, `/` and `^`, and functions such as `SUM`, `AVG` and `STDDEV`. ``` AVG(m1) :: S AVG([m1, m2]) :: TS @@ -30,51 +25,55 @@ SUM([m1, m2]) :: TS METRICS() :: TS[] ``` -## Polymorphism -Both operators and functions are polymorphic with respect to input and output types - the result type depends on the input type, like an overloaded function. This is important, because if we aim to reproduce a representation of this mathematical system in code, then we should also aim to capture and enforce those rules. +Both operators and functions are polymorphic with respect to input and output types - the result type depends on the input type, like an overloaded function. This is important, because if we aim to reproduce a representation of this mathematical system in code, then we should also aim to capture and enforce those rules as best we can. For example, the `AVG` function returns a `S` if passed a single `TS`, while it returns a `TS` if passed a collection, `TS[]`: ``` -AVG(m1) :: S -AVG([m1, m2]) :: TS +AVG(m1) :: S // average of all data points +AVG([m1, m2]) :: TS // average of the series' points at each interval ``` -The consequence of this is you can not alarm on `AVG(TS)` because its type is `S`. Ideally the type system would enforce this constraint at compile time, or at least synth/run time. - -## Intermediate Computations +A consequence of this is you can not alarm on `AVG(TS)` because its type is `S`. -Metrics and expressons in a list of `MetricDataQuery` objects do not have to return data; so a complex expression may be reduced into smaller, simpler (and potentially re-usable) parts. In fact, a valid alarm definition *requires* that only one expression returns data. This is to say, the result type of an alarm definition must be `TS`. +## Intermediate Computation +Metric definitions and math expressions co-exist in a list of `MetricDataQuery` objects, but only one time-series (single line) result may be compared against the alarm condition. That is to say, the result type of a `MetricDataQuery` must be `TS` for it to be valid as an Alarm. -# Alarm +This is achieved by setting the `ReturnData` flag. When `true`, the result of that expression - whether it be a `TS` or `TS[]` - materializes as lines on a graph; setting it to `false` means the opposite. This mechanic then supports two applications: +* Reduction of a complex expression into smaller, simpler (and potentially re-usable) parts; +* Identify the expression which yields the time-series result to monitor as an alarm. -Let's look at a simple example of computing the average number of published messages to an SNS topic. Instead of specifying the metric directly, we'll use an expression to compute the average of the `Sum` stat: +Let's look at an example CloudFormation YAML definition of an alarm using metric-math: ```yaml MyAlarm: Type: AWS::CloudWatch::Alarm Properties: Metrics: - - Id: m1 + - Id: errors + MetricStat: + Metric: + MetricName: Errors + Period: 300 + Stat: Sum + ReturnData: false + - Id: requests MetricStat: Metric: - Namespace: AWS/SNS - MetricName: NumberOfPublishedMessages - Dimensions: - - Name: TopicName - Value: MyTopic + MetricName: Requests Period: 300 Stat: Sum ReturnData: false - - Id: s1 - Expression: AVG(m1) + - Id: error_rate + Expression: (requests - errors) * 100 ReturnData: true ``` -Our first metric, `m1`, is not an expression. It brings the topic's metric into the scope of the expression with the ID, `m1`. Think of it like assigning a local variable. Also note how the value of `ReturnData` is `false` - as previously mentioned, only one entry in an alarm definition is permitted to return data. By specifying `false`, we are indicating that this value is an 'intermediate' value only used as part of the larger computation. +We have requested two specific metrics, `errors` and `requests`. This brings those metrics into the scope of the query with their respective IDs. Think of it like assigning local variables for use later on. Also note how the value of `ReturnData` is `false` for these metrics - as previously mentioned, only one entry in an alarm definition is permitted to return data. By specifying `false`, we are indicating that this value is an 'intermediate' value only used as part of the larger computation. The expression, `error_rate`, then computes the error rate as a percentage using a simple expressio, `(requests - errors) * 100`. -The second metric, `s1`, is a mathematical expression which averages the `m1` time-series metric +# Option 1 - simple +The `Metric` L2 construct does a lot of work for us. Looking at the ```javascript [{ From e2e4a7ff32143299c0589fdfb246cca3671e1d85 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Tue, 18 Dec 2018 22:47:26 -0800 Subject: [PATCH 06/10] Improve prototype to better expression the S, TS and TS[] type system --- design/metric-math.md | 37 ---- .../@aws-cdk/aws-cloudwatch/lib/Untitled-1 | 4 + packages/@aws-cdk/aws-cloudwatch/lib/math.ts | 161 ++++++++++-------- .../@aws-cdk/aws-cloudwatch/lib/metric.ts | 5 +- .../@aws-cdk/aws-cloudwatch/test/test.math.ts | 63 ++++--- 5 files changed, 130 insertions(+), 140 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 diff --git a/design/metric-math.md b/design/metric-math.md index c2c91041f0506..0099560b2a7ff 100644 --- a/design/metric-math.md +++ b/design/metric-math.md @@ -73,40 +73,3 @@ We have requested two specific metrics, `errors` and `requests`. This brings tho # Option 1 - simple -The `Metric` L2 construct does a lot of work for us. Looking at the - -```javascript -[{ - Id: 'm1', // we're doing math on ths MetricStat - Metric: { - MetricName: 'NumberOfPublishedMessages', - Namespace: 'AWS/SNS', - Dimensions: [{ - Name: 'hello', - Value: 'world' - }] - }, - Period: 300, - Stat: 'Average', - ReturnData: false // just used for math -}, { - Id: 'm2', // math expression based on the 'm1' MetricStat - Expression: 'm1 / 2', - ReturnData: false // just used for math -}, { - Id: 'm3', // the final result is also an expression - Expression: 'SUM([m1,m2]) * 100', - ReturnData: true // use this time-series metric for the alarm -}] -``` - -Note how only one of the metrics (`m3`) has `ReturnData` set to `true` - this is a strict requirement for the [`PutMetricAlarm`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html) API, but not for the [`GetMetricData`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricData.html). - -https://docs.aws.amazon.com/sdkfornet/v3/apidocs/Index.html -``` -Gets and sets the property ReturnData. - -When used in GetMetricData, this option indicates whether to return the timestamps and raw data values of this metric. If you are performing this call just to do math expressions and do not also need the raw data returned, you can specify False. If you omit this, the default of True is used. - -When used in PutMetricAlarm, specify True for the one expression result to use as the alarm. For all other metrics and expressions in the same PutMetricAlarm operation, specify ReturnData as False. -``` diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 b/packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 new file mode 100644 index 0000000000000..cafbe7d48df14 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 @@ -0,0 +1,4 @@ +brazil-runtime-exec get_mcm_template.py \ +--odin-creds com.amazon.credentials.isengard.785049305830.user/mcm_signing \ +-f TM-21349 \ +-d templates/cdk/release-cdk \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts index 4a8c93c5148be..66d1e231bb06c 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts @@ -1,24 +1,41 @@ import { Metric } from "./metric"; // import { Alarm, AlarmProps } from "./alarm"; -// https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html +export interface IExpression { + render(context: ExpressionContext): string; +} -/* -S represents a scalar number, such as 2, -5, or 50.25. +export interface ITimeSeries extends IExpression { + plus(operand: number | Scalar | ITimeSeries): ITimeSeries; + plus(operand: ITimeSeriesArray): ITimeSeriesArray; +} -TS is a time series (a series of values for a single CloudWatch metric over time). -For example, the CPUUtilization metric for instance i-1234567890abcdef0 over the last three days. +export abstract class AbstractTimeSeries implements ITimeSeries { + public plus(operand: number | ITimeSeries | Scalar): ITimeSeries; + public plus(operand: ITimeSeriesArray): ITimeSeriesArray; + public plus(operand: any): any { + return new Plus(this, operand); + } -TS[] is an array of time series, such as the time series for multiple metrics. -*/ + public abstract render(context: ExpressionContext): string; +} + +export interface ITimeSeriesArray extends IExpression { + plus(operand: number | Scalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; +} export class ExpressionContext { private readonly metrics: { [key: string]: Metric } = {}; private counter = 1; - public metric(metric: Metric): string { + public nextId(): string { const id = 'm' + this.counter.toString(); this.counter += 1; + return id; + } + + public metric(metric: Metric): string { + const id = this.nextId(); this.metrics[id] = metric; return id; } @@ -32,88 +49,75 @@ export class ExpressionContext { metric: { metricName: metric.metricName, namespace: metric.namespace, - dimensions: metric.dimensions + dimensions: metric.dimensionsAsList() }, period: metric.periodSec, stat: metric.statistic, - unit: metric.unit + unit: metric.unit, + returnData: false }); }); return metrics; } } -export abstract class Expression { - public compile(): any { - const context = new ExpressionContext(); - const expression = this.render(context); - return { - expression, - metrics: context.toMetrics() - }; - } - - public abstract render(context: ExpressionContext): string; - - public plus(expression: Expression | number): Plus { - return new Plus(this, expression); - } - public minus(expression: Expression | number): Minus { - return new Minus(this, expression); - } - public multiply(expression: Expression | number): Multiply { - return new Multiply(this, expression); - } - public divide(expression: Expression | number): Divide { - return new Divide(this, expression); - } - public pow(expression: Expression | number): Exponent { - return new Exponent(this, expression); - } +export function compileExpression(ex: IExpression) { + const context = new ExpressionContext(); + const expression = ex.render(context); + const metrics = context.toMetrics(); + metrics.push({ + id: context.nextId(), + expression, + returnData: true + }); + return metrics; } -export class Scalar extends Expression { - constructor(private readonly value: number) { - super(); - } +export class Scalar implements IExpression { + constructor(private readonly value: number) {} + public render(_context: ExpressionContext): string { return this.value.toString(); } } -export class Literal extends Expression { - constructor(private readonly value: string) { - super(); - } + +export class Literal implements IExpression { + constructor(private readonly value: string) {} + public render(_context: ExpressionContext): string { return `"${this.value}"`; } } -export class TimeSeries extends Expression { - constructor(private readonly id: string) { - super(); - } +// export class TimeSeriesRef implements Expression, TimeSeries { +// constructor(private readonly id: string) {} - public render(_context: ExpressionContext): string { - return this.id; - } -} +// public render(_context: ExpressionContext): string { +// return this.id; +// } -export class TimeSeriesArray extends Expression { - constructor(private readonly array: Expression[]) { - super(); - } +// public plus(operand: number | TimeSeries | Scalar): TimeSeries; +// public plus(operand: TimeSeriesArray): TimeSeriesArray; +// public plus(operand: number | TimeSeries | Scalar | TimeSeriesArray): TimeSeries | TimeSeriesArray { +// throw new Error("Method not implemented."); +// } +// } + +export class TimeSeriesArrayRef implements IExpression, ITimeSeriesArray { + constructor(private readonly array: ITimeSeries[]) {} public render(context: ExpressionContext): string { return `[${this.array.map(a => a.render(context)).join(',')}]`; } + + public plus(_operand: number | Scalar | ITimeSeriesArray): ITimeSeriesArray { + throw new Error("Method not implemented."); + } } -export abstract class Operator extends Expression { +export abstract class Operator implements IExpression { protected abstract readonly operator: string; - constructor(private readonly lhs: Expression | number, private readonly rhs: Expression | number) { - super(); - } + constructor(private readonly lhs: IExpression | number, private readonly rhs: IExpression | number) {} public render(context: ExpressionContext): string { const lhs = typeof this.lhs === 'number' ? new Scalar(this.lhs) : this.lhs; @@ -137,29 +141,30 @@ export class Divide extends Operator { export class Exponent extends Operator { protected readonly operator: string = '^'; } - -export abstract class Function extends Expression { +export abstract class Function implements IExpression { protected abstract readonly name: string; - constructor(private readonly expressions: Expression[]) { - super(); - } + constructor(private readonly expressions: IExpression[]) {} public render(context: ExpressionContext): string { return `${this.name}(${this.expressions.map(ex => ex.render(context)).join(',')})`; } } export abstract class Function1 extends Function { - constructor(...expression: Expression[]) { - if (expression.length > 1) { - super([new TimeSeriesArray(expression)]); + constructor(...expression: ITimeSeries[]) { + if (expression.length === 1) { + if (Array.isArray(expression[0])) { + super([new TimeSeriesArrayRef(expression[0] as any)]); + } else { + super(expression); + } } else { - super(expression); + super([new TimeSeriesArrayRef(expression)]); } } } export abstract class Function2 extends Function { - constructor(expression1: Expression, expression2: Expression) { + constructor(expression1: IExpression, expression2: IExpression) { super([expression1, expression2]); } } @@ -204,6 +209,18 @@ export class Rate extends Function1 { export class StdDev extends Function1 { protected readonly name: string = 'STDDEV'; } -export class Sum extends Function1 { + +export function sum(ts: ITimeSeries): Scalar; +export function sum(...ts: ITimeSeries[]): ITimeSeries; +export function sum(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeries; +export function sum(...ts: any[]): any { + return new Sum(...ts); +} + +class Sum extends Function1 { protected readonly name: string = 'SUM'; + + public plus(a: any): any { + return new Plus(this, a); + } } diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts b/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts index b013bb402b481..732e15dc723b9 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts @@ -1,7 +1,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Alarm, ComparisonOperator, TreatMissingData } from './alarm'; -import { Expression, ExpressionContext } from './math'; +import { AbstractTimeSeries, ExpressionContext } from './math'; import { normalizeStatistic } from './util.statistic'; export type DimensionHash = {[dim: string]: any}; @@ -82,7 +82,7 @@ export interface MetricProps { * Metric is an abstraction that makes it easy to specify metrics for use in both * alarms and graphs. */ -export class Metric extends Expression { +export class Metric extends AbstractTimeSeries { /** * Grant permissions to the given identity to write metrics. * @@ -107,7 +107,6 @@ export class Metric extends Expression { constructor(props: MetricProps) { super(); - if (props.periodSec !== undefined && props.periodSec !== 1 && props.periodSec !== 5 && props.periodSec !== 10 && props.periodSec !== 30 && props.periodSec % 60 !== 0) { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts index 7166e2cf96510..bc3ea799a0cc3 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts @@ -15,35 +15,42 @@ export = { b: '2' } }); - const math = new cloudwatch.Sum(m1, m2).multiply(100); + const math = cloudwatch.sum(m1, m2).plus(100); + + test.deepEqual(cloudwatch.compileExpression(math), [{ + id: 'm1', + metric: { + metricName: 'NumberOfPublishedMessages', + namespace: 'AWS/SNS', + dimensions: [] + }, + period: 300, + stat: 'Average', + unit: undefined, + returnData: false + }, { + id: 'm2', + metric: { + metricName: 'Errors', + namespace: 'Custom', + dimensions: [{ + name: 'a', + value: '1' + }, { + name: 'b', + value: '2' + }] + }, + period: 300, + stat: 'Average', + unit: undefined, + returnData: false + }, { + id: 'm3', + expression: 'SUM([m1,m2]) + 100', + returnData: true + }]); - test.deepEqual(math.compile(), { - expression: 'SUM([m1,m2]) * 100', - metrics: [{ - id: 'm1', - metric: { - metricName: 'NumberOfPublishedMessages', - namespace: 'AWS/SNS', - dimensions: undefined - }, - period: 300, - stat: 'Average', - unit: undefined - }, { - id: 'm2', - metric: { - metricName: 'Errors', - namespace: 'Custom', - dimensions: { - a: '1', - b: '2' - } - }, - period: 300, - stat: 'Average', - unit: undefined - }] - }); test.done(); } }; \ No newline at end of file From 337bb9aa9621656ed9304531a5eefdf4a6739160 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 19 Dec 2018 00:16:53 -0800 Subject: [PATCH 07/10] Support ITimeSeries in Alarm --- packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 67 ++-- packages/@aws-cdk/aws-cloudwatch/lib/math.ts | 323 ++++++++++-------- .../@aws-cdk/aws-cloudwatch/test/test.math.ts | 5 +- 3 files changed, 223 insertions(+), 172 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 6c05caad19bbc..28080aea69713 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -1,8 +1,8 @@ import { Construct, Token } from '@aws-cdk/cdk'; import { CfnAlarm } from './cloudwatch.generated'; import { HorizontalAnnotation } from './graph'; -import { Dimension, Metric, Statistic, Unit } from './metric'; -import { parseStatistic } from './util.statistic'; +import { compileExpression, ITimeSeries } from './math'; +import { Dimension, Statistic, Unit } from './metric'; /** * Properties for Alarms @@ -14,7 +14,7 @@ export interface AlarmProps { * Metric objects can be obtained from most resources, or you can construct * custom Metric objects by instantiating one. */ - metric: Metric; + metric: ITimeSeries; /** * Name of the alarm @@ -80,12 +80,12 @@ export enum ComparisonOperator { LessThanOrEqualToThreshold = 'LessThanOrEqualToThreshold', } -const OPERATOR_SYMBOLS: {[key: string]: string} = { - GreaterThanOrEqualToThreshold: '>=', - GreaterThanThreshold: '>', - LessThanThreshold: '<', - LessThanOrEqualToThreshold: '>=', -}; +// const OPERATOR_SYMBOLS: {[key: string]: string} = { +// GreaterThanOrEqualToThreshold: '>=', +// GreaterThanThreshold: '>', +// LessThanThreshold: '<', +// LessThanOrEqualToThreshold: '>=', +// }; /** * Specify how missing data points are treated during alarm evaluation @@ -129,7 +129,7 @@ export class Alarm extends Construct { /** * The metric object this alarm was based on */ - public readonly metric: Metric; + public readonly metric: ITimeSeries; private alarmActionArns?: string[]; private insufficientDataActionArns?: string[]; @@ -164,15 +164,16 @@ export class Alarm extends Construct { okActions: new Token(() => this.okActionArns), // Metric - ...metricJson(props.metric) + // ...metricJson(props.metric) }); + alarm.addOverride('metrics', compileExpression(props.metric)); this.alarmArn = alarm.alarmArn; this.alarmName = alarm.alarmName; this.metric = props.metric; this.annotation = { // tslint:disable-next-line:max-line-length - label: `${this.metric.label || this.metric.metricName} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${props.evaluationPeriods} datapoints within ${describePeriod(props.evaluationPeriods * props.metric.periodSec)}`, + label: 'TODO', // `${this.metric.label || this.metric.metricName} ${OPERATOR_SYMBOLS[comparisonOperator]} ${props.threshold} for ${props.evaluationPeriods} datapoints within ${describePeriod(props.evaluationPeriods * props.metric.periodSec)}`, value: props.threshold, }; } @@ -242,12 +243,12 @@ export class Alarm extends Construct { * * We know the seconds are always one of a handful of allowed values. */ -function describePeriod(seconds: number) { - if (seconds === 60) { return '1 minute'; } - if (seconds === 1) { return '1 second'; } - if (seconds > 60) { return (seconds / 60) + ' minutes'; } - return seconds + ' seconds'; -} +// function describePeriod(seconds: number) { +// if (seconds === 60) { return '1 minute'; } +// if (seconds === 1) { return '1 second'; } +// if (seconds > 60) { return (seconds / 60) + ' minutes'; } +// return seconds + ' seconds'; +// } /** * Interface for objects that can be the targets of CloudWatch alarm actions @@ -262,21 +263,21 @@ export interface IAlarmAction { /** * Return the JSON structure which represents the given metric in an alarm. */ -function metricJson(metric: Metric): AlarmMetricJson { - const stat = parseStatistic(metric.statistic); - - const dims = metric.dimensionsAsList(); - - return { - dimensions: dims.length > 0 ? dims : undefined, - namespace: metric.namespace, - metricName: metric.metricName, - period: metric.periodSec, - statistic: stat.type === 'simple' ? stat.statistic : undefined, - extendedStatistic: stat.type === 'percentile' ? 'p' + stat.percentile : undefined, - unit: metric.unit - }; -} +// function metricJson(metric: Metric): AlarmMetricJson { +// const stat = parseStatistic(metric.statistic); + +// const dims = metric.dimensionsAsList(); + +// return { +// dimensions: dims.length > 0 ? dims : undefined, +// namespace: metric.namespace, +// metricName: metric.metricName, +// period: metric.periodSec, +// statistic: stat.type === 'simple' ? stat.statistic : undefined, +// extendedStatistic: stat.type === 'percentile' ? 'p' + stat.percentile : undefined, +// unit: metric.unit +// }; +// } /** * Properties used to construct the Metric identifying part of an Alarm diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts index 66d1e231bb06c..3657578b6bf21 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts @@ -1,28 +1,4 @@ import { Metric } from "./metric"; -// import { Alarm, AlarmProps } from "./alarm"; - -export interface IExpression { - render(context: ExpressionContext): string; -} - -export interface ITimeSeries extends IExpression { - plus(operand: number | Scalar | ITimeSeries): ITimeSeries; - plus(operand: ITimeSeriesArray): ITimeSeriesArray; -} - -export abstract class AbstractTimeSeries implements ITimeSeries { - public plus(operand: number | ITimeSeries | Scalar): ITimeSeries; - public plus(operand: ITimeSeriesArray): ITimeSeriesArray; - public plus(operand: any): any { - return new Plus(this, operand); - } - - public abstract render(context: ExpressionContext): string; -} - -export interface ITimeSeriesArray extends IExpression { - plus(operand: number | Scalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; -} export class ExpressionContext { private readonly metrics: { [key: string]: Metric } = {}; @@ -41,10 +17,10 @@ export class ExpressionContext { } public toMetrics(): any { - const metrics: any[] = []; + const metricArr: any[] = []; Object.keys(this.metrics).forEach(id => { const metric = this.metrics[id]; - metrics.push({ + metricArr.push({ id, metric: { metricName: metric.metricName, @@ -57,170 +33,243 @@ export class ExpressionContext { returnData: false }); }); - return metrics; + return metricArr; } } export function compileExpression(ex: IExpression) { const context = new ExpressionContext(); const expression = ex.render(context); - const metrics = context.toMetrics(); - metrics.push({ + const metricArr = context.toMetrics(); + metricArr.push({ id: context.nextId(), expression, returnData: true }); - return metrics; + return metricArr; } -export class Scalar implements IExpression { - constructor(private readonly value: number) {} - - public render(_context: ExpressionContext): string { - return this.value.toString(); - } +export interface IExpression { + render(context: ExpressionContext): string; } -export class Literal implements IExpression { - constructor(private readonly value: string) {} - - public render(_context: ExpressionContext): string { - return `"${this.value}"`; - } +export interface IScalar extends IExpression { + readonly tag: 'S'; } -// export class TimeSeriesRef implements Expression, TimeSeries { -// constructor(private readonly id: string) {} +export interface ITimeSeries extends IExpression { + readonly tag: 'TS'; -// public render(_context: ExpressionContext): string { -// return this.id; -// } + plus(operand: number | IScalar | ITimeSeries): ITimeSeries; + plus(operand: ITimeSeriesArray): ITimeSeriesArray; -// public plus(operand: number | TimeSeries | Scalar): TimeSeries; -// public plus(operand: TimeSeriesArray): TimeSeriesArray; -// public plus(operand: number | TimeSeries | Scalar | TimeSeriesArray): TimeSeries | TimeSeriesArray { -// throw new Error("Method not implemented."); -// } -// } + minus(operand: number | IScalar | ITimeSeries): ITimeSeries; + minus(operand: ITimeSeriesArray): ITimeSeriesArray; -export class TimeSeriesArrayRef implements IExpression, ITimeSeriesArray { - constructor(private readonly array: ITimeSeries[]) {} + multiply(operand: number | IScalar | ITimeSeries): ITimeSeries; + multiply(operand: ITimeSeriesArray): ITimeSeriesArray; - public render(context: ExpressionContext): string { - return `[${this.array.map(a => a.render(context)).join(',')}]`; - } + divide(operand: number | IScalar | ITimeSeries): ITimeSeries; + divide(operand: ITimeSeriesArray): ITimeSeriesArray; - public plus(_operand: number | Scalar | ITimeSeriesArray): ITimeSeriesArray { - throw new Error("Method not implemented."); - } + pow(operand: number | IScalar | ITimeSeries): ITimeSeries; + pow(operand: ITimeSeriesArray): ITimeSeriesArray; } -export abstract class Operator implements IExpression { - protected abstract readonly operator: string; - constructor(private readonly lhs: IExpression | number, private readonly rhs: IExpression | number) {} +export interface ITimeSeriesArray extends IExpression { + readonly tag: 'TS[]'; - public render(context: ExpressionContext): string { - const lhs = typeof this.lhs === 'number' ? new Scalar(this.lhs) : this.lhs; - const rhs = typeof this.rhs === 'number' ? new Scalar(this.rhs) : this.rhs; - return `${lhs.render(context)} ${this.operator} ${rhs.render(context)}`; - } + plus(operand: number | IScalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; + minus(operand: number | IScalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; + multiply(operand: number | IScalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; + divide(operand: number | IScalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; + pow(operand: number | IScalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; } -export class Plus extends Operator { - protected readonly operator: string = '+'; +export function abs(ts: ITimeSeries): IScalar; +export function abs(...ts: ITimeSeries[]): ITimeSeries; +export function abs(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeriesArray; +export function abs(...ts: any[]): any { + return new Function('ABS', ...ts); } -export class Minus extends Operator { - protected readonly operator: string = '+'; -} -export class Multiply extends Operator { - protected readonly operator: string = '*'; + +export function avg(ts: ITimeSeries): IScalar; +export function avg(...ts: ITimeSeries[]): ITimeSeries; +export function avg(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeries; +export function avg(...ts: any[]): any { + return new Function('AVG', ...ts); } -export class Divide extends Operator { - protected readonly operator: string = '/'; + +export function ceil(ts: ITimeSeries): IScalar; +export function ceil(...ts: ITimeSeries[]): ITimeSeries; +export function ceil(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeriesArray; +export function ceil(...ts: any[]): any { + return new Function('CEIL', ...ts); } -export class Exponent extends Operator { - protected readonly operator: string = '^'; + +export function fill(ts: ITimeSeries, value: ITimeSeries | IScalar | number): ITimeSeries; +export function fill(ts: ITimeSeries[] | ITimeSeriesArray, value: ITimeSeries | IScalar | number): ITimeSeriesArray; +export function fill(ts: any, value: any): any { + return new Function('FILL', [ts, value]); } -export abstract class Function implements IExpression { - protected abstract readonly name: string; - constructor(private readonly expressions: IExpression[]) {} +export function floor(ts: ITimeSeries): IScalar; +export function floor(...ts: ITimeSeries[]): ITimeSeries; +export function floor(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeriesArray; +export function floor(...ts: any[]): any { + return new Function('FLOOR', ...ts); +} - public render(context: ExpressionContext): string { - return `${this.name}(${this.expressions.map(ex => ex.render(context)).join(',')})`; - } +export function max(ts: ITimeSeries): IScalar; +export function max(...ts: ITimeSeries[]): ITimeSeries; +export function max(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeries; +export function max(...ts: any[]): any { + return new Function('MAX', ...ts); } -export abstract class Function1 extends Function { - constructor(...expression: ITimeSeries[]) { - if (expression.length === 1) { - if (Array.isArray(expression[0])) { - super([new TimeSeriesArrayRef(expression[0] as any)]); - } else { - super(expression); - } - } else { - super([new TimeSeriesArrayRef(expression)]); - } - } + +export function metricCount(): IScalar { + return new Function('METRIC_COUNT', []) as any; } -export abstract class Function2 extends Function { - constructor(expression1: IExpression, expression2: IExpression) { - super([expression1, expression2]); + +export function metrics(filter?: string): ITimeSeriesArray { + if (filter) { + return new Function('METRICS', new StringLiteral(filter)) as any; } + return new Function('METRICS') as any; } -export class Abs extends Function1 { - protected readonly name: string = 'ABS'; +export function min(ts: ITimeSeries): IScalar; +export function min(...ts: ITimeSeries[]): ITimeSeries; +export function min(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeries; +export function min(...ts: any[]): any { + return new Function('MIN', ...ts); } -export class Average extends Function1 { - protected readonly name: string = 'AVG'; + +export function period(ts: ITimeSeries): IScalar { + return new ScalarWrapper(new Function('PERIOD', ts)); } -export class Ceil extends Function1 { - protected readonly name: string = 'CEIL'; + +export function rate(ts: ITimeSeries): IScalar; +export function rate(...ts: ITimeSeries[]): ITimeSeries; +export function rate(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeriesArray; +export function rate(...ts: any[]): any { + return new Function('RATE', ...ts); } -export class Fill extends Function2 { - protected readonly name: string = 'CEIL'; + +export function stddev(ts: ITimeSeries): IScalar; +export function stddev(...ts: ITimeSeries[]): ITimeSeries; +export function stddev(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeries; +export function stddev(...ts: any[]): any { + return new Function('STDDEV', ...ts); } -export class Floor extends Function1 { - protected readonly name: string = 'FLOOR'; + +export function sum(ts: ITimeSeries): IScalar; +export function sum(...ts: ITimeSeries[]): ITimeSeries; +export function sum(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeries; +export function sum(...ts: any[]): any { + return new Function('SUM', ...ts); } -export class Max extends Function1 { - protected readonly name: string = 'MAX'; + +export abstract class AbstractExpression implements IExpression { + public plus(operand: any): any { + return new Operator('+', this, operand); + } + public minus(operand: any): any { + return new Operator('-', this, operand); + } + public multiply(operand: any): any { + return new Operator('*', this, operand); + } + public divide(operand: any): any { + return new Operator('/', this, operand); + } + public pow(operand: any): any { + return new Operator('^', this, operand); + } + + public abstract render(context: ExpressionContext): string; } -export class MetricCount extends Function1 { - protected readonly name: string = 'METRIC_COUNT'; + +export abstract class AbstractTimeSeries extends AbstractExpression implements ITimeSeries { + public readonly tag = 'TS'; + + public abstract render(context: ExpressionContext): string; } -export class Metrics extends Function { - protected readonly name: string = 'METRICS'; - constructor(filter?: string) { - super(filter !== undefined ? [new Literal(filter)] : []); +class Scalar extends AbstractExpression implements IScalar { + public readonly tag = 'S'; + + constructor(private readonly value: number) { + super(); + } + + public render(_context: ExpressionContext): string { + return this.value.toString(); } } -export class Min extends Function1 { - protected readonly name: string = 'MIN'; -} -export class Period extends Function1 { - protected readonly name: string = 'PERIOD'; + +class ScalarWrapper extends AbstractExpression implements IScalar { + public readonly tag = 'S'; + + constructor(private readonly delegate: IExpression) { + super(); + } + + public render(context: ExpressionContext): string { + return this.delegate.render(context); + } } -export class Rate extends Function1 { - protected readonly name: string = 'RATE'; + +class StringLiteral implements IScalar { + public readonly tag = 'S'; + + constructor(private readonly value: string) {} + + public render(_context: ExpressionContext): string { + return `"${this.value}"`; + } } -export class StdDev extends Function1 { - protected readonly name: string = 'STDDEV'; + +class TimeSeriesArrayRef extends AbstractExpression implements ITimeSeriesArray { + public readonly tag = 'TS[]'; + + constructor(private readonly array: ITimeSeries[]) { + super(); + } + + public render(context: ExpressionContext): string { + return `[${this.array.map(a => a.render(context)).join(',')}]`; + } } -export function sum(ts: ITimeSeries): Scalar; -export function sum(...ts: ITimeSeries[]): ITimeSeries; -export function sum(ts: ITimeSeries[] | ITimeSeriesArray): ITimeSeries; -export function sum(...ts: any[]): any { - return new Sum(...ts); +class Operator extends AbstractExpression { + constructor(private readonly operator: string, private readonly lhs: IExpression | number, private readonly rhs: IExpression | number) { + super(); + } + + public render(context: ExpressionContext): string { + const lhs = typeof this.lhs === 'number' ? new Scalar(this.lhs) : this.lhs; + const rhs = typeof this.rhs === 'number' ? new Scalar(this.rhs) : this.rhs; + return `${lhs.render(context)} ${this.operator} ${rhs.render(context)}`; + } } -class Sum extends Function1 { - protected readonly name: string = 'SUM'; +class Function extends AbstractExpression { + private readonly expressions: IExpression[]; + constructor(private readonly name: string, ...expressions: any[]) { + super(); + if (expressions.length === 1) { + if (Array.isArray(expressions[0])) { + this.expressions = [new TimeSeriesArrayRef(expressions[0] as any)]; + } else { + this.expressions = expressions; + } + } else { + this.expressions = [new TimeSeriesArrayRef(expressions)]; + } + } - public plus(a: any): any { - return new Plus(this, a); + public render(context: ExpressionContext): string { + return `${this.name}(${this.expressions.map(ex => ex.render(context)).join(',')})`; } } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts index bc3ea799a0cc3..656627b1d60d6 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.math.ts @@ -1,8 +1,9 @@ import { Test } from 'nodeunit'; import cloudwatch = require('../lib'); +import {sum} from '../lib'; export = { - 'SUM([m1,m2]) * 100'(test: Test) { + 'SUM([m1,m2]) + 100'(test: Test) { const m1 = new cloudwatch.Metric({ metricName: 'NumberOfPublishedMessages', namespace: 'AWS/SNS', @@ -15,7 +16,7 @@ export = { b: '2' } }); - const math = cloudwatch.sum(m1, m2).plus(100); + const math = sum(m1, m2).plus(100); test.deepEqual(cloudwatch.compileExpression(math), [{ id: 'm1', From 30a441ea3fc3cbf3fcb56d559d150bf9b4b9c7f6 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 19 Dec 2018 00:24:28 -0800 Subject: [PATCH 08/10] Add DataType enum --- packages/@aws-cdk/aws-cloudwatch/lib/math.ts | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts index 3657578b6bf21..052fff8187fca 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/math.ts @@ -49,16 +49,22 @@ export function compileExpression(ex: IExpression) { return metricArr; } +export enum DataType { + Scalar = 'S', + TimeSeries = 'TS', + TimeSeriesArray = 'TS[]' +} + export interface IExpression { render(context: ExpressionContext): string; } export interface IScalar extends IExpression { - readonly tag: 'S'; + readonly type: DataType.Scalar; } export interface ITimeSeries extends IExpression { - readonly tag: 'TS'; + readonly type: DataType.TimeSeries; plus(operand: number | IScalar | ITimeSeries): ITimeSeries; plus(operand: ITimeSeriesArray): ITimeSeriesArray; @@ -77,7 +83,7 @@ export interface ITimeSeries extends IExpression { } export interface ITimeSeriesArray extends IExpression { - readonly tag: 'TS[]'; + readonly type: DataType.TimeSeriesArray; plus(operand: number | IScalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; minus(operand: number | IScalar | ITimeSeriesArray | ITimeSeriesArray): ITimeSeriesArray; @@ -191,13 +197,13 @@ export abstract class AbstractExpression implements IExpression { } export abstract class AbstractTimeSeries extends AbstractExpression implements ITimeSeries { - public readonly tag = 'TS'; + public readonly type = DataType.TimeSeries; public abstract render(context: ExpressionContext): string; } class Scalar extends AbstractExpression implements IScalar { - public readonly tag = 'S'; + public readonly type = DataType.Scalar; constructor(private readonly value: number) { super(); @@ -209,7 +215,7 @@ class Scalar extends AbstractExpression implements IScalar { } class ScalarWrapper extends AbstractExpression implements IScalar { - public readonly tag = 'S'; + public readonly type = DataType.Scalar; constructor(private readonly delegate: IExpression) { super(); @@ -220,9 +226,7 @@ class ScalarWrapper extends AbstractExpression implements IScalar { } } -class StringLiteral implements IScalar { - public readonly tag = 'S'; - +class StringLiteral implements IExpression { constructor(private readonly value: string) {} public render(_context: ExpressionContext): string { @@ -231,7 +235,7 @@ class StringLiteral implements IScalar { } class TimeSeriesArrayRef extends AbstractExpression implements ITimeSeriesArray { - public readonly tag = 'TS[]'; + public readonly type = DataType.TimeSeriesArray; constructor(private readonly array: ITimeSeries[]) { super(); From d63a90a906fb8c039d76bde1940fb13b459f58d4 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 19 Dec 2018 02:23:50 -0800 Subject: [PATCH 09/10] Update design doc with option pros/cons and sample code --- design/metric-math.md | 147 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 137 insertions(+), 10 deletions(-) diff --git a/design/metric-math.md b/design/metric-math.md index 0099560b2a7ff..7bcb553b7b350 100644 --- a/design/metric-math.md +++ b/design/metric-math.md @@ -1,7 +1,7 @@ # Overview -Our L2 `Alarm` construct currently only supports specifying a specific metric to monitor - the simple case. This design discusses options for extending this construct to support metric math, a domain-specific language (DSL) of data types, operators and functions for specifying mathematical aggregations of CloudWatch metric data. +Our L2 `Alarm` construct currently only supports specifying a single metric to monitor. This design discusses options for extending this construct to support metric math, AWS's domain-specific language (DSL) which defines data types, operators and functions for specifying mathematical aggregations of CloudWatch metric data. -See the [official documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) for a complete explanation. +See the [official documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html) for a complete explanation of metric-math. ## Data Types There are three data types in the language: @@ -18,6 +18,7 @@ m1 :: TS ## Arithmetic Operations and Functions Arithmetic expressions may then be applied to these data types to compute aggregations. There are: basic arithmetic operations such as `+`, `-`, `*`, `/` and `^`, and functions such as `SUM`, `AVG` and `STDDEV`. ``` +m1 * 100 :: TS AVG(m1) :: S AVG([m1, m2]) :: TS AVG([m1, m2]) + 1 :: TS @@ -25,9 +26,9 @@ SUM([m1, m2]) :: TS METRICS() :: TS[] ``` -Both operators and functions are polymorphic with respect to input and output types - the result type depends on the input type, like an overloaded function. This is important, because if we aim to reproduce a representation of this mathematical system in code, then we should also aim to capture and enforce those rules as best we can. +Both operators and functions are polymorphic with respect to input and output types - the result type depends on the input type, like an overloaded function. This is important, because if we aim to reproduce a representation of this mathematical system in code, then we should also capture and enforce those rules as best we can. -For example, the `AVG` function returns a `S` if passed a single `TS`, while it returns a `TS` if passed a collection, `TS[]`: +For example, the `AVG` function returns a `S` if passed a single `TS`, while it returns a `TS` if passed a `TS[]`: ``` AVG(m1) :: S // average of all data points @@ -37,11 +38,11 @@ AVG([m1, m2]) :: TS // average of the series' points at each interval A consequence of this is you can not alarm on `AVG(TS)` because its type is `S`. ## Intermediate Computation -Metric definitions and math expressions co-exist in a list of `MetricDataQuery` objects, but only one time-series (single line) result may be compared against the alarm condition. That is to say, the result type of a `MetricDataQuery` must be `TS` for it to be valid as an Alarm. +Metric definitions and math expressions co-exist in a list of `MetricDataQuery` objects. Only one time-series (single line) result may be compared against the alarm condition. That is to say, the result type of a query must be `TS` to be valid as an Alarm. -This is achieved by setting the `ReturnData` flag. When `true`, the result of that expression - whether it be a `TS` or `TS[]` - materializes as lines on a graph; setting it to `false` means the opposite. This mechanic then supports two applications: +This is achieved by setting the `ReturnData` flag. When `true`, the result of that expression - whether it be a `TS` or `TS[]` - materializes as lines on a graph; setting it to `false` means it doesn't. This mechanic then enables two applications: * Reduction of a complex expression into smaller, simpler (and potentially re-usable) parts; -* Identify the expression which yields the time-series result to monitor as an alarm. +* Identify the `TS` result you want to monitor in the alarm. Let's look at an example CloudFormation YAML definition of an alarm using metric-math: @@ -54,6 +55,7 @@ MyAlarm: MetricStat: Metric: MetricName: Errors + Namespace: Test Period: 300 Stat: Sum ReturnData: false @@ -61,15 +63,140 @@ MyAlarm: MetricStat: Metric: MetricName: Requests + Namespace: Test Period: 300 Stat: Sum ReturnData: false - Id: error_rate - Expression: (requests - errors) * 100 + Expression: errors / requests * 100 ReturnData: true ``` -We have requested two specific metrics, `errors` and `requests`. This brings those metrics into the scope of the query with their respective IDs. Think of it like assigning local variables for use later on. Also note how the value of `ReturnData` is `false` for these metrics - as previously mentioned, only one entry in an alarm definition is permitted to return data. By specifying `false`, we are indicating that this value is an 'intermediate' value only used as part of the larger computation. The expression, `error_rate`, then computes the error rate as a percentage using a simple expressio, `(requests - errors) * 100`. +We have requested two specific metrics, `errors` and `requests`. This brings those metrics into the scope of the query with their respective IDs. Think of it like assigning local variables for use later on. Also note how the value of `ReturnData` is `false` for these metrics - as previously mentioned, only one entry in an alarm definition is permitted to return data. By specifying `false`, we are indicating that this value is an 'intermediate' value used only as part of the larger computation. The expression, `error_rate`, then computes the error rate as a percentage using a simple expression, `errors / requests * 100`. `ReturnData` is set to true because we want to alarm on this `TS` value. -# Option 1 - simple +# Design Options +We will discuss three options: +1. The simplest (or 'lowest') approach which simply enhances the raw CloudFormation API with our `Metric` L2. +2. A specialization of Option 1 designed to reduce boiler-plate and better express the computation of a single `TS` value +3. A type-safe DSL which makes use of functions and classes to model the metric-math system in code + +## Option 1 - Simple + +The simplest approach is to stay true to the CloudFormation format, but leverage the `Metric` L2 for importing specific metrics into the query scope: + +```typescript +const errors = new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'Test' +}); +const requests = new cloudwatch.Metric({ + metricName: 'Requests', + namespace: 'Test' +}); + +const alarm = new cloudwatch.Alarm(this, 'woof', { + // etc. + metrics: [{ + id: 'errors', + metric: errors, + returnData: false + }, { + id: 'requests', + metric: requests, + returnData: false + }, { + id: 'result', + expression: 'errors / requests * 100', + returnData: true + }] +}); +``` + +### Pros +* Reflects the official CloudFormation API +* Compatible with the existing `Metric` L2 construct +* There is no hidden 'magic' (see Option 3) + * Understanding how and what you are doing is intuitive (if you already understand the API) + * Risk of 'us' introducing bugs is minimized + +### Cons +* More verbose - the developer has to write a lot of boiler-plate such as allocating ids, returnData, etc. +* The developer must learn how to express valid metric expressions according to the somewhat complicated API and documentation. The `returnData` nuances were particulary difficult to tease out from the docs, for example. +* Not type-safe - there is no static checking of the expression to ensure it is valid. Failures are caught at deployment time which is very slow, unless we build our own parser ... ? + +## Option 2 - Slight Enhancement + +Straying from the original CFN format a little, we can specialize the API in a way that better reflects the usual case: +1. Fetch and assign metrics to IDs +2. Use an expression to compute and return a `TS` result + +```typescript +const errors = new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'Test' +}); +const requests = new cloudwatch.Metric({ + metricName: 'Requests', + namespace: 'Test' +}); + +const alarm = new cloudwatch.Alarm(this, 'woof', { + // etc. + metrics: { + ids: { + errors, + requests + }, + expression: 'errors / requests * 100' + } +}); +``` + +### Pros +* Compact + * It takes less code to express the math + * There is no need to manage `returnData` which is not as useful for alarms as it is for the `GetMetricData` API. + +### Cons +* Different to the original CFN and SDK API. +* It's opinionated - are there metrics that can not be expressed in this way? +* Also not statically checked - errors in the expression will be found at deployment time. + +## Option 3 - Type-safe DSL + +Model the data types, operators and functions as classes and methods in code. + +```typescript +const errors = new cloudwatch.Metric({ + metricName: 'Errors', + namespace: 'Test' +}); +const requests = new cloudwatch.Metric({ + metricName: 'Requests', + namespace: 'Test' +}); + +const alarm = new cloudwatch.Alarm(this, 'woof', { + // etc. + metrics: errors.divide(requests).multiply(100) +}); +``` + +### Pros +* Type-safe - the compiler and implementation checks the validity of expressions. For example, it ensures the final result is a `TS` and the arguments to functions or operators are used correctly. +* IDE discoverability, auto-complete, in-line documentation. +* Programmatic variation - like how we build constructs, the expression tree can be assembled incrementally with standard programming techniques (`if-else`, `functions`, `classes`, etc.) + +### Cons +* Highly opinionated and different to the original API +* We must maintain backwards compatibility with any future features released by the CloudWatch team +* Risk of us introducing bugs is higher - especially for large complicated expressions +* Expressing paranthesis to control mathematical precedence rules may be ugly or unintuitive +* Could be problematic for JSII, although it does pass its checks :) + +### Implementation + +This design comes with a prototype which will be briefly explained in this section. See the code and tests for more depth. + +TODO: Explain the implementation (look at the code and tests in the meantime - it is functional). From a701474ef73945af27f6b1bb810f1cc9c10cfa65 Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 19 Dec 2018 23:53:07 -0800 Subject: [PATCH 10/10] Remove accidental file and move design to cloudwatch --- .../@aws-cdk/aws-cloudwatch/design}/metric-math.md | 0 packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 | 4 ---- 2 files changed, 4 deletions(-) rename {design => packages/@aws-cdk/aws-cloudwatch/design}/metric-math.md (100%) delete mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 diff --git a/design/metric-math.md b/packages/@aws-cdk/aws-cloudwatch/design/metric-math.md similarity index 100% rename from design/metric-math.md rename to packages/@aws-cdk/aws-cloudwatch/design/metric-math.md diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 b/packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 deleted file mode 100644 index cafbe7d48df14..0000000000000 --- a/packages/@aws-cdk/aws-cloudwatch/lib/Untitled-1 +++ /dev/null @@ -1,4 +0,0 @@ -brazil-runtime-exec get_mcm_template.py \ ---odin-creds com.amazon.credentials.isengard.785049305830.user/mcm_signing \ --f TM-21349 \ --d templates/cdk/release-cdk \ No newline at end of file