diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/README.md b/packages/@aws-cdk/aws-applicationsignals-alpha/README.md index a1791377be61f..952ea89d9aacd 100644 --- a/packages/@aws-cdk/aws-applicationsignals-alpha/README.md +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/README.md @@ -287,3 +287,91 @@ class MyStack extends cdk.Stack { } } ``` + +## Application Signals SLO L2 Constructs + +A collection of L2 constructs which leverages native L1 CFN resources, simplifying Application Signals Service Level +Objectives (SLOs) creation process to monitor the reliability of a service against customer expectations. + + +### ServiceLevelObjective + +`ServiceLevelObjective` aims to address key challenges in the current CDK process of creating and managing SLOs while providing the flexibility. +The construct provides two types of SLOs:
+Period-based SLOs: Evaluate performance against goals using defined time periods.
+Request-based SLOs: Measure performance based on request success ratios + +Key Features: + +1. Easy creation of both period-based and request-based SLOs. +2. Support for custom CloudWatch metrics and math expressions. +3. Automatic error budget calculation and tracking. + +#### Use Case 1 - Create a Period-based SLO with custom metrics, default attainmentGoal: 99.9 and warningThreshold: 30 + +``` +const periodSlo = ServiceLevelObjective.periodBased(this, 'PeriodSLO', { + name: 'my-period-slo', + goal: { + interval: Interval.rolling({ + duration: 7, + unit: DurationUnit.DAY, + }), + }, + metric: { + metricThreshold: 100, + periodSeconds: 300, + statistic: 'Average', + metricDataQueries: [/* ... */], + }, +}); +``` + +#### Use Case 2 - Create a Period-based SLO with service/operation, attainmentGoal is 99.99 and warningThreshold is 50 + +``` +const availabilitySlo = ServiceLevelObjective.periodBased(this, 'ApiAvailabilitySlo', { + name: 'api-availability-slo', + description: 'API endpoint availability SLO', + goal: { + attainmentGoal: 99.99, + warningThreshold: 50, + interval: Interval.calendar({ + duration: 1, + unit: DurationUnit.MONTH, + // default startTime is now, + }), + }, + metric: { + metricThreshold: 99, + metricType: MetricType.AVAILABILITY, + operationName: 'OrderProcessing', + keyAttributes: KeyAttributes.service({ + name: 'MyService', + environment: 'Development', + }); + periodSeconds: 300, + statistic: 'Average', + }, +}); +``` + +#### Use Case 3 - Create request based SLO with custom metrics + +``` +const requestSlo = ServiceLevelObjective.requestBased(this, 'RequestSLO', { + name: 'my-request-slo', + goal: { + interval: Interval.calendar({ + duration: 30, + unit: DurationUnit.DAY, + startTime: 1, + }), + }, + metric: { + metricThreshold: 200, + goodCountMetrics: [/* ... */], + totalCountMetrics: [/* ... */], + }, +}); +``` diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/index.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/index.ts index a1a19a160235e..c39e5fdd288e5 100644 --- a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/index.ts @@ -4,3 +4,5 @@ export * from './enablement/constants'; export * from './enablement/instrumentation-versions'; export * from './enablement/ecs-cloudwatch-agent'; export * from './enablement/ecs-sdk-instrumentation'; +export * from './slo/slo'; +export * from './slo/slo-types'; diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/constants.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/constants.ts new file mode 100644 index 0000000000000..40957b29ae7d6 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/constants.ts @@ -0,0 +1,162 @@ +/** + * Types of metrics that can be used for SLIs + */ +export enum MetricType { + /** + * Latency-based metric type + * Used for measuring response time or duration + */ + LATENCY = 'LATENCY', + + /** + * Availability-based metric type + * Used for measuring uptime or success rate + */ + AVAILABILITY = 'AVAILABILITY' +} + +/** + * Default values for goal configuration + */ +export const DEFAULT_GOAL_CONFIG = { + ATTAINMENT_GOAL: 99.9, + WARNING_THRESHOLD: 30, +} as const; + +/** + * Comparison operators for metric thresholds + */ +export enum ComparisonOperator { + /** + * Greater than operator + * True if metric value is strictly greater than threshold + */ + GREATER_THAN = 'GREATER_THAN', + + /** + * Less than operator + * True if metric value is strictly less than threshold + */ + LESS_THAN = 'LESS_THAN', + + /** + * Greater than or equal operator + * True if metric value is greater than or equal to threshold + */ + GREATER_THAN_OR_EQUAL = 'GREATER_THAN_OR_EQUAL', + + /** + * Less than or equal operator + * True if metric value is less than or equal to threshold + */ + LESS_THAN_OR_EQUAL = 'LESS_THAN_OR_EQUAL' +} + +/** + * Statistical methods for aggregating metric values + */ +export enum MetricStatistic { + /** + * Average of all values in the period + */ + AVERAGE = 'Average', + + /** + * Sum of all values in the period + */ + SUM = 'Sum', + + /** + * Minimum value in the period + */ + MINIMUM = 'Minimum', + + /** + * Maximum value in the period + */ + MAXIMUM = 'Maximum', + + /** + * Count of samples in the period + */ + SAMPLE_COUNT = 'SampleCount', + + /** + * 99th percentile of values in the period + */ + P99 = 'p99', + + /** + * 95th percentile of values in the period + */ + P95 = 'p95', + + /** + * 90th percentile of values in the period + */ + P90 = 'p90', + + /** + * 50th percentile (median) of values in the period + */ + P50 = 'p50' +} + +/** + * Types of services that can be monitored + */ +export enum KeyAttributeType { + /** + * Service running in your account + */ + SERVICE = 'SERVICE', + + /** + * AWS managed service + */ + AWS_SERVICE = 'AWS_SERVICE', + + /** + * External service + */ + REMOTE_SERVICE = 'REMOTE_SERVICE', + + /** + * Resource + */ + + RESOURCE = 'RESOURCE', + + /** + * AWS managed Resource + */ + + AWS_RESOURCE = 'AWS::RESOURCE' + +} + +/** + * Units for duration measurement in SLO intervals + */ +export enum DurationUnit { + /** + * Minute unit for fine-grained intervals + */ + MINUTE = 'MINUTE', + + /** + * Hour unit for medium-term intervals + */ + HOUR = 'HOUR', + + /** + * Day unit for daily intervals + */ + DAY = 'DAY', + + /** + * Month unit for long-term intervals + * Used for calendar-aligned monitoring + */ + MONTH = 'MONTH' +} diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/interval.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/interval.ts new file mode 100644 index 0000000000000..c9f8f8bb36b85 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/interval.ts @@ -0,0 +1,152 @@ +import { CalendarIntervalProps, GoalConfig, IInterval, IntervalProps } from './slo'; +import { DEFAULT_GOAL_CONFIG, DurationUnit } from './constants'; + +/** + * Base class for Interval implementations + */ +export abstract class Interval implements IInterval { + /** + * Creates a calendar interval + */ + public static calendar(props: CalendarIntervalProps): IInterval { + return new CalendarInterval(props); + } + + /** + * Creates a rolling interval + */ + public static rolling(props: IntervalProps): IInterval { + return new RollingInterval(props); + } + + abstract bind(): any; +} + +/** + * Implementation of Calendar Interval + */ +export class CalendarInterval extends Interval { + /** + * The duration value for the interval + * Must be greater than 0 + * + * @private + */ + private readonly duration: number; + + /** + * The unit of duration measurement + * Can be MINUTE, HOUR, DAY, or MONTH + * + * @private + */ + private readonly unit: DurationUnit; + + /** + * The start time of the interval + * Specified as Unix timestamp in milliseconds + * Default starts from now + * + * @private + */ + private readonly startTime: number; + + constructor(props: CalendarIntervalProps) { + super(); + this.duration = props.duration; + this.unit = props.unit; + this.startTime = props.startTime?? Date.now(); + this.validate(); + } + + private validate() { + if (this.duration <= 0) { + throw new Error('Duration must be greater than 0'); + } + + if (this.unit === DurationUnit.MONTH && this.duration > 12) { + throw new Error('Month duration cannot exceed 12'); + } + + if (this.unit === DurationUnit.DAY && this.duration > 31) { + throw new Error('Day duration cannot exceed 31'); + } + + if (this.startTime <= 0) { + throw new Error('Start time must be greater than 0'); + } + } + + bind() { + return { + calendarInterval: { + duration: this.duration, + durationUnit: this.unit, + startTime: this.startTime, + }, + }; + } +} + +/** + * Implementation of Rolling Interval + */ +export class RollingInterval extends Interval { + private readonly duration: number; + private readonly unit: DurationUnit; + + constructor(props: IntervalProps) { + super(); + this.duration = props.duration; + this.unit = props.unit; + this.validate(); + } + + private validate() { + if (this.duration <= 0) { + throw new Error('Duration must be greater than 0'); + } + + if (this.unit === DurationUnit.MONTH && this.duration > 12) { + throw new Error('Month duration cannot exceed 12'); + } + + if (this.unit === DurationUnit.DAY && this.duration > 31) { + throw new Error('Day duration cannot exceed 31'); + } + } + + bind() { + return { + rollingInterval: { + duration: this.duration, + durationUnit: this.unit, + }, + }; + } +} + +/** + * Implementation of goal configuration + */ +export class Goal { + /** + * Creates a new goal configuration + */ + public static of(props: GoalConfig): Goal { + return new Goal(props); + } + + private constructor(private readonly props: GoalConfig) {} + + /** + * Binds the goal configuration to L1 construct properties + */ + public _bind(): applicationsignals.CfnServiceLevelObjective.GoalProperty { + return { + attainmentGoal: this.props.attainmentGoal ?? DEFAULT_GOAL_CONFIG.ATTAINMENT_GOAL, + warningThreshold: this.props.warningThreshold ?? DEFAULT_GOAL_CONFIG.WARNING_THRESHOLD, + interval: this.props.interval._bind(), + }; + } +} diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/keyAttributes.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/keyAttributes.ts new file mode 100644 index 0000000000000..4c456e88dcd88 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/keyAttributes.ts @@ -0,0 +1,104 @@ +import { KeyAttributeType } from './constants'; +import { KeyAttributesProps } from './slo'; + +/** + * Class representing Application Signals key attributes with validation + */ +export class KeyAttributes { + private readonly props: KeyAttributesProps; + + constructor(props: KeyAttributesProps) { + this.validateProps(props); + this.props = props; + } + + private validateProps(props: KeyAttributesProps) { + if (!props.type) { + throw new Error('Type is required for Application Signals service'); + } + + if ([KeyAttributeType.SERVICE, KeyAttributeType.REMOTE_SERVICE, KeyAttributeType.AWS_SERVICE].includes(props.type)) { + if (!props.name || !props.environment) { + throw new Error('Name and Environment are required for Service types'); + } + } + + if ([KeyAttributeType.RESOURCE, KeyAttributeType.AWS_RESOURCE].includes(props.type)) { + if (props.name) { + throw new Error('Name is not allowed for Resource types'); + } + if (props.resourceType === undefined) { + throw new Error('ResourceType is required for Resource types'); + } + } + } + + + public bind(): { [key: string]: string } { + const attributes: { [key: string]: string } = { + Type: this.props.type, + Name: this.props.name, + Environment: this.props.environment, + }; + + if (this.props.resourceType) { + attributes.ResourceType = this.props.resourceType; + } + + if (this.props.identifier) { + attributes.Identifier = this.props.identifier; + } + + return attributes; + } + + /** + * Creates key attributes for an AWS service + */ + public static awsService(props: Omit): KeyAttributes { + return new KeyAttributes({ + type: KeyAttributeType.AWS_SERVICE, + ...props, + }); + } + + /** + * Creates key attributes for a custom service + */ + public static service(props: Omit): KeyAttributes { + return new KeyAttributes({ + type: KeyAttributeType.SERVICE, + ...props, + }); + } + + /** + * Creates key attributes for a remote service + */ + public static remoteService(props: Omit): KeyAttributes { + return new KeyAttributes({ + type: KeyAttributeType.REMOTE_SERVICE, + ...props, + }); + } + + /** + * Creates key attributes for a resource + */ + public static resource(props: Omit): KeyAttributes { + return new KeyAttributes({ + type: KeyAttributeType.RESOURCE, + ...props, + }); + } + + /** + * Creates key attributes for an AWS resource + */ + public static awsResource(props: Omit): KeyAttributes { + return new KeyAttributes({ + type: KeyAttributeType.AWS_RESOURCE, + ...props, + }); + } +} diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/metric.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/metric.ts new file mode 100644 index 0000000000000..0d5ab3f1a0f62 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/metric.ts @@ -0,0 +1,357 @@ +import { ComparisonOperator, MetricStatistic, MetricType } from './constants'; +import { KeyAttributes } from './keyAttributes'; + +/** + * Interface for metric dimension + */ +export interface MetricDimension { + /** + * Name of the dimension + * + * @required + */ + readonly name: string; + + /** + * Value of the dimension + * + * @required + */ + readonly value: string; +} + +/** + * Properties for Application Signals metrics + */ +export interface ApplicationSignalsMetricProps { + /** + * Type of the metric + * + * @required + */ + readonly metricType: MetricType; + + /** + * Key attributes for the service + * + * @required + */ + readonly keyAttributes: KeyAttributes; + + /** + * Operation name + * + * @default - undefined + */ + readonly operationName?: string; + + /** + * Period in seconds + * Required for period-based SLOs + * + * @required + */ + readonly periodSeconds: number; + + /** + * Statistic to use + * Required for period-based SLOs + * + * @required + */ + readonly statistic: string; +} + +/** + * Properties for CloudWatch metrics + */ +export interface CloudWatchMetricProps { + /** + * Metric data queries + * + * @required + */ + readonly metricDataQueries: MetricDataQuery[]; + + /** + * Period in seconds + * Required for period-based SLOs + * + * @required + */ + readonly periodSeconds: number; + + /** + * Statistic to use + * Required for period-based SLOs + * + * @required + */ + readonly statistic: string; +} + +/** + * Properties for SLI metric configuration + */ +export type SliMetricBaseProps = { + /** + * The threshold value for the metric + * + * @required + */ + readonly metricThreshold: number; + + /** + * The comparison operator + * + * @default - Based on metric type + */ + readonly comparisonOperator?: ComparisonOperator; +} & (ApplicationSignalsMetricProps | CloudWatchMetricProps); + + +/** + * Properties for a metric statistic + */ +export interface MetricStat { + /** + * The metric to query + * + * @required + */ + readonly metric: MetricDefinition; + + /** + * The period in seconds + * Must be a multiple of 60 + * + * @default 60 + */ + readonly period: number; + + /** + * The statistic to use + * + * @required + */ + readonly stat: string; + + /** + * The unit of the metric + * + * @default - no unit + */ + readonly unit?: string; +} + +/** + * Interface for metric definition + */ +export interface MetricDefinition { + /** + * Name of the metric + * + * @required + */ + readonly metricName: string; + + /** + * Namespace of the metric + * + * @required + */ + readonly namespace: string; + + /** + * Optional dimensions for the metric + * + * @default - no dimensions + */ + readonly dimensions?: MetricDimension[]; +} + +/** + * Properties for a CloudWatch metric data query + * Used to define how metrics should be queried and processed + */ +export interface MetricDataQuery { + /** + * Unique identifier for the query + * Used to reference this query in math expressions + * Must be unique within the set of queries + * + * @required + */ + readonly id: string; + + /** + * AWS account ID where the metric is located + * + * @default - current account + */ + readonly accountId?: string; + + /** + * The math expression + * Cannot be specified if metricStat is specified + * + * @default - undefined + */ + readonly expression?: string; + + /** + * The metric statistic configuration + * Cannot be specified if expression is specified + * + * @default - undefined + */ + readonly metricStat?: MetricStat; + + /** + * Whether this query should return data to be used in results + * Set to true for queries that produce final values + * Set to false for intermediate calculations + * + * @default false + */ + readonly returnData?: boolean; +} + +/** + * Period-based metric properties with Application Signals + */ +export interface PeriodBasedAppSignalsMetricProps extends SliMetricBaseProps { + /** + * The type of metric being measured + * Can be LATENCY or AVAILABILITY + * + * @required + */ + readonly metricType: MetricType; + + /** + * Key attributes for the service being monitored + * Must include at least one of Type, Name, and Environment + * + * @required + */ + readonly keyAttributes: { [key: string]: string }; + + /** + * The name of the operation being measured + * Used to filter metrics for specific operation + * + */ + readonly operationName?: string; + + /** + * The period in seconds for metric aggregation + * Must be a multiple of 60 + * + * @required + */ + readonly periodSeconds: number; + + /** + * The statistic to use for aggregation + * Examples: Average, Sum, p99 + * + * @required + */ + readonly statistic: MetricStatistic; +} + +/** + * Period-based metric properties with CloudWatch metrics + */ +export interface PeriodBasedCloudWatchMetricProps extends SliMetricBaseProps { + /** + * The metric data queries to execute + * Can include raw metrics and math expressions + * + * @required + */ + readonly metricDataQueries: MetricDataQuery[]; + + /** + * The period in seconds for metric aggregation + * Must be a multiple of 60 + * + * @required + */ + readonly periodSeconds: number; + + /** + * The statistic to use for aggregation + * Examples: Average, Sum, p99 + * + * @required + */ + readonly statistic: MetricStatistic; +} + +/** + * Request-based metric properties with Application Signals + */ +export interface RequestBasedAppSignalsMetricProps extends SliMetricBaseProps { + /** + * The type of metric being measured + * Can be LATENCY or AVAILABILITY + * + * @required + */ + readonly metricType: MetricType; + + /** + * Key attributes for the applications being monitored + * Must include at least one of Type, Name, and Environment + * + * @required + */ + readonly keyAttributes: { [key: string]: string }; + + /** + * The name of the operation being measured + * + */ + readonly operationName?: string; +} + +/** + * Request-based metric properties with CloudWatch metrics + */ +export interface RequestBasedCloudWatchMetricProps extends SliMetricBaseProps { + /** + * Metrics that count successful requests + * Optional if can be derived from total - bad + * Used to calculate success rate + * + * @required + */ + readonly goodCountMetrics: MetricDataQuery[]; + + /** + * Metrics that count total requests + * Used as denominator for success rate + * + * @required + */ + readonly totalCountMetrics: MetricDataQuery[]; + + /** + * Metrics that count failed requests + * Optional if can be derived from total - good + * + */ + readonly badCountMetrics?: MetricDataQuery[]; +} + +/** + * Period-based metric properties + */ +export type PeriodBasedMetricProps = PeriodBasedAppSignalsMetricProps | PeriodBasedCloudWatchMetricProps; + +/** + * Request-based metric properties + */ +export type RequestBasedMetricProps = RequestBasedAppSignalsMetricProps | RequestBasedCloudWatchMetricProps; diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/slo-types.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/slo-types.ts new file mode 100644 index 0000000000000..ddb728320d0be --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/slo-types.ts @@ -0,0 +1,138 @@ +import { ISlo, PeriodBasedSloProps, RequestBasedSloProps } from './slo'; +import { ComparisonOperator, MetricType } from './constants'; +import { Goal } from './interval'; +import { PeriodBasedAppSignalsMetricProps, PeriodBasedCloudWatchMetricProps, RequestBasedAppSignalsMetricProps, RequestBasedCloudWatchMetricProps } from './metric'; +import { Resource } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as applicationsignals from 'aws-cdk-lib/aws-applicationsginals'; + +/** + * Base class for all SLO implementations + */ +export abstract class ServiceLevelObjective extends Resource implements ISlo { + public readonly arn: string; + public readonly name: string; + public readonly goal: Goal; + + protected constructor( + scope: Construct, + id: string, + protected readonly props: PeriodBasedSloProps | RequestBasedSloProps + ) { + super(scope, id); + this.name = props.name; + this.goal = props.goal; + } + + public static periodBased( + scope: Construct, + id: string, + props: PeriodBasedSloProps + ): ServiceLevelObjective { + return new PeriodBasedSlo(scope, id, props); + } + + public static requestBased( + scope: Construct, + id: string, + props: RequestBasedSloProps + ): ServiceLevelObjective { + return new RequestBasedSlo(scope, id, props); + } + + protected createBurnRateConfigurations(): applicationsignals.CfnServiceLevelObjective.BurnRateConfigurationProperty[] | undefined { + return this.props.burnRateWindows?.map(minutes => ({ + lookBackWindowMinutes: minutes, + })); + } + + protected getMetricComparisonOperator(metric: PeriodBasedAppSignalsMetricProps | PeriodBasedCloudWatchMetricProps | RequestBasedAppSignalsMetricProps | RequestBasedCloudWatchMetricProps): ComparisonOperator { + return metric.comparisonOperator ?? this.getDefaultComparisonOperator(metric.metricType); + } + + protected getPeriod(periodSeconds?: number): number { + return periodSeconds ?? 60; + } + + protected isAppSignalsMetric(metric: PeriodBasedAppSignalsMetricProps | PeriodBasedCloudWatchMetricProps | RequestBasedAppSignalsMetricProps | RequestBasedCloudWatchMetricProps): boolean { + return 'metricType' in metric && 'keyAttributes' in metric; + } + + protected getDefaultComparisonOperator(metricType?: MetricType): ComparisonOperator { + switch (metricType) { + case MetricType.LATENCY: + return ComparisonOperator.LESS_THAN_OR_EQUAL; + case MetricType.AVAILABILITY: + return ComparisonOperator.GREATER_THAN_OR_EQUAL; + default: + throw new Error('ComparisonOperator must be specified when metricType is not provided'); + } + } + abstract _bind(): applicationsignals.CfnServiceLevelObjectiveProps; +} + +/** + * Period-based slo + */ +export class PeriodBasedSlo extends ServiceLevelObjective { + constructor(scope: Construct, id: string, props: PeriodBasedSloProps) { + super(scope, id, props); + } + + _bind(): applicationsignals.CfnServiceLevelObjectiveProps { + const metric = this.props.metric; + return { + name: this.name, + description: this.props.description, + goal: this.goal._bind(), + burnRateConfigurations: this.createBurnRateConfigurations(), + sli: { + sliMetric: this.isAppSignalsMetric(metric) ? { + metricType: metric.metricType, + keyAttributes: metric.keyAttributes, + operationName: metric.operationName, + periodSeconds: this.getPeriod(metric.periodSeconds), + statistic: metric.statistic, + } : { + metricDataQueries: metric.metricDataQueries, + periodSeconds: this.getPeriod(metric.periodSeconds), + statistic: metric.statistic, + }, + comparisonOperator: this.getMetricComparisonOperator(metric), + metricThreshold: metric.metricThreshold, + }, + }; + } +} + +/** + * Request-based slo + */ +export class RequestBasedSlo extends ServiceLevelObjective { + constructor(scope: Construct, id: string, props: RequestBasedSloProps) { + super(scope, id, props); + } + + _bind(): applicationsignals.CfnServiceLevelObjectiveProps { + const metric = this.props.metric; + return { + name: this.name, + description: this.props.description, + goal: this.goal._bind(), + burnRateConfigurations: this.createBurnRateConfigurations(), + requestBasedSli: { + requestBasedSliMetric: this.isAppSignalsMetric(metric) ? { + metricType: metric.metricType, + keyAttributes: metric.keyAttributes, + operationName: metric.operationName, + } : { + goodCountMetric: (metric as RequestBasedCloudWatchMetricProps).goodCountMetrics, + totalRequestCountMetric: (metric as RequestBasedCloudWatchMetricProps).totalCountMetrics, + badCountMetric: (metric as RequestBasedCloudWatchMetricProps).badCountMetrics, + }, + comparisonOperator: this.getMetricComparisonOperator(metric), + metricThreshold: metric.metricThreshold, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/slo.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/slo.ts new file mode 100644 index 0000000000000..76a5c6b91f9df --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/lib/slo/slo.ts @@ -0,0 +1,224 @@ +import { DurationUnit, KeyAttributeType } from './constants'; +import { PeriodBasedMetricProps, RequestBasedMetricProps } from './metric'; +import { Goal } from './interval'; + +/** + * Interface defining interval behavior + */ +export interface IInterval { + bind(): any; +} + +/** + * Interface for interval properties + */ +export interface IntervalProps { + /** + * Duration value for the interval + * Must be greater than 0 + * + * @required + */ + readonly duration: number; + + /** + * Unit of duration + * Can be MINUTE, HOUR, DAY, or MONTH + * + * @required + */ + readonly unit: DurationUnit; +} + +/** + * Interface for calendar interval properties + */ +export interface CalendarIntervalProps { + /** + * Duration value for the calendar interval + * Must be greater than 0 + * + * @required + */ + readonly duration: number; + + /** + * Start time for the calendar interval + * + * @default - current timestamp + */ + readonly startTime?: number; + + /** + * Unit of duration + * Default is MONTH + * + */ + readonly unit: DurationUnit; +} + +/** + * Interface for goal configuration + */ +export interface GoalConfig { + /** + * The target goal percentage + * Must be between 0 and 100 + * + * @default 99.9 + */ + readonly attainmentGoal?: number; + + /** + * Warning threshold percentage + * Must be between 0 and 100 + * + * @default 30 + */ + readonly warningThreshold?: number; + + /** + * Interval configuration for the goal + * + * @required + */ + readonly interval: IInterval; +} + +/** + * Interface for key attributes + */ +export interface KeyAttributesProps { + /** + * The type of service + * Can be SERVICE, AWS_SERVICE, or REMOTE_SERVICE + * + * @required + */ + readonly type: KeyAttributeType; + + /** + * The name of the service + * + * @required + */ + readonly name: string; + + /** + * The environment of the service + * + * @required + */ + readonly environment: string; + + /** + * Optional additional identifier for the service + * + * @default - no identifier + */ + readonly identifier?: string; + + readonly resourceType?: string; + +} + +/** + * Interface defining SLO behavior and runtime properties + */ +export interface ISlo { + /** + * The ARN (Amazon Resource Name) of the SLO + * Generated by AWS when the SLO is created + * Used to reference this SLO in other AWS resources + * + * @required + */ + readonly arn: string; + + /** + * The name of the SLO + * Matches the name provided in the properties + * + * @required + */ + readonly name: string; + + /** + * The goal configuration of the SLO + * Contains the configured targets and time window + * + * @required + */ + readonly goal: Goal; + + /** + * Binds the SLO configuration to L1 construct properties + * Used internally by CDK to generate CloudFormation + * + * @returns The L1 construct properties + */ + _bind(): any; +} + +/** +* Base interface for all SLO properties +*/ +export interface SloBaseProps { + /** + * The name of the SLO + * Must be unique within the account/region + * + * @required + */ + readonly name: string; + + /** + * A description of the SLO's purpose + * + * @default - no description + */ + readonly description?: string; + + /** + * The goal configuration for the SLO + * Includes attainment target and time window + * Defines what "good" looks like for this SLO + * + * @required + */ + readonly goal: Goal; + + /** + * The burn rate windows in minutes + * Used to calculate error budget consumption + * Maximum of 10 windows + * Each window cannot exceed 7 days (10080 minutes) + * + * @default - no burn rate windows + */ + readonly burnRateWindows?: number[]; +} + +/** + * Properties for period-based SLO configuration + */ +export interface PeriodBasedSloProps extends SloBaseProps { + /** + * The Period Based Slo metric configuration + * + * @required + */ + readonly metric: PeriodBasedMetricProps; +} + +/** + * Properties for request-based SLO configuration + */ +export interface RequestBasedSloProps extends SloBaseProps{ + /** + * The Request Based Slo metric configuration + * + * @required + */ + readonly metric: RequestBasedMetricProps; +} diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/test/slo/interval.test.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/test/slo/interval.test.ts new file mode 100644 index 0000000000000..d7a1c2352eeb2 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/test/slo/interval.test.ts @@ -0,0 +1,134 @@ +import {Interval} from '../../lib/slo/interval' +import {CalendarIntervalProps, IntervalProps} from '../../lib'; +import {DurationUnit} from "../../lib/slo/constants"; + +describe('Interval', () => { + describe('calendar()', () => { + it('should create a calendar interval with valid props', () => { + const props: CalendarIntervalProps = { + unit: DurationUnit.MONTH, + duration: 1 + }; + + const interval = Interval.calendar(props); + expect(interval).toBeDefined(); + expect(interval.bind).toBeDefined(); + }); + + it('should create a calendar interval with different time units', () => { + const units = [ + DurationUnit.DAY, + DurationUnit.MONTH + ]; + + units.forEach(unit => { + const props: CalendarIntervalProps = { + unit: unit, + duration: 1 + }; + const interval = Interval.calendar(props); + expect(interval).toBeDefined(); + }); + }); + + it('should handle different duration values', () => { + const durations = [1, 7, 14, 30]; + + durations.forEach(duration => { + const props: CalendarIntervalProps = { + unit: DurationUnit.DAY, + duration + }; + const interval = Interval.calendar(props); + expect(interval).toBeDefined(); + }); + }); + }); + + describe('rolling()', () => { + it('should create a rolling interval with valid props', () => { + const props: IntervalProps = { + duration: 24, + unit: DurationUnit.HOUR + }; + + const interval = Interval.rolling(props); + expect(interval).toBeDefined(); + expect(interval.bind).toBeDefined(); + }); + + it('should handle different duration values', () => { + const durations = [1, 7, 14, 31]; + + durations.forEach(duration => { + const props: CalendarIntervalProps = { + unit: DurationUnit.DAY, + duration + }; + const interval = Interval.calendar(props); + expect(interval).toBeDefined(); + }); + }); + }); + + describe('bind()', () => { + it('should return the correct structure for calendar intervals', () => { + const props: CalendarIntervalProps = { + unit: DurationUnit.MONTH, + duration: 1 + }; + + const interval = Interval.calendar(props); + const bound = interval.bind(); + expect(bound).toBeDefined(); + + }); + + it('should return the correct structure for rolling intervals', () => { + const props: IntervalProps = { + duration: 24, + unit: DurationUnit.HOUR + }; + + const interval = Interval.rolling(props); + const bound = interval.bind(); + expect(bound).toBeDefined(); + }); + }); + + describe('error cases', () => { + it('should handle invalid calendar interval props', () => { + expect(() => { + Interval.calendar({ + unit: DurationUnit.DAY, + duration: -1 + }).bind() + }).toThrow(); + }); + + it('should handle invalid rolling interval props', () => { + expect(() => { + Interval.rolling({ + duration: -1, + unit: DurationUnit.HOUR + } as any).bind() + }).toThrow(); + }); + + it('should handle missing required properties', () => { + expect(() => { + Interval.calendar({ + unit: DurationUnit.DAY, + duration: 0 + } as CalendarIntervalProps).bind(); + }).toThrow('Duration must be greater than 0'); + + expect(() => { + Interval.rolling({ + unit: DurationUnit.HOUR, + duration: 0 + } as IntervalProps).bind(); + }).toThrow('Duration must be greater than 0'); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-applicationsignals-alpha/test/slo/keyAttributes.test.ts b/packages/@aws-cdk/aws-applicationsignals-alpha/test/slo/keyAttributes.test.ts new file mode 100644 index 0000000000000..3d0eb1178a8f0 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationsignals-alpha/test/slo/keyAttributes.test.ts @@ -0,0 +1,137 @@ +import { KeyAttributes } from '../../lib/slo/keyAttributes'; +import { KeyAttributeType } from '../../lib/slo/constants'; +import { KeyAttributesProps } from "../../lib"; + +describe('KeyAttributes', () => { + describe('constructor validation', () => { + test('throws error when type is missing', () => { + expect(() => { + new KeyAttributes({} as KeyAttributesProps); + }).toThrow('Type is required for Application Signals service'); + }); + + describe('Service type validation', () => { + const serviceTypes = [ + KeyAttributeType.SERVICE, + KeyAttributeType.REMOTE_SERVICE, + KeyAttributeType.AWS_SERVICE, + ]; + + serviceTypes.forEach(type => { + test(`requires name and environment for ${type}`, () => { + expect(() => { + new KeyAttributes({ + type, + name: undefined, + environment: undefined + } as any); + }).toThrow('Name and Environment are required for Service types'); + + expect(() => { + new KeyAttributes({ + type, + name: 'test', + environment: undefined + } as any); + }).toThrow('Name and Environment are required for Service types'); + + expect(() => { + new KeyAttributes({ + type, + name: undefined, + environment: 'prod' + } as any); + }).toThrow('Name and Environment are required for Service types'); + }); + + test(`accepts valid configuration for ${type}`, () => { + const props: KeyAttributesProps = { + type, + name: 'test', + environment: 'prod' + }; + expect(() => { + new KeyAttributes(props); + }).not.toThrow(); + }); + }); + }); + + describe('Resource type validation', () => { + const resourceTypes = [ + KeyAttributeType.RESOURCE, + KeyAttributeType.AWS_RESOURCE, + ]; + + resourceTypes.forEach(type => { + test(`validates ${type} requirements`, () => { + expect(() => { + new KeyAttributes({ + type, + name: 'test', + resourceType: 'someResource', + environment: 'prod' + } as any); + }).toThrow('Name is not allowed for Resource types'); + + expect(() => { + new KeyAttributes({ + type, + resourceType: undefined + } as any); + }).toThrow('ResourceType is required for Resource types'); + }); + + test(`accepts valid configuration for ${type}`, () => { + const props = { + type, + resourceType: 'someResource' + } as KeyAttributesProps; + expect(() => { + new KeyAttributes(props); + }).not.toThrow(); + }); + }); + }); + }); + + describe('bind method', () => { + test('returns correct attributes for service type', () => { + const keyAttributes = new KeyAttributes({ + type: KeyAttributeType.SERVICE, + name: 'testService', + environment: 'beta' + }); + expect(keyAttributes.bind()).toEqual({ + Type: KeyAttributeType.SERVICE, + Name: 'testService', + Environment: 'beta' + }); + }); + + test('returns correct attributes for resource type', () => { + const keyAttributes = new KeyAttributes({ + type: KeyAttributeType.RESOURCE, + resourceType: 'database' + } as KeyAttributesProps); + expect(keyAttributes.bind()).toEqual({ + Type: KeyAttributeType.RESOURCE, + ResourceType: 'database' + }); + }); + }); + + describe('static factory methods', () => { + test('service creates correct configuration', () => { + const keyAttributes = KeyAttributes.service({ + name: 'testService', + environment: 'production' + }); + expect(keyAttributes.bind()).toEqual({ + Type: KeyAttributeType.SERVICE, + Name: 'testService', + Environment: 'production' + }); + }); + }); +});