Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")})
})
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 @@ -875,7 +875,7 @@ public static void checkDiskSpace(
+ Utils.parseMetricsReplicaName(collection, parentShardLeader.getCoreName());

ModifiableSolrParams params =
new ModifiableSolrParams().add("name", indexSizeMetric).add("name", freeDiskSpaceMetric);
new ModifiableSolrParams().add("name", indexSizeMetric).add("name", freeDiskSpaceMetric);

var req = new MetricsRequest(params);
req.setResponseParser(new InputStreamResponseParser("prometheus"));
Expand Down
6 changes: 2 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 @@ -3103,8 +3101,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(MetricUtils.PROMETHEUS_METRICS_WT, new PrometheusResponseWriter());
m.put(MetricUtils.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,15 +33,18 @@
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.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.util.stats.MetricUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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 =
MetricUtils.PROMETHEUS_METRICS_WT.equals(wt)
|| MetricUtils.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 (MetricUtils.PROMETHEUS_METRICS_WT.equals(wt) || MetricUtils.OPEN_METRICS_WT.equals(wt)) {
proxyReq.setResponseParser(new InputStreamResponseParser(wt));
}
if (wt == null && uriPath.endsWith("/metrics")) {
proxyReq.setResponseParser(new InputStreamResponseParser(MetricUtils.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