Skip to content
8 changes: 8 additions & 0 deletions changelog/unreleased/SOLR-17436-v2-metrics-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
title: Create a v2 equivalent for /admin/metrics
type: added # added, changed, fixed, deprecated, removed, dependency_update, security, other
authors:
- name: Isabelle Giguère
links:
- name: SOLR-17436
url: https://issues.apache.org/jira/browse/SOLR-17436
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.client.api.endpoint;

import static org.apache.solr.client.api.util.Constants.RAW_OUTPUT_PROPERTY;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.StreamingOutput;

/** V2 API definitions to fetch metrics. */
@Path("/metrics")
public interface MetricsApi {

@GET
@Operation(
summary = "Retrieve metrics gathered by Solr.",
tags = {"metrics"},
extensions = {
@Extension(properties = {@ExtensionProperty(name = RAW_OUTPUT_PROPERTY, value = "true")})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[+1] Just came here from SOLR-17436 where you left a comment or two about struggling to get the template-generated code to compile. Glad you were able to find "StreamingOutput" and this property, that's exactly the right approach!

})
StreamingOutput getMetrics(
@HeaderParam("Accept") String acceptHeader,
@Parameter(
schema =
@Schema(
name = "node",
description = "Name of the node to which proxy the request.",
defaultValue = "all"))
@QueryParam(value = "node")
String node,
@Parameter(schema = @Schema(name = "name", description = "The metric name to filter on."))
@QueryParam(value = "name")
String name,
@Parameter(
schema = @Schema(name = "category", description = "The category label to filter on."))
@QueryParam(value = "category")
String category,
@Parameter(
schema =
@Schema(
name = "core",
description =
"TThe core name to filter on. More than one core can be specified in a comma-separated list."))
@QueryParam(value = "core")
String core,
@Parameter(
schema =
@Schema(name = "collection", description = "The collection name to filter on. "))
@QueryParam(value = "collection")
String collection,
@Parameter(schema = @Schema(name = "shard", description = "The shard name to filter on."))
@QueryParam(value = "shard")
String shard,
@Parameter(
schema =
@Schema(
name = "replica_type",
description = "The replica type to filter on.",
allowableValues = {"NRT", "TLOG", "PULL"}))
@QueryParam(value = "replica_type")
String replicaType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -874,11 +874,16 @@ public static void checkDiskSpace(
new ModifiableSolrParams()
.add("key", indexSizeMetricName)
.add("key", freeDiskSpaceMetricName);
// TODO: SOLR-17955
SolrResponse rsp = new MetricsRequest(params).process(cloudManager.getSolrClient());
if (rsp == null) {
log.warn("No Solr response available from parent shard leader.");
return;
}

Number size = (Number) rsp.getResponse()._get(List.of("metrics", indexSizeMetricName), null);
if (size == null) {
log.warn("cannot verify information for parent shard leader");
log.warn("missing index size information for parent shard leader");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[+1] Great little logging clarification; thanks!

return;
}
double indexSize = size.doubleValue();
Expand Down
7 changes: 3 additions & 4 deletions solr/core/src/java/org/apache/solr/core/SolrCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package org.apache.solr.core;

import static org.apache.solr.common.params.CommonParams.PATH;
import static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT;
import static org.apache.solr.handler.admin.MetricsHandler.PROMETHEUS_METRICS_WT;
import static org.apache.solr.metrics.SolrCoreMetricManager.COLLECTION_ATTR;
import static org.apache.solr.metrics.SolrCoreMetricManager.CORE_ATTR;
import static org.apache.solr.metrics.SolrCoreMetricManager.REPLICA_TYPE_ATTR;
Expand Down Expand Up @@ -117,6 +115,7 @@
import org.apache.solr.handler.component.HighlightComponent;
import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.logging.MDCLoggingContext;
import org.apache.solr.metrics.MetricsUtil;
import org.apache.solr.metrics.SolrCoreMetricManager;
import org.apache.solr.metrics.SolrMetricProducer;
import org.apache.solr.metrics.SolrMetricsContext;
Expand Down Expand Up @@ -3103,8 +3102,8 @@ public PluginBag<QueryResponseWriter> getResponseWriters() {
m.put("csv", new CSVResponseWriter());
m.put("schema.xml", new SchemaXmlResponseWriter());
m.put("smile", new SmileResponseWriter());
m.put(PROMETHEUS_METRICS_WT, new PrometheusResponseWriter());
m.put(OPEN_METRICS_WT, new PrometheusResponseWriter());
m.put(MetricsUtil.PROMETHEUS_METRICS_WT, new PrometheusResponseWriter());
m.put(MetricsUtil.OPEN_METRICS_WT, new PrometheusResponseWriter());
m.put(ReplicationAPIBase.FILE_STREAM, getFileStreamWriter());
DEFAULT_RESPONSE_WRITERS = Collections.unmodifiableMap(m);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.client.solrj.request.GenericV2SolrRequest;
import org.apache.solr.client.solrj.response.InputStreamResponseParser;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.metrics.MetricsUtil;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.slf4j.Logger;
Expand All @@ -55,17 +58,34 @@ public class AdminHandlersProxy {
private static final String PARAM_NODE = "node";
private static final long PROMETHEUS_FETCH_TIMEOUT_SECONDS = 10;

/** Proxy this request to a different remote node if 'node' or 'nodes' parameter is provided */
/**
* Proxy this request to a different remote node's V1 API if 'node' or 'nodes' parameter is
* provided. For V2, use {@link AdminHandlersProxy#maybeProxyToNodes(String, SolrQueryRequest,
* SolrQueryResponse, CoreContainer)}
*/
public static boolean maybeProxyToNodes(
SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer container)
throws IOException, SolrServerException, InterruptedException {
return maybeProxyToNodes("V1", req, rsp, container);
}

/**
* Proxy this request to a different remote node's selected API version if 'node' or 'nodes'
* parameter is provided
*/
public static boolean maybeProxyToNodes(
String apiVersion, SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer container)
throws IOException, SolrServerException, InterruptedException {

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

// Check if response format is Prometheus/OpenMetrics
String wt = params.get("wt");
boolean isPrometheusFormat = "prometheus".equals(wt) || "openmetrics".equals(wt);
String wt = params.get(CommonParams.WT);
boolean isPrometheusFormat =
MetricsUtil.PROMETHEUS_METRICS_WT.equals(wt)
|| MetricsUtil.OPEN_METRICS_WT.equals(wt)
|| (wt == null && pathStr.endsWith("/metrics"));

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

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

params.remove(PARAM_NODES);
Set<String> nodes = resolveNodes(nodeNames, container);
handleNamedListFormat(nodes, pathStr, params, container.getZkController(), rsp);
handleNamedListFormat(apiVersion, nodes, pathStr, params, container.getZkController(), rsp);
}

return true;
}

/** Handle non-Prometheus formats using the existing NamedList approach. */
private static void handleNamedListFormat(
String apiVersion,
Set<String> nodes,
String pathStr,
SolrParams params,
Expand All @@ -101,7 +122,7 @@ private static void handleNamedListFormat(

Map<String, Future<NamedList<Object>>> responses = new LinkedHashMap<>();
for (String node : nodes) {
responses.put(node, callRemoteNode(node, pathStr, params, zkController));
responses.put(node, callRemoteNode(apiVersion, node, pathStr, params, zkController));
}

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

/** Makes a remote request asynchronously. */
public static CompletableFuture<NamedList<Object>> callRemoteNode(
String nodeName, String uriPath, SolrParams params, ZkController zkController) {
private static CompletableFuture<NamedList<Object>> callRemoteNode(
String apiVersion,
String nodeName,
String uriPath,
SolrParams params,
ZkController zkController) {

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

log.debug("Proxying {} request to node {}", uriPath, nodeName);
URI baseUri = URI.create(zkController.zkStateReader.getBaseUrlForNodeName(nodeName));
SolrRequest<?> proxyReq = new GenericSolrRequest(SolrRequest.METHOD.GET, uriPath, params);

SolrRequest<?> proxyReq = createRequest(apiVersion, uriPath, params);

// Set response parser based on wt parameter to ensure correct format is used
String wt = params.get("wt");
if ("prometheus".equals(wt) || "openmetrics".equals(wt)) {
String wt = params.get(CommonParams.WT);
if (MetricsUtil.PROMETHEUS_METRICS_WT.equals(wt) || MetricsUtil.OPEN_METRICS_WT.equals(wt)) {
proxyReq.setResponseParser(new InputStreamResponseParser(wt));
}
if (wt == null && uriPath.endsWith("/metrics")) {
proxyReq.setResponseParser(new InputStreamResponseParser(MetricsUtil.PROMETHEUS_METRICS_WT));
}

try {
return zkController
Expand Down Expand Up @@ -195,6 +224,7 @@ private static Set<String> resolveNodes(String nodeNames, CoreContainer containe
* @param rsp the response to populate
*/
private static void handlePrometheusSingleNode(
String apiVersion,
String nodeName,
String pathStr,
ModifiableSolrParams params,
Expand All @@ -205,7 +235,7 @@ private static void handlePrometheusSingleNode(
// Keep wt=prometheus for the remote request so MetricsHandler accepts it
// The InputStreamResponseParser will return the Prometheus text in a "stream" key
Future<NamedList<Object>> response =
callRemoteNode(nodeName, pathStr, params, container.getZkController());
callRemoteNode(apiVersion, nodeName, pathStr, params, container.getZkController());

try {
try {
Expand All @@ -220,4 +250,12 @@ private static void handlePrometheusSingleNode(
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, t);
}
}

private static SolrRequest<?> createRequest(
String apiVersion, String uriPath, SolrParams params) {
if (apiVersion.equalsIgnoreCase("V1")) {
return new GenericSolrRequest(SolrRequest.METHOD.GET, uriPath, params);
}
return new GenericV2SolrRequest(SolrRequest.METHOD.GET, uriPath, params);
}
}
Loading