Skip to content

Commit 27ae408

Browse files
author
Eugene Cheung
authored
feat(opensearchserverless): add monitoring for Collections and Indices (#660)
Closes #335 Examples: <img width="1656" height="528" alt="Screenshot 2025-08-14 at 11 50 30" src="https://github.com/user-attachments/assets/c3dac59d-db96-4bcc-8cd9-a670289ae2c8" /> <img width="1654" height="290" alt="Screenshot 2025-08-14 at 11 50 39" src="https://github.com/user-attachments/assets/dd824f03-e5d6-4f33-a357-52631768e6c1" /> --- _By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license_
1 parent f9b64e6 commit 27ae408

15 files changed

+15799
-12276
lines changed

API.md

Lines changed: 13818 additions & 12276 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ You can browse the documentation at https://constructs.dev/packages/cdk-monitori
8585
| AWS Load Balancing (`.monitorNetworkLoadBalancer()`, `.monitorFargateApplicationLoadBalancer()`, `.monitorFargateNetworkLoadBalancer()`, `.monitorEc2ApplicationLoadBalancer()`, `.monitorEc2NetworkLoadBalancer()`) | System resources and task health | Unhealthy task count, running tasks count, (for Fargate/Ec2 apps) CPU/memory usage | Use for FargateService or Ec2Service backed by a NetworkLoadBalancer or ApplicationLoadBalancer |
8686
| AWS OpenSearch/Elasticsearch (`.monitorOpenSearchCluster()`, `.monitorElasticsearchCluster()`) | Indexing and search latency, disk/memory/CPU usage | Indexing and search latency, disk/memory/CPU usage, cluster status, KMS keys | |
8787
| AWS OpenSearch Ingestion (`.monitorOpenSearchIngestionPipeline()`) | Latency, incoming data, DLQ records count | DLQ records count | |
88+
| AWS OpenSearch Serverless (`.monitorOpenSearchServerlessCollection()`) | Search latency, errors, ingestion requests/latency | Search latency, errors | |
89+
| AWS OpenSearch Serverless (`.monitorOpenSearchServerlessIndex()`) | Documents count | | |
8890
| AWS RDS (`.monitorRdsCluster()`) | Query duration, connections, latency, disk/CPU usage | Connections, disk and CPU usage | |
8991
| AWS RDS (`.monitorRdsInstance()`) | Query duration, connections, latency, disk/CPU usage | Connections, disk and CPU usage | |
9092
| AWS Redshift (`.monitorRedshiftCluster()`) | Query duration, connections, latency, disk/CPU usage | Query duration, connections, disk and CPU usage | |

lib/common/url/AwsConsoleUrlFactory.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ export class AwsConsoleUrlFactory {
113113
return this.getAwsConsoleUrl(destinationUrl);
114114
}
115115

116+
getOpenSearchServerlessCollectionUrl(
117+
collectionName: string,
118+
): string | undefined {
119+
const region = this.awsAccountRegion;
120+
const destinationUrl = `https://${region}.console.aws.amazon.com/aos/home?region=${region}#opensearch/collections/${collectionName}`;
121+
return this.getAwsConsoleUrl(destinationUrl);
122+
}
123+
116124
getOsisPipelineUrl(pipelineName: string): string | undefined {
117125
const region = this.awsAccountRegion;
118126
const destinationUrl = `https://${region}.console.aws.amazon.com/aos/osis/home?region=${region}#osis/ingestion-pipelines/${pipelineName}`;

lib/facade/MonitoringFacade.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ import {
9393
OpenSearchClusterMonitoringProps,
9494
OpenSearchIngestionPipelineMonitoring,
9595
OpenSearchIngestionPipelineMonitoringProps,
96+
OpenSearchServerlessIndexMonitoring,
97+
OpenSearchServerlessIndexMonitoringProps,
98+
OpenSearchServerlessMonitoring,
99+
OpenSearchServerlessMonitoringProps,
96100
QueueProcessingEc2ServiceMonitoringProps,
97101
QueueProcessingFargateServiceMonitoringProps,
98102
RdsClusterMonitoring,
@@ -594,6 +598,22 @@ export class MonitoringFacade extends MonitoringScope {
594598
return this;
595599
}
596600

601+
monitorOpenSearchServerlessCollection(
602+
props: OpenSearchServerlessMonitoringProps,
603+
): this {
604+
const segment = new OpenSearchServerlessMonitoring(this, props);
605+
this.addSegment(segment, props);
606+
return this;
607+
}
608+
609+
monitorOpenSearchServerlessIndex(
610+
props: OpenSearchServerlessIndexMonitoringProps,
611+
): this {
612+
const segment = new OpenSearchServerlessIndexMonitoring(this, props);
613+
this.addSegment(segment, props);
614+
return this;
615+
}
616+
597617
monitorElastiCacheCluster(props: ElastiCacheClusterMonitoringProps): this {
598618
const segment = new ElastiCacheClusterMonitoring(this, props);
599619
this.addSegment(segment, props);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Stack } from "aws-cdk-lib";
2+
import type { DimensionsMap } from "aws-cdk-lib/aws-cloudwatch";
3+
import type { CfnCollection } from "aws-cdk-lib/aws-opensearchserverless";
4+
import {
5+
BaseMetricFactoryProps,
6+
BaseMetricFactory,
7+
MetricFactory,
8+
MetricWithAlarmSupport,
9+
MetricStatistic,
10+
} from "../../common";
11+
12+
const OpenSearchServerlessNamespace = "AWS/AOSS";
13+
14+
export interface OpenSearchServerlessIndexMetricFactoryProps
15+
extends BaseMetricFactoryProps {
16+
readonly collection: CfnCollection;
17+
readonly indexId: string;
18+
readonly indexName: string;
19+
}
20+
21+
/**
22+
* @experimental This is subject to change if an L2 construct becomes available.
23+
*
24+
* @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/monitoring-cloudwatch.html
25+
*/
26+
export class OpenSearchServerlessIndexMetricFactory extends BaseMetricFactory<OpenSearchServerlessIndexMetricFactoryProps> {
27+
protected readonly dimensionsMap: DimensionsMap;
28+
29+
constructor(
30+
metricFactory: MetricFactory,
31+
props: OpenSearchServerlessIndexMetricFactoryProps,
32+
) {
33+
super(metricFactory, props);
34+
35+
this.dimensionsMap = {
36+
ClientId: this.account ?? Stack.of(props.collection).account,
37+
CollectionId: props.collection.attrId,
38+
CollectionName: props.collection.name,
39+
IndexId: props.indexId,
40+
IndexName: props.indexName,
41+
};
42+
}
43+
44+
metricIndexSearchableDocuments(): MetricWithAlarmSupport {
45+
return this.metricFactory.createMetric(
46+
"SearchableDocuments",
47+
MetricStatistic.SUM,
48+
`SearchableDocuments: ${this.dimensionsMap.IndexName}`,
49+
this.dimensionsMap,
50+
undefined,
51+
OpenSearchServerlessNamespace,
52+
undefined,
53+
this.region,
54+
this.account,
55+
);
56+
}
57+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { IWidget } from "aws-cdk-lib/aws-cloudwatch";
2+
import { GraphWidget } from "aws-cdk-lib/aws-cloudwatch";
3+
4+
import type { OpenSearchServerlessIndexMetricFactoryProps } from "./OpenSearchServerlessIndexMetricFactory";
5+
import { OpenSearchServerlessIndexMetricFactory } from "./OpenSearchServerlessIndexMetricFactory";
6+
import {
7+
BaseMonitoringProps,
8+
MetricWithAlarmSupport,
9+
Monitoring,
10+
MonitoringScope,
11+
FullWidth,
12+
DefaultGraphWidgetHeight,
13+
CountAxisFromZero,
14+
} from "../../common";
15+
import {
16+
MonitoringNamingStrategy,
17+
MonitoringHeaderWidget,
18+
} from "../../dashboard";
19+
20+
export type OpenSearchServerlessIndexMonitoringOptions = BaseMonitoringProps;
21+
22+
export interface OpenSearchServerlessIndexMonitoringProps
23+
extends OpenSearchServerlessIndexMetricFactoryProps,
24+
OpenSearchServerlessIndexMonitoringOptions {}
25+
26+
/**
27+
* @experimental This is subject to change if an L2 construct becomes available.
28+
*/
29+
export class OpenSearchServerlessIndexMonitoring extends Monitoring {
30+
readonly title: string;
31+
32+
readonly metricIndexSearchableDocuments: MetricWithAlarmSupport;
33+
34+
constructor(
35+
scope: MonitoringScope,
36+
props: OpenSearchServerlessIndexMonitoringProps,
37+
) {
38+
super(scope, props);
39+
40+
const namingStrategy = new MonitoringNamingStrategy({
41+
...props,
42+
fallbackConstructName: props.indexName,
43+
});
44+
this.title = namingStrategy.resolveHumanReadableName();
45+
46+
const metricFactory = new OpenSearchServerlessIndexMetricFactory(
47+
scope.createMetricFactory(),
48+
props,
49+
);
50+
51+
this.metricIndexSearchableDocuments =
52+
metricFactory.metricIndexSearchableDocuments();
53+
54+
props.useCreatedAlarms?.consume(this.createdAlarms());
55+
}
56+
57+
summaryWidgets(): IWidget[] {
58+
return this.widgets();
59+
}
60+
61+
widgets(): IWidget[] {
62+
return [
63+
this.createTitleWidget(),
64+
this.createDocumentsWidget(FullWidth, DefaultGraphWidgetHeight),
65+
];
66+
}
67+
68+
protected createTitleWidget(): IWidget {
69+
return new MonitoringHeaderWidget({
70+
family: "OpenSearch Serverless Index",
71+
title: this.title,
72+
// TODO: add goToLinkUrl for AWS Console
73+
});
74+
}
75+
76+
protected createDocumentsWidget(width: number, height: number): IWidget {
77+
return new GraphWidget({
78+
width,
79+
height,
80+
title: "Documents",
81+
left: [this.metricIndexSearchableDocuments],
82+
leftYAxis: CountAxisFromZero,
83+
});
84+
}
85+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Stack } from "aws-cdk-lib";
2+
import type { DimensionsMap } from "aws-cdk-lib/aws-cloudwatch";
3+
import type { CfnCollection } from "aws-cdk-lib/aws-opensearchserverless";
4+
5+
import {
6+
BaseMetricFactoryProps,
7+
RateComputationMethod,
8+
BaseMetricFactory,
9+
MetricFactory,
10+
MetricWithAlarmSupport,
11+
MetricStatistic,
12+
LatencyType,
13+
getLatencyTypeStatistic,
14+
getLatencyTypeLabel,
15+
} from "../../common";
16+
17+
const OpenSearchServerlessNamespace = "AWS/AOSS";
18+
19+
export interface OpenSearchServerlessMetricFactoryProps
20+
extends BaseMetricFactoryProps {
21+
readonly collection: CfnCollection;
22+
23+
/**
24+
* @default - {@link RateComputationMethod.AVERAGE}
25+
*/
26+
readonly rateComputationMethod?: RateComputationMethod;
27+
}
28+
29+
/**
30+
* @experimental This is subject to change if an L2 construct becomes available.
31+
*
32+
* @see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/monitoring-cloudwatch.html
33+
*/
34+
export class OpenSearchServerlessMetricFactory extends BaseMetricFactory<OpenSearchServerlessMetricFactoryProps> {
35+
protected readonly rateComputationMethod: RateComputationMethod;
36+
protected readonly dimensionsMap: DimensionsMap;
37+
38+
constructor(
39+
metricFactory: MetricFactory,
40+
props: OpenSearchServerlessMetricFactoryProps,
41+
) {
42+
super(metricFactory, props);
43+
44+
this.rateComputationMethod =
45+
props.rateComputationMethod ?? RateComputationMethod.AVERAGE;
46+
this.dimensionsMap = {
47+
ClientId: this.account ?? Stack.of(props.collection).account,
48+
CollectionId: props.collection.attrId,
49+
CollectionName: props.collection.name,
50+
};
51+
}
52+
53+
metricSearchRequestErrors(): MetricWithAlarmSupport {
54+
return this.metricFactory.createMetric(
55+
"SearchRequestErrors",
56+
MetricStatistic.SUM,
57+
"SearchRequestErrors",
58+
this.dimensionsMap,
59+
undefined,
60+
OpenSearchServerlessNamespace,
61+
undefined,
62+
this.region,
63+
this.account,
64+
);
65+
}
66+
67+
metricSearchRequestLatency(statistic: LatencyType): MetricWithAlarmSupport {
68+
return this.metricFactory.createMetric(
69+
"SearchRequestLatency",
70+
getLatencyTypeStatistic(statistic),
71+
`SearchRequestLatency ${getLatencyTypeLabel(statistic)}`,
72+
this.dimensionsMap,
73+
undefined,
74+
OpenSearchServerlessNamespace,
75+
undefined,
76+
this.region,
77+
this.account,
78+
);
79+
}
80+
81+
metricIngestionRequestSuccess(): MetricWithAlarmSupport {
82+
return this.metricFactory.createMetric(
83+
"IngestionRequestSuccess",
84+
MetricStatistic.SUM,
85+
"IngestionRequestSuccess",
86+
this.dimensionsMap,
87+
undefined,
88+
OpenSearchServerlessNamespace,
89+
undefined,
90+
this.region,
91+
this.account,
92+
);
93+
}
94+
95+
metricIngestionRequestErrors(): MetricWithAlarmSupport {
96+
return this.metricFactory.createMetric(
97+
"IngestionRequestErrors",
98+
MetricStatistic.SUM,
99+
"IngestionRequestErrors",
100+
this.dimensionsMap,
101+
undefined,
102+
OpenSearchServerlessNamespace,
103+
undefined,
104+
this.region,
105+
this.account,
106+
);
107+
}
108+
109+
metricIngestionRequestLatency(
110+
statistic: LatencyType,
111+
): MetricWithAlarmSupport {
112+
return this.metricFactory.createMetric(
113+
"IngestionRequestLatency",
114+
getLatencyTypeStatistic(statistic),
115+
`IngestionRequestLatency ${getLatencyTypeLabel(statistic)}`,
116+
this.dimensionsMap,
117+
undefined,
118+
OpenSearchServerlessNamespace,
119+
undefined,
120+
this.region,
121+
this.account,
122+
);
123+
}
124+
125+
metric4xxCount(): MetricWithAlarmSupport {
126+
return this.metricFactory.createMetric(
127+
"4xx",
128+
MetricStatistic.SUM,
129+
"4xx",
130+
this.dimensionsMap,
131+
undefined,
132+
OpenSearchServerlessNamespace,
133+
undefined,
134+
this.region,
135+
this.account,
136+
);
137+
}
138+
139+
metric4xxRate(): MetricWithAlarmSupport {
140+
return this.metricFactory.toRate(
141+
this.metric4xxCount(),
142+
this.rateComputationMethod,
143+
false,
144+
"errors",
145+
);
146+
}
147+
148+
metric5xxCount(): MetricWithAlarmSupport {
149+
return this.metricFactory.createMetric(
150+
"5xx",
151+
MetricStatistic.SUM,
152+
"5xx",
153+
this.dimensionsMap,
154+
undefined,
155+
OpenSearchServerlessNamespace,
156+
undefined,
157+
this.region,
158+
this.account,
159+
);
160+
}
161+
162+
metric5xxRate(): MetricWithAlarmSupport {
163+
return this.metricFactory.toRate(
164+
this.metric5xxCount(),
165+
this.rateComputationMethod,
166+
false,
167+
"faults",
168+
);
169+
}
170+
}

0 commit comments

Comments
 (0)