Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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,40 @@
/*
* 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.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
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")})
Comment thread
igiguere marked this conversation as resolved.
})
StreamingOutput getMetrics();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.model;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import java.util.HashMap;
import java.util.Map;

/**
* Response from /api/metrics is actually a "prometheus" or "openmetrics" format
* (jakarta.ws.rs.core.StreamingOutput).
*
* <p>This class could be used if a json output is ever needed again?
*/
public class MetricsResponse extends SolrJerseyResponse {
Comment thread
igiguere marked this conversation as resolved.
Outdated

private Map<String, Object> metrics = new HashMap<>();

@JsonAnyGetter
public Map<String, Object> getMetrics() {
return metrics;
}

@JsonAnySetter
public void setMetric(String field, Object value) {
metrics.put(field, value);
}

public void setMetrics(Map<String, Object> metrics) {
this.metrics = metrics;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -863,11 +863,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");
Comment thread
igiguere marked this conversation as resolved.
Outdated
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
Comment thread
igiguere marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ public static boolean maybeProxyToNodes(

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

if (isPrometheusFormat) {
// Prometheus format: use singular 'node' parameter for single-node proxy
Expand Down Expand Up @@ -144,6 +147,9 @@ public static CompletableFuture<NamedList<Object>> callRemoteNode(
if ("prometheus".equals(wt) || "openmetrics".equals(wt)) {
proxyReq.setResponseParser(new InputStreamResponseParser(wt));
}
if (wt == null && uriPath.endsWith("/metrics")) {
proxyReq.setResponseParser(new InputStreamResponseParser("prometheus"));
}

try {
return zkController
Expand Down
160 changes: 20 additions & 140 deletions solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,22 @@

package org.apache.solr.handler.admin;

import io.prometheus.metrics.model.snapshots.CounterSnapshot;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
import io.prometheus.metrics.model.snapshots.InfoSnapshot;
import io.prometheus.metrics.model.snapshots.MetricSnapshot;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import org.apache.solr.api.JerseyResource;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.CommonTestInjection;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.admin.api.GetMetrics;
import org.apache.solr.metrics.MetricsUtil;
import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.metrics.otel.FilterablePrometheusMetricReader;
import org.apache.solr.request.SolrQueryRequest;
Expand All @@ -61,25 +55,9 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName
public static final String EXPR_PARAM = "expr";
public static final String TYPE_PARAM = "type";

// Prometheus filtering parameters
public static final String CATEGORY_PARAM = "category";
public static final String CORE_PARAM = "core";
public static final String COLLECTION_PARAM = "collection";
public static final String SHARD_PARAM = "shard";
public static final String REPLICA_TYPE_PARAM = "replica_type";
public static final String METRIC_NAME_PARAM = "name";
private static final Set<String> labelFilterKeys =
Set.of(CATEGORY_PARAM, CORE_PARAM, COLLECTION_PARAM, SHARD_PARAM, REPLICA_TYPE_PARAM);

public static final String PROMETHEUS_METRICS_WT = "prometheus";
public static final String OPEN_METRICS_WT = "openmetrics";

public static final String ALL = "all";

private static final Pattern KEY_SPLIT_REGEX =
Pattern.compile("(?<!" + Pattern.quote("\\") + ")" + Pattern.quote(":"));
private final CoreContainer cc;
private final Map<String, String> injectedSysProps = CommonTestInjection.injectAdditionalProps();
private final boolean enabled;

public MetricsHandler(CoreContainer coreContainer) {
Expand Down Expand Up @@ -115,7 +93,8 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw

if (format == null) {
req.setParams(SolrParams.wrapDefaults(params, SolrParams.of("wt", "prometheus")));
} else if (!PROMETHEUS_METRICS_WT.equals(format) && !OPEN_METRICS_WT.equals(format)) {
} else if (!MetricsUtil.PROMETHEUS_METRICS_WT.equals(format)
&& !MetricsUtil.OPEN_METRICS_WT.equals(format)) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
"Only Prometheus and OpenMetrics metric formats supported. Unsupported format requested: "
Expand All @@ -139,13 +118,13 @@ private void handleRequest(SolrParams params, BiConsumer<String, Object> consume
return;
}

Set<String> metricNames = readParamsAsSet(params, METRIC_NAME_PARAM);
SortedMap<String, Set<String>> labelFilters = labelFilters(params);
Set<String> metricNames = MetricsUtil.readParamsAsSet(params, MetricsUtil.METRIC_NAME_PARAM);
SortedMap<String, Set<String>> labelFilters = MetricsUtil.labelFilters(params);

if (metricNames.isEmpty() && labelFilters.isEmpty()) {
consumer.accept(
"metrics",
mergeSnapshots(
MetricsUtil.mergeSnapshots(
metricManager.getPrometheusMetricReaders().values().stream()
.flatMap(r -> r.collect().stream())
.toList()));
Expand All @@ -160,119 +139,10 @@ private void handleRequest(SolrParams params, BiConsumer<String, Object> consume
}

// Merge all filtered snapshots and return the merged result
MetricSnapshots mergedSnapshots = mergeSnapshots(allSnapshots);
MetricSnapshots mergedSnapshots = MetricsUtil.mergeSnapshots(allSnapshots);
consumer.accept("metrics", mergedSnapshots);
}

private SortedMap<String, Set<String>> labelFilters(SolrParams params) {
SortedMap<String, Set<String>> labelFilters = new TreeMap<>();
labelFilterKeys.forEach(
(paramName) -> {
Set<String> filterValues = readParamsAsSet(params, paramName);
if (!filterValues.isEmpty()) {
labelFilters.put(paramName, filterValues);
}
});

return labelFilters;
}

private Set<String> readParamsAsSet(SolrParams params, String paramName) {
String[] paramValues = params.getParams(paramName);
if (paramValues == null || paramValues.length == 0) {
return Set.of();
}

List<String> paramSet = new ArrayList<>();
for (String param : paramValues) {
paramSet.addAll(StrUtils.splitSmart(param, ','));
}
return Set.copyOf(paramSet);
}

/**
* Merge a collection of individual {@link MetricSnapshot} instances into one {@link
* MetricSnapshots}. This is necessary because we create a {@link
* io.opentelemetry.sdk.metrics.SdkMeterProvider} per Solr core resulting in duplicate metric
* names across cores which is an illegal format if under the same prometheus grouping.
*/
private MetricSnapshots mergeSnapshots(List<MetricSnapshot> snapshots) {
Map<String, CounterSnapshot.Builder> counterSnapshotMap = new HashMap<>();
Map<String, GaugeSnapshot.Builder> gaugeSnapshotMap = new HashMap<>();
Map<String, HistogramSnapshot.Builder> histogramSnapshotMap = new HashMap<>();
InfoSnapshot otelInfoSnapshots = null;

for (MetricSnapshot snapshot : snapshots) {
String metricName = snapshot.getMetadata().getPrometheusName();

switch (snapshot) {
case CounterSnapshot counterSnapshot -> {
CounterSnapshot.Builder builder =
counterSnapshotMap.computeIfAbsent(
metricName,
k -> {
var base =
CounterSnapshot.builder()
.name(counterSnapshot.getMetadata().getName())
.help(counterSnapshot.getMetadata().getHelp());
return counterSnapshot.getMetadata().hasUnit()
? base.unit(counterSnapshot.getMetadata().getUnit())
: base;
});
counterSnapshot.getDataPoints().forEach(builder::dataPoint);
}
case GaugeSnapshot gaugeSnapshot -> {
GaugeSnapshot.Builder builder =
gaugeSnapshotMap.computeIfAbsent(
metricName,
k -> {
var base =
GaugeSnapshot.builder()
.name(gaugeSnapshot.getMetadata().getName())
.help(gaugeSnapshot.getMetadata().getHelp());
return gaugeSnapshot.getMetadata().hasUnit()
? base.unit(gaugeSnapshot.getMetadata().getUnit())
: base;
});
gaugeSnapshot.getDataPoints().forEach(builder::dataPoint);
}
case HistogramSnapshot histogramSnapshot -> {
HistogramSnapshot.Builder builder =
histogramSnapshotMap.computeIfAbsent(
metricName,
k -> {
var base =
HistogramSnapshot.builder()
.name(histogramSnapshot.getMetadata().getName())
.help(histogramSnapshot.getMetadata().getHelp());
return histogramSnapshot.getMetadata().hasUnit()
? base.unit(histogramSnapshot.getMetadata().getUnit())
: base;
});
histogramSnapshot.getDataPoints().forEach(builder::dataPoint);
}
case InfoSnapshot infoSnapshot -> {
// InfoSnapshot is a special case in that each SdkMeterProvider will create a duplicate
// metric called target_info containing OTEL SDK metadata. Only one of these need to be
// kept
if (otelInfoSnapshots == null)
otelInfoSnapshots =
new InfoSnapshot(infoSnapshot.getMetadata(), infoSnapshot.getDataPoints());
}
default -> {
// Handle unexpected snapshot types gracefully
}
}
}

MetricSnapshots.Builder snapshotsBuilder = MetricSnapshots.builder();
counterSnapshotMap.values().forEach(b -> snapshotsBuilder.metricSnapshot(b.build()));
gaugeSnapshotMap.values().forEach(b -> snapshotsBuilder.metricSnapshot(b.build()));
histogramSnapshotMap.values().forEach(b -> snapshotsBuilder.metricSnapshot(b.build()));
if (otelInfoSnapshots != null) snapshotsBuilder.metricSnapshot(otelInfoSnapshots);
return snapshotsBuilder.build();
}

@Override
public String getDescription() {
return "A handler to return all the metrics gathered by Solr";
Expand All @@ -282,4 +152,14 @@ public String getDescription() {
public Category getCategory() {
return Category.ADMIN;
}

@Override
public Collection<Class<? extends JerseyResource>> getJerseyResources() {
return List.of(GetMetrics.class);
}

@Override
public Boolean registerV2() {
return Boolean.TRUE;
}
}
Loading
Loading