Skip to content

Commit 63d244e

Browse files
authored
SOLR-17436: Create a v2 equivalent for /admin/metrics (#4057)
Co-authored-by: Isabelle Giguere <igiguere71@yahoo.ca> New v2 /metrics handler supporting both Prometheus and OpenMetrics format.
1 parent 507859f commit 63d244e

26 files changed

Lines changed: 1141 additions & 232 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
2+
title: Create a v2 equivalent for /admin/metrics
3+
type: added # added, changed, fixed, deprecated, removed, dependency_update, security, other
4+
authors:
5+
- name: Isabelle Giguère
6+
links:
7+
- name: SOLR-17436
8+
url: https://issues.apache.org/jira/browse/SOLR-17436
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.solr.client.api.endpoint;
18+
19+
import static org.apache.solr.client.api.util.Constants.RAW_OUTPUT_PROPERTY;
20+
21+
import io.swagger.v3.oas.annotations.Operation;
22+
import io.swagger.v3.oas.annotations.Parameter;
23+
import io.swagger.v3.oas.annotations.extensions.Extension;
24+
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
25+
import io.swagger.v3.oas.annotations.media.Schema;
26+
import jakarta.ws.rs.GET;
27+
import jakarta.ws.rs.HeaderParam;
28+
import jakarta.ws.rs.Path;
29+
import jakarta.ws.rs.QueryParam;
30+
import jakarta.ws.rs.core.StreamingOutput;
31+
32+
/** V2 API definitions to fetch metrics. */
33+
@Path("/metrics")
34+
public interface MetricsApi {
35+
36+
@GET
37+
@Operation(
38+
summary = "Retrieve metrics gathered by Solr.",
39+
tags = {"metrics"},
40+
extensions = {
41+
@Extension(properties = {@ExtensionProperty(name = RAW_OUTPUT_PROPERTY, value = "true")})
42+
})
43+
StreamingOutput getMetrics(
44+
@HeaderParam("Accept") String acceptHeader,
45+
@Parameter(
46+
schema =
47+
@Schema(
48+
name = "node",
49+
description = "Name of the node to which proxy the request.",
50+
defaultValue = "all"))
51+
@QueryParam(value = "node")
52+
String node,
53+
@Parameter(schema = @Schema(name = "name", description = "The metric name to filter on."))
54+
@QueryParam(value = "name")
55+
String name,
56+
@Parameter(
57+
schema = @Schema(name = "category", description = "The category label to filter on."))
58+
@QueryParam(value = "category")
59+
String category,
60+
@Parameter(
61+
schema =
62+
@Schema(
63+
name = "core",
64+
description =
65+
"TThe core name to filter on. More than one core can be specified in a comma-separated list."))
66+
@QueryParam(value = "core")
67+
String core,
68+
@Parameter(
69+
schema =
70+
@Schema(name = "collection", description = "The collection name to filter on. "))
71+
@QueryParam(value = "collection")
72+
String collection,
73+
@Parameter(schema = @Schema(name = "shard", description = "The shard name to filter on."))
74+
@QueryParam(value = "shard")
75+
String shard,
76+
@Parameter(
77+
schema =
78+
@Schema(
79+
name = "replica_type",
80+
description = "The replica type to filter on.",
81+
allowableValues = {"NRT", "TLOG", "PULL"}))
82+
@QueryParam(value = "replica_type")
83+
String replicaType);
84+
}

solr/core/src/java/org/apache/solr/handler/admin/AdminHandlersProxy.java

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@
3333
import org.apache.solr.client.solrj.SolrRequest;
3434
import org.apache.solr.client.solrj.SolrServerException;
3535
import org.apache.solr.client.solrj.request.GenericSolrRequest;
36+
import org.apache.solr.client.solrj.request.GenericV2SolrRequest;
3637
import org.apache.solr.client.solrj.response.InputStreamResponseParser;
3738
import org.apache.solr.cloud.ZkController;
3839
import org.apache.solr.common.SolrException;
40+
import org.apache.solr.common.params.CommonParams;
3941
import org.apache.solr.common.params.ModifiableSolrParams;
4042
import org.apache.solr.common.params.SolrParams;
4143
import org.apache.solr.common.util.NamedList;
4244
import org.apache.solr.core.CoreContainer;
4345
import org.apache.solr.request.SolrQueryRequest;
4446
import org.apache.solr.response.SolrQueryResponse;
47+
import org.apache.solr.util.stats.MetricUtils;
4548
import org.slf4j.Logger;
4649
import org.slf4j.LoggerFactory;
4750

@@ -55,17 +58,34 @@ public class AdminHandlersProxy {
5558
private static final String PARAM_NODE = "node";
5659
private static final long PROMETHEUS_FETCH_TIMEOUT_SECONDS = 10;
5760

58-
/** Proxy this request to a different remote node if 'node' or 'nodes' parameter is provided */
61+
/**
62+
* Proxy this request to a different remote node's V1 API if 'node' or 'nodes' parameter is
63+
* provided. For V2, use {@link AdminHandlersProxy#maybeProxyToNodes(String, SolrQueryRequest,
64+
* SolrQueryResponse, CoreContainer)}
65+
*/
5966
public static boolean maybeProxyToNodes(
6067
SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer container)
6168
throws IOException, SolrServerException, InterruptedException {
69+
return maybeProxyToNodes("V1", req, rsp, container);
70+
}
71+
72+
/**
73+
* Proxy this request to a different remote node's selected API version if 'node' or 'nodes'
74+
* parameter is provided
75+
*/
76+
public static boolean maybeProxyToNodes(
77+
String apiVersion, SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer container)
78+
throws IOException, SolrServerException, InterruptedException {
6279

6380
String pathStr = req.getPath();
6481
ModifiableSolrParams params = new ModifiableSolrParams(req.getParams());
6582

6683
// Check if response format is Prometheus/OpenMetrics
67-
String wt = params.get("wt");
68-
boolean isPrometheusFormat = "prometheus".equals(wt) || "openmetrics".equals(wt);
84+
String wt = params.get(CommonParams.WT);
85+
boolean isPrometheusFormat =
86+
MetricUtils.PROMETHEUS_METRICS_WT.equals(wt)
87+
|| MetricUtils.OPEN_METRICS_WT.equals(wt)
88+
|| (wt == null && pathStr.endsWith("/metrics"));
6989

7090
if (isPrometheusFormat) {
7191
// Prometheus format: use singular 'node' parameter for single-node proxy
@@ -75,7 +95,7 @@ public static boolean maybeProxyToNodes(
7595
}
7696

7797
params.remove(PARAM_NODE);
78-
handlePrometheusSingleNode(nodeName, pathStr, params, container, rsp);
98+
handlePrometheusSingleNode(apiVersion, nodeName, pathStr, params, container, rsp);
7999
} else {
80100
// Other formats (JSON/XML): use plural 'nodes' parameter for multi-node aggregation
81101
String nodeNames = req.getParams().get(PARAM_NODES);
@@ -85,14 +105,15 @@ public static boolean maybeProxyToNodes(
85105

86106
params.remove(PARAM_NODES);
87107
Set<String> nodes = resolveNodes(nodeNames, container);
88-
handleNamedListFormat(nodes, pathStr, params, container.getZkController(), rsp);
108+
handleNamedListFormat(apiVersion, nodes, pathStr, params, container.getZkController(), rsp);
89109
}
90110

91111
return true;
92112
}
93113

94114
/** Handle non-Prometheus formats using the existing NamedList approach. */
95115
private static void handleNamedListFormat(
116+
String apiVersion,
96117
Set<String> nodes,
97118
String pathStr,
98119
SolrParams params,
@@ -101,7 +122,7 @@ private static void handleNamedListFormat(
101122

102123
Map<String, Future<NamedList<Object>>> responses = new LinkedHashMap<>();
103124
for (String node : nodes) {
104-
responses.put(node, callRemoteNode(node, pathStr, params, zkController));
125+
responses.put(node, callRemoteNode(apiVersion, node, pathStr, params, zkController));
105126
}
106127

107128
for (Map.Entry<String, Future<NamedList<Object>>> entry : responses.entrySet()) {
@@ -125,8 +146,12 @@ private static void handleNamedListFormat(
125146
}
126147

127148
/** Makes a remote request asynchronously. */
128-
public static CompletableFuture<NamedList<Object>> callRemoteNode(
129-
String nodeName, String uriPath, SolrParams params, ZkController zkController) {
149+
private static CompletableFuture<NamedList<Object>> callRemoteNode(
150+
String apiVersion,
151+
String nodeName,
152+
String uriPath,
153+
SolrParams params,
154+
ZkController zkController) {
130155

131156
// Validate that the node exists in the cluster
132157
if (!zkController.zkStateReader.getClusterState().getLiveNodes().contains(nodeName)) {
@@ -137,13 +162,17 @@ public static CompletableFuture<NamedList<Object>> callRemoteNode(
137162

138163
log.debug("Proxying {} request to node {}", uriPath, nodeName);
139164
URI baseUri = URI.create(zkController.zkStateReader.getBaseUrlForNodeName(nodeName));
140-
SolrRequest<?> proxyReq = new GenericSolrRequest(SolrRequest.METHOD.GET, uriPath, params);
165+
166+
SolrRequest<?> proxyReq = createRequest(apiVersion, uriPath, params);
141167

142168
// Set response parser based on wt parameter to ensure correct format is used
143-
String wt = params.get("wt");
144-
if ("prometheus".equals(wt) || "openmetrics".equals(wt)) {
169+
String wt = params.get(CommonParams.WT);
170+
if (MetricUtils.PROMETHEUS_METRICS_WT.equals(wt) || MetricUtils.OPEN_METRICS_WT.equals(wt)) {
145171
proxyReq.setResponseParser(new InputStreamResponseParser(wt));
146172
}
173+
if (wt == null && uriPath.endsWith("/metrics")) {
174+
proxyReq.setResponseParser(new InputStreamResponseParser(MetricUtils.PROMETHEUS_METRICS_WT));
175+
}
147176

148177
try {
149178
return zkController
@@ -195,6 +224,7 @@ private static Set<String> resolveNodes(String nodeNames, CoreContainer containe
195224
* @param rsp the response to populate
196225
*/
197226
private static void handlePrometheusSingleNode(
227+
String apiVersion,
198228
String nodeName,
199229
String pathStr,
200230
ModifiableSolrParams params,
@@ -205,7 +235,7 @@ private static void handlePrometheusSingleNode(
205235
// Keep wt=prometheus for the remote request so MetricsHandler accepts it
206236
// The InputStreamResponseParser will return the Prometheus text in a "stream" key
207237
Future<NamedList<Object>> response =
208-
callRemoteNode(nodeName, pathStr, params, container.getZkController());
238+
callRemoteNode(apiVersion, nodeName, pathStr, params, container.getZkController());
209239

210240
try {
211241
try {
@@ -220,4 +250,12 @@ private static void handlePrometheusSingleNode(
220250
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, t);
221251
}
222252
}
253+
254+
private static SolrRequest<?> createRequest(
255+
String apiVersion, String uriPath, SolrParams params) {
256+
if (apiVersion.equalsIgnoreCase("V1")) {
257+
return new GenericSolrRequest(SolrRequest.METHOD.GET, uriPath, params);
258+
}
259+
return new GenericV2SolrRequest(SolrRequest.METHOD.GET, uriPath, params);
260+
}
223261
}

0 commit comments

Comments
 (0)