Skip to content

Commit 89c2de0

Browse files
authored
fix(prometheus): handle /api/v1/metadata responses without data field (#5437)
Prometheus-compatible backends (notably Cortex) legitimately return {"status":"success"} with no "data" key when there is no metric metadata to report — which is always the case for OTLP-ingested metrics, since OTLP carries no OpenMetrics HELP/TYPE comments. PrometheusClientImpl.getAllMetrics(), queryRange(), query(), getLabels(), getLabel(), getSeries(), queryExemplars(), and getAlerts() blindly called response.getJSONObject("data")/getJSONArray("data"), throwing JSONException and breaking OSD's Metrics Explore UI end-to-end for Cortex-backed Prometheus datasources. Guard each call site with a response.has("data") check, returning an empty result for the no-metadata case. Signed-off-by: Ashish Agrawal <ashisagr@amazon.com>
1 parent df5d21d commit 89c2de0

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ public JSONObject queryRange(
115115
logger.debug("Received Prometheus response for query_range: code={}", response);
116116

117117
JSONObject jsonObject = readResponse(response);
118+
if (!jsonObject.has("data")) {
119+
return new JSONObject();
120+
}
118121
return jsonObject.getJSONObject("data");
119122
}
120123

@@ -149,6 +152,9 @@ public JSONObject query(String query, Long time, Integer limit, Integer timeout)
149152

150153
logger.info("Received Prometheus response for instant query: code={}", response);
151154
JSONObject jsonObject = readResponse(response);
155+
if (!jsonObject.has("data")) {
156+
return new JSONObject();
157+
}
152158
return jsonObject.getJSONObject("data");
153159
}
154160

@@ -167,6 +173,9 @@ public List<String> getLabels(Map<String, String> queryParams) throws IOExceptio
167173
Request request = new Request.Builder().url(queryUrl).build();
168174
Response response = AccessController.doPrivilegedChecked(() -> this.prometheusHttpClient.newCall(request).execute());
169175
JSONObject jsonObject = readResponse(response);
176+
if (!jsonObject.has("data")) {
177+
return new ArrayList<>();
178+
}
170179
return toListOfLabels(jsonObject.getJSONArray("data"));
171180
}
172181

@@ -182,6 +191,9 @@ public List<String> getLabel(String labelName, Map<String, String> queryParams)
182191
Request request = new Request.Builder().url(queryUrl).build();
183192
Response response = AccessController.doPrivilegedChecked(() -> this.prometheusHttpClient.newCall(request).execute());
184193
JSONObject jsonObject = readResponse(response);
194+
if (!jsonObject.has("data")) {
195+
return new ArrayList<>();
196+
}
185197
return toListOfLabels(jsonObject.getJSONArray("data"));
186198
}
187199

@@ -196,6 +208,9 @@ public Map<String, List<MetricMetadata>> getAllMetrics(Map<String, String> query
196208
Request request = new Request.Builder().url(queryUrl).build();
197209
Response response = AccessController.doPrivilegedChecked(() -> this.prometheusHttpClient.newCall(request).execute());
198210
JSONObject jsonObject = readResponse(response);
211+
if (!jsonObject.has("data")) {
212+
return new HashMap<>();
213+
}
199214
TypeReference<HashMap<String, List<MetricMetadata>>> typeRef = new TypeReference<>() {};
200215
return new ObjectMapper().readValue(jsonObject.getJSONObject("data").toString(), typeRef);
201216
}
@@ -215,6 +230,9 @@ public List<Map<String, String>> getSeries(Map<String, String> queryParams) thro
215230
Request request = new Request.Builder().url(queryUrl).build();
216231
Response response = AccessController.doPrivilegedChecked(() -> this.prometheusHttpClient.newCall(request).execute());
217232
JSONObject jsonObject = readResponse(response);
233+
if (!jsonObject.has("data")) {
234+
return new ArrayList<>();
235+
}
218236
JSONArray dataArray = jsonObject.getJSONArray("data");
219237
return toListOfSeries(dataArray);
220238
}
@@ -232,6 +250,9 @@ public JSONArray queryExemplars(String query, Long start, Long end) throws IOExc
232250
Request request = new Request.Builder().url(queryUrl).build();
233251
Response response = AccessController.doPrivilegedChecked(() -> this.prometheusHttpClient.newCall(request).execute());
234252
JSONObject jsonObject = readResponse(response);
253+
if (!jsonObject.has("data")) {
254+
return new JSONArray();
255+
}
235256
return jsonObject.getJSONArray("data");
236257
}
237258

@@ -243,6 +264,9 @@ public JSONObject getAlerts() throws IOException {
243264
Request request = new Request.Builder().url(queryUrl).build();
244265
Response response = AccessController.doPrivilegedChecked(() -> this.prometheusHttpClient.newCall(request).execute());
245266
JSONObject jsonObject = readResponse(response);
267+
if (!jsonObject.has("data")) {
268+
return new JSONObject();
269+
}
246270
return jsonObject.getJSONObject("data");
247271
}
248272

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,4 +1119,109 @@ public void testGetAlertmanagerStatusHttpErrorWithNullBody() throws IOException
11191119
PrometheusClientException.class, () -> nullBodyClient.getAlertmanagerStatus());
11201120
assertTrue(exception.getMessage().contains("No response body"));
11211121
}
1122+
1123+
// Prometheus-compatible backends (e.g. Cortex) may return {"status":"success"} with no
1124+
// "data" key when there is nothing to report. Each of the following tests verifies the
1125+
// corresponding client method returns an empty result instead of throwing JSONException.
1126+
1127+
@Test
1128+
public void testGetAllMetricsReturnsEmptyWhenDataFieldMissing() throws IOException {
1129+
String emptyResponse = "{\"status\":\"success\"}";
1130+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1131+
1132+
var result = client.getAllMetrics();
1133+
1134+
assertNotNull(result);
1135+
assertTrue(result.isEmpty());
1136+
}
1137+
1138+
@Test
1139+
public void testGetAllMetricsWithParamsReturnsEmptyWhenDataFieldMissing() throws IOException {
1140+
String emptyResponse = "{\"status\":\"success\"}";
1141+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1142+
1143+
HashMap<String, String> params = new HashMap<>();
1144+
params.put("limit", "10");
1145+
var result = client.getAllMetrics(params);
1146+
1147+
assertNotNull(result);
1148+
assertTrue(result.isEmpty());
1149+
}
1150+
1151+
@Test
1152+
public void testQueryRangeReturnsEmptyWhenDataFieldMissing() throws IOException {
1153+
String emptyResponse = "{\"status\":\"success\"}";
1154+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1155+
1156+
JSONObject result = client.queryRange("up", 1435781430L, 1435781460L, "15s");
1157+
1158+
assertNotNull(result);
1159+
assertTrue(result.isEmpty());
1160+
}
1161+
1162+
@Test
1163+
public void testQueryReturnsEmptyWhenDataFieldMissing() throws IOException {
1164+
String emptyResponse = "{\"status\":\"success\"}";
1165+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1166+
1167+
JSONObject result = client.query("up", 1435781460L, 100, 30);
1168+
1169+
assertNotNull(result);
1170+
assertTrue(result.isEmpty());
1171+
}
1172+
1173+
@Test
1174+
public void testGetLabelsReturnsEmptyWhenDataFieldMissing() throws IOException {
1175+
String emptyResponse = "{\"status\":\"success\"}";
1176+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1177+
1178+
List<String> result = client.getLabels(new HashMap<>());
1179+
1180+
assertNotNull(result);
1181+
assertTrue(result.isEmpty());
1182+
}
1183+
1184+
@Test
1185+
public void testGetLabelReturnsEmptyWhenDataFieldMissing() throws IOException {
1186+
String emptyResponse = "{\"status\":\"success\"}";
1187+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1188+
1189+
List<String> result = client.getLabel("job", new HashMap<>());
1190+
1191+
assertNotNull(result);
1192+
assertTrue(result.isEmpty());
1193+
}
1194+
1195+
@Test
1196+
public void testGetSeriesReturnsEmptyWhenDataFieldMissing() throws IOException {
1197+
String emptyResponse = "{\"status\":\"success\"}";
1198+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1199+
1200+
var result = client.getSeries(new HashMap<>());
1201+
1202+
assertNotNull(result);
1203+
assertTrue(result.isEmpty());
1204+
}
1205+
1206+
@Test
1207+
public void testQueryExemplarsReturnsEmptyWhenDataFieldMissing() throws IOException {
1208+
String emptyResponse = "{\"status\":\"success\"}";
1209+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1210+
1211+
JSONArray result = client.queryExemplars("http_request_duration_seconds_bucket", 1L, 2L);
1212+
1213+
assertNotNull(result);
1214+
assertEquals(0, result.length());
1215+
}
1216+
1217+
@Test
1218+
public void testGetAlertsReturnsEmptyWhenDataFieldMissing() throws IOException {
1219+
String emptyResponse = "{\"status\":\"success\"}";
1220+
mockWebServer.enqueue(new MockResponse().setBody(emptyResponse));
1221+
1222+
JSONObject result = client.getAlerts();
1223+
1224+
assertNotNull(result);
1225+
assertTrue(result.isEmpty());
1226+
}
11221227
}

0 commit comments

Comments
 (0)