diff --git a/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelValuesRestIT.java b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelValuesRestIT.java index 79e0261259c04..86ce1e080a4db 100644 --- a/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelValuesRestIT.java +++ b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelValuesRestIT.java @@ -12,6 +12,7 @@ import io.netty.handler.codec.compression.Snappy; import org.apache.http.HttpHeaders; +import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.elasticsearch.client.Request; @@ -30,6 +31,7 @@ import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -135,6 +137,25 @@ public void testGetWithMatchSelectorFiltersValues() throws Exception { assertThat(values, not(hasItem("other_job"))); } + public void testGetWithMultipleMatchSelectorsReturnsCombinedValues() throws Exception { + writeMetric("multi_selector_metric_a", Map.of("job", "multi_job_a")); + writeMetric("multi_selector_metric_b", Map.of("job", "multi_job_b")); + writeMetric("multi_selector_metric_c", Map.of("job", "multi_job_c")); // must not appear in results + + // Use URIBuilder to send two match[] selectors in a single request, working around the + // test client's single-value-per-key restriction on Request.addParameter. + Request request = new Request( + "GET", + new URIBuilder("/_prometheus/api/v1/label/job/values").addParameter("match[]", "multi_selector_metric_a") + .addParameter("match[]", "multi_selector_metric_b") + .build() + .toString() + ); + List values = labelValuesData(client().performRequest(request)); + + assertThat(values, containsInAnyOrder("multi_job_a", "multi_job_b")); + } + public void testGetValuesAreSorted() throws Exception { writeMetric("sorted_gauge", Map.of("job", "zebra")); writeMetric("sorted_gauge", Map.of("job", "alpha")); diff --git a/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelsRestIT.java b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelsRestIT.java index fd856acea0c66..8b45597d62aef 100644 --- a/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelsRestIT.java +++ b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusLabelsRestIT.java @@ -12,6 +12,7 @@ import io.netty.handler.codec.compression.Snappy; import org.apache.http.HttpHeaders; +import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.elasticsearch.client.Request; @@ -135,6 +136,26 @@ public void testGetWithMatchSelectorFiltersToMatchingLabels() throws Exception { assertThat(data, hasItem("unique_label")); } + @SuppressWarnings("unchecked") + public void testGetWithMultipleMatchSelectorsReturnsCombinedLabels() throws Exception { + writeMetric("multi_labels_metric_a", Map.of("label_only_in_a", "value_a")); + writeMetric("multi_labels_metric_b", Map.of("label_only_in_b", "value_b")); + writeMetric("multi_labels_metric_c", Map.of("label_only_in_c", "value_c")); // must not appear in results + + // Use URIBuilder to send two match[] selectors in a single request, working around the + // test client's single-value-per-key restriction on Request.addParameter. + Request request = new Request( + "GET", + new URIBuilder("/_prometheus/api/v1/labels").addParameter("match[]", "multi_labels_metric_a") + .addParameter("match[]", "multi_labels_metric_b") + .build() + .toString() + ); + List data = (List) entityAsMap(client().performRequest(request)).get("data"); + + assertThat(data, containsInAnyOrder("__name__", "label_only_in_a", "label_only_in_b")); + } + /** Builds a labels request with optional {@code match[]} parameters. */ private static Request labelsRequest(String... matchers) { Request request = new Request("GET", "/_prometheus/api/v1/labels"); diff --git a/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusSeriesRestIT.java b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusSeriesRestIT.java index 2a7d7f132adff..51a7e7c73b03c 100644 --- a/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusSeriesRestIT.java +++ b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusSeriesRestIT.java @@ -12,6 +12,7 @@ import io.netty.handler.codec.compression.Snappy; import org.apache.http.HttpHeaders; +import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.apache.http.util.EntityUtils; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -143,12 +145,28 @@ public void testSeriesWithIndexPattern() throws Exception { assertThat(data.getFirst().get("__name__"), equalTo("test_gauge_idx")); } + public void testGetWithMultipleMatchSelectorsReturnsCombinedSeries() throws Exception { + writeMetric("multi_series_selector_a", Map.of("job", "job_a")); + writeMetric("multi_series_selector_b", Map.of("job", "job_b")); + writeMetric("multi_series_selector_c", Map.of("job", "job_c")); // must not appear in results + + // Use URIBuilder to send two match[] selectors in a single request, working around the + // test client's single-value-per-key restriction on Request.addParameter. + Request request = new Request( + "GET", + new URIBuilder("/_prometheus/api/v1/series").addParameter("match[]", "multi_series_selector_a") + .addParameter("match[]", "multi_series_selector_b") + .build() + .toString() + ); + List> data = seriesData(client().performRequest(request)); + + List names = data.stream().map(s -> (String) s.get("__name__")).toList(); + assertThat(names, containsInAnyOrder("multi_series_selector_a", "multi_series_selector_b")); + } + // Helpers - /** - * Builds a series request with a single {@code match[]} parameter. - * TODO: support multiple {@code match[]} values once multi-value query param support lands. - */ private static Request seriesRequest(String matcher) { return seriesRequest(null, matcher); } diff --git a/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelValuesRestAction.java b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelValuesRestAction.java index 1c2e9becc9e7a..6bdb34d491382 100644 --- a/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelValuesRestAction.java +++ b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelValuesRestAction.java @@ -9,6 +9,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RequestParams; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; @@ -73,9 +74,10 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli String labelName = PrometheusLabelNameUtils.decodeLabelName(rawName); String index = request.param(INDEX_PARAM, "*"); - // TODO: support multiple match[] selectors once multi-value param support is added - String matchSelector = request.param(MATCH_PARAM); - List matchSelectors = matchSelector != null ? List.of(matchSelector) : List.of(); + // Consume the parameter; re-parse from the raw URI to handle repeated match[] params, + // since request processing currently keeps only the last value for repeated parameters. + request.repeatedParamAsList(MATCH_PARAM); + List matchSelectors = RequestParams.fromUri(request.uri()).getAll(MATCH_PARAM); // Time range String endParam = request.param(END_PARAM); diff --git a/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelsRestAction.java b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelsRestAction.java index ce17e81f60884..31be9d8bfc028 100644 --- a/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelsRestAction.java +++ b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelsRestAction.java @@ -9,6 +9,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RequestParams; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; @@ -59,8 +60,10 @@ public List routes() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - String matchParam = request.param(MATCH_PARAM); - List matchSelectors = matchParam != null ? List.of(matchParam) : List.of(); + // Consume the parameter; re-parse from the raw URI to handle repeated match[] params, + // since request processing currently keeps only the last value for repeated parameters. + request.repeatedParamAsList(MATCH_PARAM); + List matchSelectors = RequestParams.fromUri(request.uri()).getAll(MATCH_PARAM); // Time range String endParam = request.param(END_PARAM); diff --git a/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusSeriesRestAction.java b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusSeriesRestAction.java index 8ce07cf3aca4b..fc754c72a524d 100644 --- a/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusSeriesRestAction.java +++ b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusSeriesRestAction.java @@ -9,6 +9,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RequestParams; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; @@ -57,12 +58,13 @@ public List routes() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - // TODO: support multiple match[] values once multi-value query param support lands - String matchSelector = request.param(MATCH_PARAM); - if (matchSelector == null) { + // Consume the parameter; re-parse from the raw URI to handle repeated match[] params, + // since request processing currently keeps only the last value for repeated parameters. + request.repeatedParamAsList(MATCH_PARAM); + List matchSelectors = RequestParams.fromUri(request.uri()).getAll(MATCH_PARAM); + if (matchSelectors.isEmpty()) { throw new IllegalArgumentException("At least one [match[]] selector is required"); } - List matchSelectors = List.of(matchSelector); // Time range String endParam = request.param(END_PARAM); diff --git a/x-pack/plugin/prometheus/src/test/java/org/elasticsearch/xpack/prometheus/rest/PrometheusSeriesRestActionTests.java b/x-pack/plugin/prometheus/src/test/java/org/elasticsearch/xpack/prometheus/rest/PrometheusSeriesRestActionTests.java new file mode 100644 index 0000000000000..e124d73493d9d --- /dev/null +++ b/x-pack/plugin/prometheus/src/test/java/org/elasticsearch/xpack/prometheus/rest/PrometheusSeriesRestActionTests.java @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.prometheus.rest; + +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpNodeClient; +import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.threadpool.ThreadPool; +import org.junit.After; +import org.junit.Before; + +import java.util.Map; + +public class PrometheusSeriesRestActionTests extends ESTestCase { + + private ThreadPool threadPool; + private NoOpNodeClient client; + + @Before + public void setUp() throws Exception { + super.setUp(); + threadPool = createThreadPool(); + client = new NoOpNodeClient(threadPool); + } + + @After + @Override + public void tearDown() throws Exception { + super.tearDown(); + terminate(threadPool); + } + + public void testMissingMatchSelectorThrows() { + var action = new PrometheusSeriesRestAction(); + var httpRequest = new FakeRestRequest.FakeHttpRequest(RestRequest.Method.GET, "/_prometheus/api/v1/series", null, Map.of()); + var request = RestRequest.request(parserConfig(), httpRequest, new FakeRestRequest.FakeHttpChannel(null)); + var e = expectThrows(IllegalArgumentException.class, () -> action.prepareRequest(request, client)); + assertEquals("At least one [match[]] selector is required", e.getMessage()); + } +}