Skip to content

Commit b0ab3b5

Browse files
committed
Add support to delete silences and fetch alert manager status
Signed-off-by: Ashish Agrawal <ashisagr@amazon.com>
1 parent b5480e9 commit b0ab3b5

10 files changed

Lines changed: 320 additions & 4 deletions

File tree

direct-query-core/src/main/java/org/opensearch/sql/directquery/rest/model/DirectQueryResourceType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public enum DirectQueryResourceType {
2121
ALERTMANAGER_ALERTS,
2222
ALERTMANAGER_ALERT_GROUPS,
2323
ALERTMANAGER_RECEIVERS,
24-
ALERTMANAGER_SILENCES;
24+
ALERTMANAGER_SILENCES,
25+
ALERTMANAGER_STATUS;
2526

2627
/**
2728
* Convert a string to the corresponding enum value, case-insensitive.

direct-query-core/src/main/java/org/opensearch/sql/prometheus/client/PrometheusClient.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ Map<String, List<MetricMetadata>> getAllMetrics(Map<String, String> queryParams)
110110
*/
111111
String createAlertmanagerSilences(String silenceJson) throws IOException;
112112

113+
/**
114+
* Expire (delete) a silence in Alertmanager.
115+
*
116+
* @param silenceId The ID of the silence to expire
117+
* @return String containing the response
118+
* @throws IOException If there is an issue with the request
119+
*/
120+
String deleteAlertmanagerSilence(String silenceId) throws IOException;
121+
122+
/**
123+
* Get Alertmanager status including configuration, version, and cluster info.
124+
*
125+
* @return JSONObject containing the Alertmanager status
126+
* @throws IOException If there is an issue with the request
127+
*/
128+
JSONObject getAlertmanagerStatus() throws IOException;
129+
113130
/**
114131
* Get rules for a specific namespace, normalized to a consistent JSON format. Handles
115132
* Cortex/Thanos YAML and AMP JSON responses, returning them all as a {"groups":[...]} structure.

direct-query-core/src/main/java/org/opensearch/sql/prometheus/client/PrometheusClientImpl.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,63 @@ public String createAlertmanagerSilences(String silenceJson) throws IOException
335335
}
336336
}
337337

338+
@Override
339+
public String deleteAlertmanagerSilence(String silenceId) throws IOException {
340+
String baseUrl = alertmanagerUri.toString().replaceAll("/$", "");
341+
String queryUrl =
342+
String.format(
343+
"%s/api/v2/silence/%s",
344+
baseUrl, URLEncoder.encode(silenceId, StandardCharsets.UTF_8));
345+
346+
logger.debug("Making Delete Alertmanager silence request: {}", queryUrl);
347+
Request request = new Request.Builder().url(queryUrl).delete().build();
348+
Response response =
349+
AccessController.doPrivilegedChecked(
350+
() -> this.alertmanagerHttpClient.newCall(request).execute());
351+
352+
if (response.isSuccessful()) {
353+
return "{\"status\":\"success\"}";
354+
} else {
355+
String errorBody = response.body() != null ? response.body().string() : "No response body";
356+
logger.error(
357+
"Delete Alertmanager Silence request failed with code: {}, error body: {}",
358+
response.code(),
359+
errorBody);
360+
throw new PrometheusClientException(
361+
String.format(
362+
"Alertmanager request failed with code: %s. Error details: %s",
363+
response.code(), errorBody));
364+
}
365+
}
366+
367+
@Override
368+
public JSONObject getAlertmanagerStatus() throws IOException {
369+
String baseUrl = alertmanagerUri.toString().replaceAll("/$", "");
370+
String queryUrl = String.format("%s/api/v2/status", baseUrl);
371+
372+
logger.debug("Making Alertmanager status request: {}", queryUrl);
373+
Request request = new Request.Builder().url(queryUrl).build();
374+
Response response =
375+
AccessController.doPrivilegedChecked(
376+
() -> this.alertmanagerHttpClient.newCall(request).execute());
377+
378+
if (response.isSuccessful()) {
379+
String bodyString = Objects.requireNonNull(response.body()).string();
380+
logger.debug("Alertmanager status response body: {}", bodyString);
381+
return new JSONObject(bodyString);
382+
} else {
383+
String errorBody = response.body() != null ? response.body().string() : "No response body";
384+
logger.error(
385+
"Alertmanager status request failed with code: {}, error body: {}",
386+
response.code(),
387+
errorBody);
388+
throw new PrometheusClientException(
389+
String.format(
390+
"Alertmanager request failed with code: %s. Error details: %s",
391+
response.code(), errorBody));
392+
}
393+
}
394+
338395
@Override
339396
public JSONObject getRulesByNamespace(String namespace, Map<String, String> queryParams)
340397
throws IOException {

direct-query-core/src/main/java/org/opensearch/sql/prometheus/query/PrometheusQueryHandler.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ public GetDirectQueryResourcesResponse<?> getResources(
179179
JSONArray silences = client.getAlertmanagerSilences();
180180
return GetDirectQueryResourcesResponse.withList(silences.toList());
181181
}
182+
case ALERTMANAGER_STATUS:
183+
{
184+
JSONObject status = client.getAlertmanagerStatus();
185+
return GetDirectQueryResourcesResponse.withMap(status.toMap());
186+
}
182187
default:
183188
throw new IllegalArgumentException(
184189
"Invalid prometheus resource type: " + request.getResourceType());
@@ -203,6 +208,13 @@ public WriteDirectQueryResourcesResponse<?> writeResources(
203208
switch (request.getResourceType()) {
204209
case ALERTMANAGER_SILENCES:
205210
{
211+
if (request.isDelete()) {
212+
if (request.getResourceName() == null || request.getResourceName().isEmpty()) {
213+
throw new IllegalArgumentException("Silence ID is required for deleting a silence");
214+
}
215+
String result = client.deleteAlertmanagerSilence(request.getResourceName());
216+
return WriteDirectQueryResourcesResponse.withStringList(List.of(result));
217+
}
206218
String createdSilence = client.createAlertmanagerSilences(request.getRequest());
207219
return WriteDirectQueryResourcesResponse.withList(List.of(createdSilence));
208220
}

direct-query-core/src/test/java/org/opensearch/sql/prometheus/client/PrometheusClientImplTest.java

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,4 +1011,116 @@ public void testDeleteRuleGroupUrlEncodesSpecialCharacters() throws Exception {
10111011
assertTrue(path.contains("ns+with+spaces") || path.contains("ns%20with%20spaces"));
10121012
assertTrue(path.contains("group%2Fname"));
10131013
}
1014+
1015+
@Test
1016+
public void testDeleteAlertmanagerSilenceSuccess() throws IOException {
1017+
mockWebServer.enqueue(new MockResponse().setResponseCode(200));
1018+
1019+
String result = client.deleteAlertmanagerSilence("silence-12345");
1020+
1021+
assertNotNull(result);
1022+
assertTrue(result.contains("success"));
1023+
}
1024+
1025+
@Test
1026+
public void testDeleteAlertmanagerSilenceHttpError() {
1027+
mockWebServer.enqueue(
1028+
new MockResponse().setResponseCode(404).setBody("silence silence-bad not found"));
1029+
1030+
PrometheusClientException exception =
1031+
assertThrows(
1032+
PrometheusClientException.class,
1033+
() -> client.deleteAlertmanagerSilence("silence-bad"));
1034+
assertTrue(exception.getMessage().contains("404"));
1035+
}
1036+
1037+
@Test
1038+
public void testDeleteAlertmanagerSilenceHttpErrorWithNullBody() throws IOException {
1039+
Request dummyRequest = new Request.Builder().url(mockWebServer.url("/")).build();
1040+
Response nullBodyResponse =
1041+
new Response.Builder()
1042+
.request(dummyRequest)
1043+
.protocol(Protocol.HTTP_1_1)
1044+
.code(500)
1045+
.message("Server Error")
1046+
.body(null)
1047+
.build();
1048+
1049+
OkHttpClient spyClient = spy(new OkHttpClient());
1050+
Call mockCall = mock(Call.class);
1051+
when(mockCall.execute()).thenReturn(nullBodyResponse);
1052+
doAnswer(invocation -> mockCall).when(spyClient).newCall(any(Request.class));
1053+
1054+
PrometheusClientImpl nullBodyClient =
1055+
new PrometheusClientImpl(
1056+
new OkHttpClient(),
1057+
URI.create(String.format("http://%s:%s", "localhost", mockWebServer.getPort())),
1058+
spyClient,
1059+
URI.create(
1060+
String.format(
1061+
"http://%s:%s/alertmanager", "localhost", mockWebServer.getPort())));
1062+
1063+
PrometheusClientException exception =
1064+
assertThrows(
1065+
PrometheusClientException.class,
1066+
() -> nullBodyClient.deleteAlertmanagerSilence("silence-bad"));
1067+
assertTrue(exception.getMessage().contains("No response body"));
1068+
}
1069+
1070+
@Test
1071+
public void testGetAlertmanagerStatusSuccess() throws IOException {
1072+
String statusResponse =
1073+
"{\"cluster\":{\"status\":\"ready\"},\"versionInfo\":{\"version\":\"0.27.0\"},\"config\":{\"original\":\"route:\\n receiver: default\"}}";
1074+
mockWebServer.enqueue(new MockResponse().setBody(statusResponse));
1075+
1076+
JSONObject result = client.getAlertmanagerStatus();
1077+
1078+
assertNotNull(result);
1079+
assertTrue(result.has("cluster"));
1080+
assertTrue(result.has("versionInfo"));
1081+
assertTrue(result.has("config"));
1082+
}
1083+
1084+
@Test
1085+
public void testGetAlertmanagerStatusHttpError() {
1086+
mockWebServer.enqueue(
1087+
new MockResponse().setResponseCode(500).setBody("Internal server error"));
1088+
1089+
PrometheusClientException exception =
1090+
assertThrows(
1091+
PrometheusClientException.class, () -> client.getAlertmanagerStatus());
1092+
assertTrue(exception.getMessage().contains("500"));
1093+
}
1094+
1095+
@Test
1096+
public void testGetAlertmanagerStatusHttpErrorWithNullBody() throws IOException {
1097+
Request dummyRequest = new Request.Builder().url(mockWebServer.url("/")).build();
1098+
Response nullBodyResponse =
1099+
new Response.Builder()
1100+
.request(dummyRequest)
1101+
.protocol(Protocol.HTTP_1_1)
1102+
.code(500)
1103+
.message("Server Error")
1104+
.body(null)
1105+
.build();
1106+
1107+
OkHttpClient spyClient = spy(new OkHttpClient());
1108+
Call mockCall = mock(Call.class);
1109+
when(mockCall.execute()).thenReturn(nullBodyResponse);
1110+
doAnswer(invocation -> mockCall).when(spyClient).newCall(any(Request.class));
1111+
1112+
PrometheusClientImpl nullBodyClient =
1113+
new PrometheusClientImpl(
1114+
new OkHttpClient(),
1115+
URI.create(String.format("http://%s:%s", "localhost", mockWebServer.getPort())),
1116+
spyClient,
1117+
URI.create(
1118+
String.format(
1119+
"http://%s:%s/alertmanager", "localhost", mockWebServer.getPort())));
1120+
1121+
PrometheusClientException exception =
1122+
assertThrows(
1123+
PrometheusClientException.class, () -> nullBodyClient.getAlertmanagerStatus());
1124+
assertTrue(exception.getMessage().contains("No response body"));
1125+
}
10141126
}

direct-query-core/src/test/java/org/opensearch/sql/prometheus/query/PrometheusQueryHandlerTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,4 +858,71 @@ public void testWriteResourcesRulesWithIOException() throws IOException {
858858

859859
handler.writeResources(prometheusClient, request);
860860
}
861+
862+
@Test
863+
public void testGetResourcesAlertmanagerStatus() throws IOException {
864+
GetDirectQueryResourcesRequest request = new GetDirectQueryResourcesRequest();
865+
request.setResourceType(DirectQueryResourceType.ALERTMANAGER_STATUS);
866+
867+
JSONObject statusJson =
868+
new JSONObject(
869+
"{\"cluster\":{\"status\":\"ready\"},\"versionInfo\":{\"version\":\"0.27.0\"}}");
870+
when(prometheusClient.getAlertmanagerStatus()).thenReturn(statusJson);
871+
872+
GetDirectQueryResourcesResponse<?> response =
873+
handler.getResources(prometheusClient, request);
874+
875+
assertNotNull(response);
876+
Map<?, ?> data = (Map<?, ?>) response.getData();
877+
assertTrue(data.containsKey("cluster"));
878+
}
879+
880+
@Test
881+
public void testDeleteAlertmanagerSilence() throws IOException {
882+
WriteDirectQueryResourcesRequest request = new WriteDirectQueryResourcesRequest();
883+
request.setResourceType(DirectQueryResourceType.ALERTMANAGER_SILENCES);
884+
request.setResourceName("silence-12345");
885+
request.setDelete(true);
886+
887+
when(prometheusClient.deleteAlertmanagerSilence(eq("silence-12345")))
888+
.thenReturn("{\"status\":\"success\"}");
889+
890+
WriteDirectQueryResourcesResponse<?> response =
891+
handler.writeResources(prometheusClient, request);
892+
893+
assertNotNull(response);
894+
}
895+
896+
@Test(expected = IllegalArgumentException.class)
897+
public void testDeleteAlertmanagerSilenceNullId() {
898+
WriteDirectQueryResourcesRequest request = new WriteDirectQueryResourcesRequest();
899+
request.setResourceType(DirectQueryResourceType.ALERTMANAGER_SILENCES);
900+
request.setResourceName(null);
901+
request.setDelete(true);
902+
903+
handler.writeResources(prometheusClient, request);
904+
}
905+
906+
@Test(expected = IllegalArgumentException.class)
907+
public void testDeleteAlertmanagerSilenceEmptyId() {
908+
WriteDirectQueryResourcesRequest request = new WriteDirectQueryResourcesRequest();
909+
request.setResourceType(DirectQueryResourceType.ALERTMANAGER_SILENCES);
910+
request.setResourceName("");
911+
request.setDelete(true);
912+
913+
handler.writeResources(prometheusClient, request);
914+
}
915+
916+
@Test(expected = PrometheusClientException.class)
917+
public void testDeleteAlertmanagerSilenceWithIOException() throws IOException {
918+
WriteDirectQueryResourcesRequest request = new WriteDirectQueryResourcesRequest();
919+
request.setResourceType(DirectQueryResourceType.ALERTMANAGER_SILENCES);
920+
request.setResourceName("silence-12345");
921+
request.setDelete(true);
922+
923+
when(prometheusClient.deleteAlertmanagerSilence(eq("silence-12345")))
924+
.thenThrow(new IOException("Connection failed"));
925+
926+
handler.writeResources(prometheusClient, request);
927+
}
861928
}

direct-query/src/main/java/org/opensearch/sql/directquery/rest/RestDirectQueryResourcesManagementAction.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,19 @@ public List<Route> routes() {
108108
Locale.ROOT,
109109
"%s/alertmanager/api/v2/{resourceType}",
110110
BASE_DIRECT_QUERY_RESOURCES_URL)),
111-
// Only support creating alert silences for prometheus for now
111+
// Create alert silences
112112
new Route(
113113
POST,
114114
String.format(
115115
Locale.ROOT,
116116
"%s/alertmanager/api/v2/{resourceType}",
117+
BASE_DIRECT_QUERY_RESOURCES_URL)),
118+
// Expire (delete) a specific silence
119+
new Route(
120+
DELETE,
121+
String.format(
122+
Locale.ROOT,
123+
"%s/alertmanager/api/v2/silence/{silenceID}",
117124
BASE_DIRECT_QUERY_RESOURCES_URL)));
118125
}
119126

direct-query/src/main/java/org/opensearch/sql/directquery/transport/format/DirectQueryResourcesRequestConverter.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public static GetDirectQueryResourcesRequest toGetDirectRestRequest(RestRequest
3535
// Handle Alertmanager API endpoints
3636
if (path.contains("/alerts/groups")) {
3737
directQueryRequest.setResourceType(DirectQueryResourceType.ALERTMANAGER_ALERT_GROUPS);
38+
} else if (path.contains("/status")) {
39+
directQueryRequest.setResourceType(DirectQueryResourceType.ALERTMANAGER_STATUS);
3840
} else {
3941
directQueryRequest.setResourceType(
4042
DirectQueryResourceType.fromString(
@@ -84,7 +86,11 @@ public static WriteDirectQueryResourcesRequest toWriteDirectRestRequest(
8486
String path = restRequest.path();
8587
if (path.contains("/alertmanager/api/v2/")) {
8688
// Handle Alertmanager API endpoints
87-
if (path.contains("/alerts/groups")) {
89+
if (path.contains("/silence/")) {
90+
// DELETE /alertmanager/api/v2/silence/{silenceID}
91+
directQueryRequest.setResourceType(DirectQueryResourceType.ALERTMANAGER_SILENCES);
92+
directQueryRequest.setResourceName(restRequest.param("silenceID"));
93+
} else if (path.contains("/alerts/groups")) {
8894
directQueryRequest.setResourceType(DirectQueryResourceType.ALERTMANAGER_ALERT_GROUPS);
8995
} else {
9096
directQueryRequest.setResourceType(

direct-query/src/test/java/org/opensearch/sql/directquery/rest/RestDirectQueryResourcesManagementActionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public void testGetName() {
132132
public void testRoutes() {
133133
List<RestDirectQueryResourcesManagementAction.Route> routes = unit.routes();
134134
Assertions.assertNotNull(routes);
135-
Assertions.assertEquals(9, routes.size());
135+
Assertions.assertEquals(10, routes.size());
136136

137137
boolean foundResourceTypeRoute = false;
138138
boolean foundResourceValuesRoute = false;

0 commit comments

Comments
 (0)