packages/aws-cdk-lib/aws-cloudwatch/lib/metric.ts (450 lines of code) (raw):

import { Construct, IConstruct } from 'constructs'; import { Alarm, ComparisonOperator, TreatMissingData } from './alarm'; import { Dimension, IMetric, MetricAlarmConfig, MetricConfig, MetricGraphConfig, Statistic, Unit } from './metric-types'; import { dispatchMetric, metricKey } from './private/metric-util'; import { normalizeStatistic, pairStatisticToString, parseStatistic, singleStatisticToString } from './private/statistic'; import { Stats } from './stats'; import * as iam from '../../aws-iam'; import * as cdk from '../../core'; import { makeEnumerable } from './private/make-enumerable'; export type DimensionHash = { [dim: string]: any }; export type DimensionsMap = { [dim: string]: string }; /** * Options shared by most methods accepting metric options */ export interface CommonMetricOptions { /** * The period over which the specified statistic is applied. * * @default Duration.minutes(5) */ readonly period?: cdk.Duration; /** * What function to use for aggregating. * * Use the `aws_cloudwatch.Stats` helper class to construct valid input strings. * * Can be one of the following: * * - "Minimum" | "min" * - "Maximum" | "max" * - "Average" | "avg" * - "Sum" | "sum" * - "SampleCount | "n" * - "pNN.NN" * - "tmNN.NN" | "tm(NN.NN%:NN.NN%)" * - "iqm" * - "wmNN.NN" | "wm(NN.NN%:NN.NN%)" * - "tcNN.NN" | "tc(NN.NN%:NN.NN%)" * - "tsNN.NN" | "ts(NN.NN%:NN.NN%)" * * @default Average */ readonly statistic?: string; /** * Dimensions of the metric * * @default - No dimensions. * * @deprecated Use 'dimensionsMap' instead. */ readonly dimensions?: DimensionHash; /** * Dimensions of the metric * * @default - No dimensions. */ readonly dimensionsMap?: DimensionsMap; /** * Unit used to filter the metric stream * * Only refer to datums emitted to the metric stream with the given unit and * ignore all others. Only useful when datums are being emitted to the same * metric stream under different units. * * The default is to use all matric datums in the stream, regardless of unit, * which is recommended in nearly all cases. * * CloudWatch does not honor this property for graphs. * * @default - All metric datums in the given metric stream */ readonly unit?: Unit; /** * Label for this metric when added to a Graph in a Dashboard * * You can use [dynamic labels](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/graph-dynamic-labels.html) * to show summary information about the entire displayed time series * in the legend. For example, if you use: * * ``` * [max: ${MAX}] MyMetric * ``` * * As the metric label, the maximum value in the visible range will * be shown next to the time series name in the graph's legend. * * @default - No label */ readonly label?: string; /** * The hex color code, prefixed with '#' (e.g. '#00ff00'), to use when this metric is rendered on a graph. * The `Color` class has a set of standard colors that can be used here. * @default - Automatic color */ readonly color?: string; /** * Account which this metric comes from. * * @default - Deployment account. */ readonly account?: string; /** * Region which this metric comes from. * * @default - Deployment region. */ readonly region?: string; /** * Account of the stack this metric is attached to. * * @default - Deployment account. */ readonly stackAccount?: string; /** * Region of the stack this metric is attached to. * * @default - Deployment region. */ readonly stackRegion?: string; } /** * Properties for a metric */ export interface MetricProps extends CommonMetricOptions { /** * Namespace of the metric. */ readonly namespace: string; /** * Name of the metric. */ readonly metricName: string; } /** * Properties of a metric that can be changed */ export interface MetricOptions extends CommonMetricOptions { } /** * Configurable options for MathExpressions */ export interface MathExpressionOptions { /** * Label for this expression when added to a Graph in a Dashboard * * If this expression evaluates to more than one time series (for * example, through the use of `METRICS()` or `SEARCH()` expressions), * each time series will appear in the graph using a combination of the * expression label and the individual metric label. Specify the empty * string (`''`) to suppress the expression label and only keep the * metric label. * * You can use [dynamic labels](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/graph-dynamic-labels.html) * to show summary information about the displayed time series * in the legend. For example, if you use: * * ``` * [max: ${MAX}] MyMetric * ``` * * As the metric label, the maximum value in the visible range will * be shown next to the time series name in the graph's legend. If the * math expression produces more than one time series, the maximum * will be shown for each individual time series produce by this * math expression. * * @default - Expression value is used as label */ readonly label?: string; /** * Color for this metric when added to a Graph in a Dashboard * * @default - Automatic color */ readonly color?: string; /** * The period over which the expression's statistics are applied. * * This period overrides all periods in the metrics used in this * math expression. * * @default Duration.minutes(5) */ readonly period?: cdk.Duration; /** * Account to evaluate search expressions within. * * Specifying a searchAccount has no effect to the account used * for metrics within the expression (passed via usingMetrics). * * @default - Deployment account. */ readonly searchAccount?: string; /** * Region to evaluate search expressions within. * * Specifying a searchRegion has no effect to the region used * for metrics within the expression (passed via usingMetrics). * * @default - Deployment region. */ readonly searchRegion?: string; } /** * Properties for a MathExpression */ export interface MathExpressionProps extends MathExpressionOptions { /** * The expression defining the metric. * * When an expression contains a SEARCH function, it cannot be used * within an Alarm. */ readonly expression: string; /** * The metrics used in the expression, in a map. * * The key is the identifier that represents the given metric in the * expression, and the value is the actual Metric object. * * The `period` of each metric in `usingMetrics` is ignored and instead overridden * by the `period` specified for the `MathExpression` construct. Even if no `period` * is specified for the `MathExpression`, it will be overridden by the default * value (`Duration.minutes(5)`). * * Example: * * ```ts * declare const metrics: elbv2.IApplicationLoadBalancerMetrics; * new cloudwatch.MathExpression({ * expression: 'm1+m2', * label: 'AlbErrors', * usingMetrics: { * m1: metrics.custom('HTTPCode_ELB_500_Count', { * period: Duration.minutes(1), // <- This period will be ignored * statistic: 'Sum', * label: 'HTTPCode_ELB_500_Count', * }), * m2: metrics.custom('HTTPCode_ELB_502_Count', { * period: Duration.minutes(1), // <- This period will be ignored * statistic: 'Sum', * label: 'HTTPCode_ELB_502_Count', * }), * }, * period: Duration.minutes(3), // <- This overrides the period of each metric in `usingMetrics` * // (Even if not specified, it is overridden by the default value) * }); * ``` * * @default - Empty map. */ readonly usingMetrics?: Record<string, IMetric>; } /** * A metric emitted by a service * * The metric is a combination of a metric identifier (namespace, name and dimensions) * and an aggregation function (statistic, period and unit). * * It also contains metadata which is used only in graphs, such as color and label. * It makes sense to embed this in here, so that compound constructs can attach * that metadata to metrics they expose. * * This class does not represent a resource, so hence is not a construct. Instead, * Metric is an abstraction that makes it easy to specify metrics for use in both * alarms and graphs. */ export class Metric implements IMetric { /** * Grant permissions to the given identity to write metrics. * * @param grantee The IAM identity to give permissions to. */ public static grantPutMetricData(grantee: iam.IGrantable): iam.Grant { return iam.Grant.addToPrincipal({ grantee, actions: ['cloudwatch:PutMetricData'], resourceArns: ['*'], }); } /** Dimensions of this metric */ public readonly dimensions?: DimensionHash; /** Namespace of this metric */ public readonly namespace: string; /** Name of this metric */ public readonly metricName: string; /** Period of this metric */ public readonly period: cdk.Duration; /** Statistic of this metric */ public readonly statistic: string; /** Label for this metric when added to a Graph in a Dashboard */ public readonly label?: string; /** The hex color code used when this metric is rendered on a graph. */ public readonly color?: string; /** Unit of the metric. */ public readonly unit?: Unit; /** Account of the stack this metric is attached to. */ readonly #stackAccount?: string; /** Region of the stack this metric is attached to. */ readonly #stackRegion?: string; /** Account set directly on the metric, taking precedence over the stack account. */ readonly #accountOverride?: string; /** Region set directly on the metric, taking precedence over the stack region. */ readonly #regionOverride?: string; /** * Warnings attached to this metric. * @deprecated - use warningsV2 **/ public readonly warnings?: string[]; /** Warnings attached to this metric. */ public readonly warningsV2?: { [id: string]: string }; constructor(props: MetricProps) { this.period = props.period || cdk.Duration.minutes(5); const periodSec = this.period.toSeconds(); if (periodSec !== 1 && periodSec !== 5 && periodSec !== 10 && periodSec !== 30 && periodSec % 60 !== 0) { throw new cdk.UnscopedValidationError(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`); } this.warnings = undefined; this.dimensions = this.validateDimensions(props.dimensionsMap ?? props.dimensions); this.namespace = props.namespace; this.metricName = props.metricName; const parsedStat = parseStatistic(props.statistic || Stats.AVERAGE); if (parsedStat.type === 'generic') { // Unrecognized statistic, do not throw, just warn // There may be a new statistic that this lib does not support yet const label = props.label ? `, label "${props.label}"`: ''; const warning = `Unrecognized statistic "${props.statistic}" for metric with namespace "${props.namespace}"${label} and metric name "${props.metricName}".` + ' Preferably use the `aws_cloudwatch.Stats` helper class to specify a statistic.' + ' You can ignore this warning if your statistic is valid but not yet supported by the `aws_cloudwatch.Stats` helper class.'; this.warningsV2 = { 'CloudWatch:Alarm:UnrecognizedStatistic': warning, }; this.warnings = [warning]; } this.statistic = normalizeStatistic(parsedStat); this.label = props.label; this.color = props.color; this.unit = props.unit; this.#accountOverride = props.account; this.#regionOverride = props.region; this.#stackAccount = props.stackAccount; this.#stackRegion = props.stackRegion; // Make getters enumerable. makeEnumerable(Metric.prototype, this, 'account'); makeEnumerable(Metric.prototype, this, 'region'); } /** * Return a copy of Metric `with` properties changed. * * All properties except namespace and metricName can be changed. * * @param props The set of properties to change. */ public with(props: MetricOptions): Metric { // Short-circuit creating a new object if there would be no effective change if ((props.label === undefined || props.label === this.label) && (props.color === undefined || props.color === this.color) && (props.statistic === undefined || props.statistic === this.statistic) && (props.unit === undefined || props.unit === this.unit) && (props.account === undefined || props.account === this.#accountOverride) && (props.region === undefined || props.region === this.#regionOverride) && (props.stackAccount === undefined || props.stackAccount === this.#stackAccount) && (props.stackRegion === undefined || props.stackRegion === this.#stackRegion) // For these we're not going to do deep equality, misses some opportunity for optimization // but that's okay. && (props.dimensions === undefined) && (props.dimensionsMap === undefined) && (props.period === undefined || props.period.toSeconds() === this.period.toSeconds())) { return this; } return new Metric({ dimensionsMap: props.dimensionsMap ?? props.dimensions ?? this.dimensions, namespace: this.namespace, metricName: this.metricName, period: ifUndefined(props.period, this.period), statistic: ifUndefined(props.statistic, this.statistic), unit: ifUndefined(props.unit, this.unit), label: ifUndefined(props.label, this.label), color: ifUndefined(props.color, this.color), account: ifUndefined(props.account, this.#accountOverride), region: ifUndefined(props.region, this.#regionOverride), stackAccount: ifUndefined(props.stackAccount, this.#stackAccount), stackRegion: ifUndefined(props.stackRegion, this.#stackRegion), }); } /** * Attach the metric object to the given construct scope * * Returns a Metric object that uses the account and region from the Stack * the given construct is defined in. If the metric is subsequently used * in a Dashboard or Alarm in a different Stack defined in a different * account or region, the appropriate 'region' and 'account' fields * will be added to it. * * If the scope we attach to is in an environment-agnostic stack, * nothing is done and the same Metric object is returned. */ public attachTo(scope: IConstruct): Metric { const stack = cdk.Stack.of(scope); return this.with({ stackAccount: cdk.Token.isUnresolved(stack.account) ? undefined : stack.account, stackRegion: cdk.Token.isUnresolved(stack.region) ? undefined : stack.region, }); } /** * Account which this metric comes from. */ public get account(): string | undefined { return this.#accountOverride || this.#stackAccount; } /** * Region which this metric comes from. */ public get region(): string | undefined { return this.#regionOverride || this.#stackRegion; } public toMetricConfig(): MetricConfig { const dims = this.dimensionsAsList(); return { metricStat: { dimensions: dims.length > 0 ? dims : undefined, namespace: this.namespace, metricName: this.metricName, period: this.period, statistic: this.statistic, unitFilter: this.unit, account: this.account, region: this.region, accountOverride: this.#accountOverride, regionOverride: this.#regionOverride, }, renderingProperties: { color: this.color, label: this.label, }, }; } /** @deprecated use toMetricConfig() */ public toAlarmConfig(): MetricAlarmConfig { const metricConfig = this.toMetricConfig(); if (metricConfig.metricStat === undefined) { throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead'); } const parsed = parseStatistic(metricConfig.metricStat.statistic); let extendedStatistic: string | undefined = undefined; if (parsed.type === 'single') { extendedStatistic = singleStatisticToString(parsed); } else if (parsed.type === 'pair') { extendedStatistic = pairStatisticToString(parsed); } return { dimensions: metricConfig.metricStat.dimensions, namespace: metricConfig.metricStat.namespace, metricName: metricConfig.metricStat.metricName, period: metricConfig.metricStat.period.toSeconds(), statistic: parsed.type === 'simple' ? parsed.statistic as Statistic : undefined, extendedStatistic, unit: this.unit, }; } /** * @deprecated use toMetricConfig() */ public toGraphConfig(): MetricGraphConfig { const metricConfig = this.toMetricConfig(); if (metricConfig.metricStat === undefined) { throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead'); } return { dimensions: metricConfig.metricStat.dimensions, namespace: metricConfig.metricStat.namespace, metricName: metricConfig.metricStat.metricName, renderingProperties: { period: metricConfig.metricStat.period.toSeconds(), stat: metricConfig.metricStat.statistic, color: asString(metricConfig.renderingProperties?.color), label: asString(metricConfig.renderingProperties?.label), }, // deprecated properties for backwards compatibility period: metricConfig.metricStat.period.toSeconds(), statistic: metricConfig.metricStat.statistic, color: asString(metricConfig.renderingProperties?.color), label: asString(metricConfig.renderingProperties?.label), unit: this.unit, }; } /** * Make a new Alarm for this metric * * Combines both properties that may adjust the metric (aggregation) as well * as alarm properties. */ public createAlarm(scope: Construct, id: string, props: CreateAlarmOptions): Alarm { return new Alarm(scope, id, { metric: this.with({ statistic: props.statistic, period: props.period, }), alarmName: props.alarmName, alarmDescription: props.alarmDescription, comparisonOperator: props.comparisonOperator, datapointsToAlarm: props.datapointsToAlarm, threshold: props.threshold, evaluationPeriods: props.evaluationPeriods, evaluateLowSampleCountPercentile: props.evaluateLowSampleCountPercentile, treatMissingData: props.treatMissingData, actionsEnabled: props.actionsEnabled, }); } public toString() { return this.label || this.metricName; } /** * Return the dimensions of this Metric as a list of Dimension. */ private dimensionsAsList(): Dimension[] { const dims = this.dimensions; if (dims === undefined) { return []; } const list = Object.keys(dims).sort().map(key => ({ name: key, value: dims[key] })); return list; } private validateDimensions(dims?: DimensionHash): DimensionHash | undefined { if (!dims) { return dims; } var dimsArray = Object.keys(dims); if (dimsArray?.length > 30) { throw new cdk.UnscopedValidationError(`The maximum number of dimensions is 30, received ${dimsArray.length}`); } dimsArray.map(key => { if (dims[key] === undefined || dims[key] === null) { throw new cdk.UnscopedValidationError(`Dimension value of '${dims[key]}' is invalid for key: ${key}`); } if (key.length < 1 || key.length > 255) { throw new cdk.UnscopedValidationError(`Dimension name must be at least 1 and no more than 255 characters; received ${key}`); } if (dims[key].length < 1 || dims[key].length > 255) { throw new cdk.UnscopedValidationError(`Dimension value must be at least 1 and no more than 255 characters; received ${dims[key]}`); } }); return dims; } } function asString(x?: unknown): string | undefined { if (x === undefined) { return undefined; } if (typeof x !== 'string') { throw new cdk.UnscopedValidationError(`Expected string, got ${x}`); } return x; } /** * A math expression built with metric(s) emitted by a service * * The math expression is a combination of an expression (x+y) and metrics to apply expression on. * It also contains metadata which is used only in graphs, such as color and label. * It makes sense to embed this in here, so that compound constructs can attach * that metadata to metrics they expose. * * MathExpression can also be used for search expressions. In this case, * it also optionally accepts a searchRegion and searchAccount property for cross-environment * search expressions. * * This class does not represent a resource, so hence is not a construct. Instead, * MathExpression is an abstraction that makes it easy to specify metrics for use in both * alarms and graphs. */ export class MathExpression implements IMetric { /** * The expression defining the metric. */ public readonly expression: string; /** * The metrics used in the expression as KeyValuePair <id, metric>. */ public readonly usingMetrics: Record<string, IMetric>; /** * Label for this metric when added to a Graph. */ public readonly label?: string; /** * The hex color code, prefixed with '#' (e.g. '#00ff00'), to use when this metric is rendered on a graph. * The `Color` class has a set of standard colors that can be used here. */ public readonly color?: string; /** * Aggregation period of this metric */ public readonly period: cdk.Duration; /** * Account to evaluate search expressions within. */ public readonly searchAccount?: string; /** * Region to evaluate search expressions within. */ public readonly searchRegion?: string; /** * Warnings generated by this math expression * @deprecated - use warningsV2 */ public readonly warnings?: string[]; /** * Warnings generated by this math expression */ public readonly warningsV2?: { [id: string]: string }; constructor(props: MathExpressionProps) { this.period = props.period || cdk.Duration.minutes(5); this.expression = props.expression; this.label = props.label; this.color = props.color; this.searchAccount = props.searchAccount; this.searchRegion = props.searchRegion; const { record, overridden } = changeAllPeriods(props.usingMetrics ?? {}, this.period); this.usingMetrics = record; const warnings: { [id: string]: string } = {}; if (overridden) { warnings['CloudWatch:Math:MetricsPeriodsOverridden'] = `Periods of metrics in 'usingMetrics' for Math expression '${this.expression}' have been overridden to ${this.period.toSeconds()} seconds.`; } const invalidVariableNames = Object.keys(this.usingMetrics).filter(x => !validVariableName(x)); if (invalidVariableNames.length > 0) { throw new cdk.UnscopedValidationError(`Invalid variable names in expression: ${invalidVariableNames}. Must start with lowercase letter and only contain alphanumerics.`); } this.validateNoIdConflicts(); // Check that all IDs used in the expression are also in the `usingMetrics` map. We // can't throw on this anymore since we didn't use to do this validation from the start // and now there will be loads of people who are violating the expected contract, but // we can add warnings. const missingIdentifiers = allIdentifiersInExpression(this.expression).filter(i => !this.usingMetrics[i]); if (!this.expression.toUpperCase().match('\\b(INSIGHT_RULE_METRIC|SELECT|SEARCH|METRICS)\\b') && missingIdentifiers.length > 0) { warnings['CloudWatch:Math:UnknownIdentifier'] = `Math expression '${this.expression}' references unknown identifiers: ${missingIdentifiers.join(', ')}. Please add them to the 'usingMetrics' map.`; } // Also copy warnings from deeper levels so graphs, alarms only have to inspect the top-level objects for (const m of Object.values(this.usingMetrics)) { for (const [id, message] of Object.entries(m.warningsV2 ?? {})) { warnings[id] = message; } } if (Object.keys(warnings).length > 0) { this.warnings = Array.from(Object.values(warnings)); this.warningsV2 = warnings; } } /** * Return a copy of Metric with properties changed. * * All properties except namespace and metricName can be changed. * * @param props The set of properties to change. */ public with(props: MathExpressionOptions): MathExpression { // Short-circuit creating a new object if there would be no effective change if ((props.label === undefined || props.label === this.label) && (props.color === undefined || props.color === this.color) && (props.period === undefined || props.period.toSeconds() === this.period.toSeconds()) && (props.searchAccount === undefined || props.searchAccount === this.searchAccount) && (props.searchRegion === undefined || props.searchRegion === this.searchRegion)) { return this; } return new MathExpression({ expression: this.expression, usingMetrics: this.usingMetrics, label: ifUndefined(props.label, this.label), color: ifUndefined(props.color, this.color), period: ifUndefined(props.period, this.period), searchAccount: ifUndefined(props.searchAccount, this.searchAccount), searchRegion: ifUndefined(props.searchRegion, this.searchRegion), }); } /** * @deprecated use toMetricConfig() */ public toAlarmConfig(): MetricAlarmConfig { throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead'); } /** * @deprecated use toMetricConfig() */ public toGraphConfig(): MetricGraphConfig { throw new cdk.UnscopedValidationError('Using a math expression is not supported here. Pass a \'Metric\' object instead'); } public toMetricConfig(): MetricConfig { return { mathExpression: { period: this.period.toSeconds(), expression: this.expression, usingMetrics: this.usingMetrics, searchAccount: this.searchAccount, searchRegion: this.searchRegion, }, renderingProperties: { label: this.label, color: this.color, }, }; } /** * Make a new Alarm for this metric * * Combines both properties that may adjust the metric (aggregation) as well * as alarm properties. */ public createAlarm(scope: Construct, id: string, props: CreateAlarmOptions): Alarm { return new Alarm(scope, id, { metric: this.with({ period: props.period, }), alarmName: props.alarmName, alarmDescription: props.alarmDescription, comparisonOperator: props.comparisonOperator, datapointsToAlarm: props.datapointsToAlarm, threshold: props.threshold, evaluationPeriods: props.evaluationPeriods, evaluateLowSampleCountPercentile: props.evaluateLowSampleCountPercentile, treatMissingData: props.treatMissingData, actionsEnabled: props.actionsEnabled, }); } public toString() { return this.label || this.expression; } private validateNoIdConflicts() { const seen = new Map<string, IMetric>(); visit(this); function visit(metric: IMetric) { dispatchMetric(metric, { withStat() { // Nothing }, withExpression(expr) { for (const [id, subMetric] of Object.entries(expr.usingMetrics)) { const existing = seen.get(id); if (existing && metricKey(existing) !== metricKey(subMetric)) { throw new cdk.UnscopedValidationError(`The ID '${id}' used for two metrics in the expression: '${subMetric}' and '${existing}'. Rename one.`); } seen.set(id, subMetric); visit(subMetric); } }, }); } } } /** * Pattern for a variable name. Alphanum starting with lowercase. */ const VARIABLE_PAT = '[a-z][a-zA-Z0-9_]*'; const VALID_VARIABLE = new RegExp(`^${VARIABLE_PAT}$`); const FIND_VARIABLE = new RegExp(VARIABLE_PAT, 'g'); function validVariableName(x: string) { return VALID_VARIABLE.test(x); } /** * Return all variable names used in an expression */ function allIdentifiersInExpression(x: string) { return Array.from(matchAll(x, FIND_VARIABLE)).map(m => m[0]); } /** * Properties needed to make an alarm from a metric */ export interface CreateAlarmOptions { /** * The period over which the specified statistic is applied. * * Cannot be used with `MathExpression` objects. * * @default - The period from the metric * @deprecated Use `metric.with({ period: ... })` to encode the period into the Metric object */ readonly period?: cdk.Duration; /** * What function to use for aggregating. * * Can be one of the following: * * - "Minimum" | "min" * - "Maximum" | "max" * - "Average" | "avg" * - "Sum" | "sum" * - "SampleCount | "n" * - "pNN.NN" * * Cannot be used with `MathExpression` objects. * * @default - The statistic from the metric * @deprecated Use `metric.with({ statistic: ... })` to encode the period into the Metric object */ readonly statistic?: string; /** * Name of the alarm * * @default Automatically generated name */ readonly alarmName?: string; /** * Description for the alarm * * @default No description */ readonly alarmDescription?: string; /** * Comparison to use to check if metric is breaching * * @default GreaterThanOrEqualToThreshold */ readonly comparisonOperator?: ComparisonOperator; /** * The value against which the specified statistic is compared. */ readonly threshold: number; /** * The number of periods over which data is compared to the specified threshold. */ readonly evaluationPeriods: number; /** * Specifies whether to evaluate the data and potentially change the alarm state if there are too few data points to be statistically significant. * * Used only for alarms that are based on percentiles. * * @default - Not configured. */ readonly evaluateLowSampleCountPercentile?: string; /** * Sets how this alarm is to handle missing data points. * * @default TreatMissingData.Missing */ readonly treatMissingData?: TreatMissingData; /** * Whether the actions for this alarm are enabled * * @default true */ readonly actionsEnabled?: boolean; /** * The number of datapoints that must be breaching to trigger the alarm. This is used only if you are setting an "M * out of N" alarm. In that case, this value is the M. For more information, see Evaluating an Alarm in the Amazon * CloudWatch User Guide. * * @default ``evaluationPeriods`` * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarm-evaluation */ readonly datapointsToAlarm?: number; } function ifUndefined<T>(x: T | undefined, def: T | undefined): T | undefined { if (x !== undefined) { return x; } return def; } /** * Change periods of all metrics in the map */ function changeAllPeriods(metrics: Record<string, IMetric>, period: cdk.Duration): { record: Record<string, IMetric>; overridden: boolean } { const retRecord: Record<string, IMetric> = {}; let retOverridden = false; for (const [id, m] of Object.entries(metrics)) { const { metric, overridden } = changePeriod(m, period); retRecord[id] = metric; if (overridden) { retOverridden = true; } } return { record: retRecord, overridden: retOverridden }; } /** * Return a new metric object which is the same type as the input object but with the period changed, * and a flag to indicate whether the period has been overwritten. * * Relies on the fact that implementations of `IMetric` are also supposed to have * an implementation of `with` that accepts an argument called `period`. See `IModifiableMetric`. */ function changePeriod(metric: IMetric, period: cdk.Duration): { metric: IMetric; overridden: boolean} { if (isModifiableMetric(metric)) { const overridden = isMetricWithPeriod(metric) && // always true, as the period property is set with a default value even if it is not specified metric.period.toSeconds() !== cdk.Duration.minutes(5).toSeconds() && // exclude the default value of a metric, assuming the user has not specified it metric.period.toSeconds() !== period.toSeconds(); return { metric: metric.with({ period }), overridden }; } throw new cdk.UnscopedValidationError(`Metric object should also implement 'with': ${metric}`); } /** * Private protocol for metrics * * Metric types used in a MathExpression need to implement at least this: * a `with` method that takes at least a `period` and returns a modified copy * of the metric object. * * We put it here instead of on `IMetric` because there is no way to type * it in jsii in a way that concrete implementations `Metric` and `MathExpression` * can be statically typable about the fields that are changeable: all * `with` methods would need to take the same argument type, but not all * classes have the same `with`-able properties. * * This class exists to prevent having to use `instanceof` in the `changePeriod` * function, so that we have a system where in principle new implementations * of `IMetric` can be added. Because it will be rare, the mechanism doesn't have * to be exposed very well, just has to be possible. */ interface IModifiableMetric { with(options: { period?: cdk.Duration }): IMetric; } function isModifiableMetric(m: any): m is IModifiableMetric { return typeof m === 'object' && m !== null && !!m.with; } interface IMetricWithPeriod { period: cdk.Duration; } function isMetricWithPeriod(m: any): m is IMetricWithPeriod { return typeof m === 'object' && m !== null && !!m.period; } // Polyfill for string.matchAll(regexp) function matchAll(x: string, re: RegExp): RegExpMatchArray[] { const ret = new Array<RegExpMatchArray>(); let m: RegExpExecArray | null; while (m = re.exec(x)) { ret.push(m); } return ret; }