This is an automated email from the ASF dual-hosted git repository. mlbiscoc pushed a commit to branch feature/SOLR-17458-rebased in repository https://gitbox.apache.org/repos/asf/solr.git
commit 046b206de34417bb672b335fe607d90dda586e39 Author: Matthew Biscocho <[email protected]> AuthorDate: Fri Sep 5 12:57:31 2025 -0400 SOLR-17854: Support filters to /admin/metrics endpoint for Prometheus metrics (#3499) * Add fitlering for /admin/metrics * Rename vars * Use a generic method for handling the 3 types metric types * Optimize requiredLabelsFilter * Add tests for MetricsHandlerTest * Add tests around FilterablePrometheusMetricReader * static member * Fix tests --------- Co-authored-by: David Smiley <[email protected]> Co-authored-by: Matthew Biscocho <[email protected]> --- .../src/java/org/apache/solr/core/SolrCore.java | 15 +- .../apache/solr/handler/admin/MetricsHandler.java | 159 ++++- .../org/apache/solr/metrics/SolrMetricManager.java | 12 +- .../otel/FilterablePrometheusMetricReader.java | 185 +++++ .../solr/response/PrometheusResponseWriter.java | 113 +--- .../solr/handler/admin/MetricsHandlerTest.java | 751 ++++----------------- .../otel/FilterablePrometheusMetricReaderTest.java | 95 +++ .../apache/solr/update/SolrIndexMetricsTest.java | 2 +- 8 files changed, 586 insertions(+), 746 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java index ab4db7d04f2..a8cb4800fde 100644 --- a/solr/core/src/java/org/apache/solr/core/SolrCore.java +++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java @@ -1422,14 +1422,13 @@ public class SolrCore implements SolrInfoBean, Closeable { }), OtelUnit.BYTES)); - observables.add( - parentContext.observableLongGauge( - "solr_core_segment_count", - "Number of segments in a Solr core", - (observableLongMeasurement -> { - if (isReady()) - observableLongMeasurement.record(getSegmentCount(), baseGaugeCoreAttributes); - }))); + parentContext.observableLongGauge( + "solr_core_segments", + "Number of segments in a Solr core", + (observableLongMeasurement -> { + if (isReady()) + observableLongMeasurement.record(getSegmentCount(), baseGaugeCoreAttributes); + })); // NOCOMMIT: Do we need these start_time metrics? I think at minimum it should be optional // otherwise we fall into metric bloat for something people may not care about. diff --git a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java index 633ccc7bb00..143cf453f41 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java @@ -25,14 +25,23 @@ import com.codahale.metrics.Metric; import com.codahale.metrics.MetricFilter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +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.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.TreeSet; import java.util.function.BiConsumer; import java.util.function.Predicate; @@ -49,6 +58,7 @@ import org.apache.solr.common.util.StrUtils; import org.apache.solr.core.CoreContainer; import org.apache.solr.handler.RequestHandlerBase; import org.apache.solr.metrics.SolrMetricManager; +import org.apache.solr.metrics.otel.FilterablePrometheusMetricReader; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrRequestInfo; import org.apache.solr.response.SolrQueryResponse; @@ -69,6 +79,16 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName public static final String KEY_PARAM = "key"; 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_PARAM = "replica"; + 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_PARAM); + // NOCOMMIT: This wt=prometheus will be removed as it will become the default for /admin/metrics public static final String PROMETHEUS_METRICS_WT = "prometheus"; public static final String OPEN_METRICS_WT = "openmetrics"; @@ -131,7 +151,7 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName // NOCOMMIT SOLR-17458: Make this the default option after dropwizard removal if (PROMETHEUS_METRICS_WT.equals(params.get(CommonParams.WT)) || OPEN_METRICS_WT.equals(params.get(CommonParams.WT))) { - consumer.accept("metrics", metricManager.getPrometheusMetricReaders()); + handlePrometheusRequest(params, consumer); return; } @@ -151,6 +171,58 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName consumer.accept("metrics", response); } + private void handlePrometheusRequest(SolrParams params, BiConsumer<String, Object> consumer) { + Set<String> metricNames = readParamsAsSet(params, METRIC_NAME_PARAM); + SortedMap<String, Set<String>> labelFilters = labelFilters(params); + + if (metricNames.isEmpty() && labelFilters.isEmpty()) { + consumer.accept( + "metrics", + mergeSnapshots( + metricManager.getPrometheusMetricReaders().values().stream() + .flatMap(r -> r.collect().stream()) + .toList())); + return; + } + + List<MetricSnapshot> allSnapshots = new ArrayList<>(); + for (FilterablePrometheusMetricReader reader : + metricManager.getPrometheusMetricReaders().values()) { + MetricSnapshots filteredSnapshots = reader.collect(metricNames, labelFilters); + filteredSnapshots.forEach(allSnapshots::add); + } + + // Merge all filtered snapshots and return the merged result + MetricSnapshots mergedSnapshots = 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); + } + private NamedList<Object> handleDropwizardRegistry(SolrParams params) { boolean compact = params.getBool(COMPACT_PARAM, true); MetricFilter mustMatchFilter = parseMustMatchFilter(params); @@ -182,6 +254,7 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName return response; } + // NOCOMMIT: Remove this filtering logic private static class MetricsExpr { Pattern registryRegex; MetricFilter metricFilter; @@ -499,9 +572,87 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName return metricTypes; } - private String getCoreNameFromRegistry(String registryName) { - String coreName = registryName.substring(registryName.indexOf('.') + 1); - return coreName.replace(".", "_"); + /** + * 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 diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java index bda5fa4af39..4ddc6443f80 100644 --- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java +++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java @@ -52,7 +52,6 @@ import io.opentelemetry.api.metrics.ObservableLongGauge; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; import io.opentelemetry.api.metrics.ObservableMeasurement; -import io.opentelemetry.exporter.prometheus.PrometheusMetricReader; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; @@ -86,6 +85,7 @@ import org.apache.solr.core.SolrCore; import org.apache.solr.core.SolrInfoBean; import org.apache.solr.core.SolrResourceLoader; import org.apache.solr.logging.MDCLoggingContext; +import org.apache.solr.metrics.otel.FilterablePrometheusMetricReader; import org.apache.solr.metrics.otel.OtelUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -753,7 +753,7 @@ public class SolrMetricManager { .computeIfAbsent( providerName, key -> { - var reader = new PrometheusMetricReader(true, null); + var reader = new FilterablePrometheusMetricReader(true, null); // NOCOMMIT: We need to add a Periodic Metric Reader here if we want to push with OTLP // with an exporter var provider = SdkMeterProvider.builder().registerMetricReader(reader); @@ -1640,17 +1640,17 @@ public class SolrMetricManager { return metricsConfig; } - /** Get a shallow copied map of {@link PrometheusMetricReader}. */ - public Map<String, PrometheusMetricReader> getPrometheusMetricReaders() { + /** Get a shallow copied map of {@link FilterablePrometheusMetricReader}. */ + public Map<String, FilterablePrometheusMetricReader> getPrometheusMetricReaders() { return meterProviderAndReaders.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().prometheusMetricReader())); } - public PrometheusMetricReader getPrometheusMetricReader(String providerName) { + public FilterablePrometheusMetricReader getPrometheusMetricReader(String providerName) { MeterProviderAndReaders mpr = meterProviderAndReaders.get(enforcePrefix(providerName)); return (mpr != null) ? mpr.prometheusMetricReader() : null; } private record MeterProviderAndReaders( - SdkMeterProvider sdkMeterProvider, PrometheusMetricReader prometheusMetricReader) {} + SdkMeterProvider sdkMeterProvider, FilterablePrometheusMetricReader prometheusMetricReader) {} } diff --git a/solr/core/src/java/org/apache/solr/metrics/otel/FilterablePrometheusMetricReader.java b/solr/core/src/java/org/apache/solr/metrics/otel/FilterablePrometheusMetricReader.java new file mode 100644 index 00000000000..4cd321c112f --- /dev/null +++ b/solr/core/src/java/org/apache/solr/metrics/otel/FilterablePrometheusMetricReader.java @@ -0,0 +1,185 @@ +/* + * 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.metrics.otel; + +import static java.util.stream.Collectors.toList; + +import io.opentelemetry.exporter.prometheus.PrometheusMetricReader; +import io.prometheus.metrics.model.snapshots.CounterSnapshot; +import io.prometheus.metrics.model.snapshots.DataPointSnapshot; +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.Labels; +import io.prometheus.metrics.model.snapshots.MetricSnapshot; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FilterablePrometheusMetricReader extends PrometheusMetricReader { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final Set<String> PROM_SUFFIXES = + Set.of("_total", "_sum", "_count", "_bucket", "_gcount", "_gsum", "_created", "_info"); + + public FilterablePrometheusMetricReader( + boolean otelScopeEnabled, Predicate<String> allowedResourceAttributesFilter) { + super(otelScopeEnabled, allowedResourceAttributesFilter); + } + + /** + * Collect metrics with filtering support for metric names and labels. + * + * @param includedNames Set of metric names to include. If empty, all metric names are included. + * @param requiredLabels Map of label names to their allowed values. If empty, no label filtering + * is applied. + * @return Filtered MetricSnapshots + */ + public MetricSnapshots collect( + Set<String> includedNames, SortedMap<String, Set<String>> requiredLabels) { + + // If no filtering is requested then return all metrics + if (includedNames.isEmpty() && requiredLabels.isEmpty()) { + return super.collect(); + } + + // Prometheus appends a suffix to the metrics depending on the metric type. We need to sanitize + // the suffix off if they filter by Prometheus name instead of OTEL name. + Set<String> sanitizedNames = + includedNames.stream() + .map( + (name) -> { + for (String suffix : PROM_SUFFIXES) { + if (name.endsWith(suffix)) { + return name.substring(0, name.lastIndexOf(suffix)); + } + } + return name; + }) + .collect(Collectors.toSet()); + + MetricSnapshots snapshotsToFilter; + if (sanitizedNames.isEmpty()) { + snapshotsToFilter = super.collect(); + } else { + snapshotsToFilter = super.collect(sanitizedNames::contains); + } + + // Return named filtered snapshots if not label filters provided + if (requiredLabels.isEmpty()) { + return snapshotsToFilter; + } + + MetricSnapshots.Builder filteredSnapshots = MetricSnapshots.builder(); + for (MetricSnapshot metricSnapshot : snapshotsToFilter) { + switch (metricSnapshot) { + case CounterSnapshot snapshot -> { + List<CounterSnapshot.CounterDataPointSnapshot> filtered = + filterDatapoint( + snapshot, requiredLabels, CounterSnapshot.CounterDataPointSnapshot.class); + if (!filtered.isEmpty()) { + filteredSnapshots.metricSnapshot(new CounterSnapshot(snapshot.getMetadata(), filtered)); + } + } + case HistogramSnapshot snapshot -> { + List<HistogramSnapshot.HistogramDataPointSnapshot> filtered = + filterDatapoint( + snapshot, requiredLabels, HistogramSnapshot.HistogramDataPointSnapshot.class); + if (!filtered.isEmpty()) { + filteredSnapshots.metricSnapshot( + new HistogramSnapshot(snapshot.getMetadata(), filtered)); + } + } + case GaugeSnapshot snapshot -> { + List<GaugeSnapshot.GaugeDataPointSnapshot> filtered = + filterDatapoint(snapshot, requiredLabels, GaugeSnapshot.GaugeDataPointSnapshot.class); + if (!filtered.isEmpty()) { + filteredSnapshots.metricSnapshot(new GaugeSnapshot(snapshot.getMetadata(), filtered)); + } + } + case InfoSnapshot ignored -> { + // Do nothing for InfoSnapshots. Always filter it out + } + default -> { + log.error("Unknown metric snapshot type {}", metricSnapshot.getClass()); + } + } + } + return filteredSnapshots.build(); + } + + private <D extends DataPointSnapshot> List<D> filterDatapoint( + MetricSnapshot snapshot, + SortedMap<String, Set<String>> requiredLabels, + Class<D> dataPointClass) { + return snapshot.getDataPoints().stream() + .filter(dp -> requiredLabelsFilter(dp.getLabels(), requiredLabels)) + .map(dataPointClass::cast) + .collect(toList()); + } + + static boolean requiredLabelsFilter( + Labels labels, SortedMap<String, Set<String>> requiredLabels) { + // Both Labels and requiredLabels are name-sorted with unique names. + // For each required label, scan forward through labels until we find it. + int labelIdx = 0; + requireLoop: + for (Map.Entry<String, Set<String>> entry : requiredLabels.entrySet()) { + String requiredLabelName = entry.getKey(); + Set<String> allowedValues = entry.getValue(); + + // Labels are sorted, so we can continue from the last position + while (labelIdx < labels.size()) { + String labelName = labels.getName(labelIdx); + + int comparison = labelName.compareTo(requiredLabelName); + + if (comparison < 0) { + // a label before requiredLabelName; we don't care about this one + assert !requiredLabels.containsKey(labelName); + labelIdx++; + continue; // inspect the next input label + } + if (comparison > 0) { + // We've passed where requiredLabelName should be, so it doesn't exist + assert !labels.contains(requiredLabelName); + return false; + } + + assert labels.contains(requiredLabelName); + if (!allowedValues.contains(labels.getValue(labelIdx))) { + return false; + } + // we satisfied requiredLabelName with its allowedValues. Move onto next + labelIdx++; + continue requireLoop; + } // end while loop on input Labels + // didn't find requiredLabelName; ran out of names + assert !labels.contains(requiredLabelName); + return false; + } + return true; // found all requiredLabels in Labels + } +} diff --git a/solr/core/src/java/org/apache/solr/response/PrometheusResponseWriter.java b/solr/core/src/java/org/apache/solr/response/PrometheusResponseWriter.java index 070ed662dc0..a5f69e1632b 100644 --- a/solr/core/src/java/org/apache/solr/response/PrometheusResponseWriter.java +++ b/solr/core/src/java/org/apache/solr/response/PrometheusResponseWriter.java @@ -18,27 +18,14 @@ package org.apache.solr.response; import static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT; -import io.opentelemetry.exporter.prometheus.PrometheusMetricReader; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; -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.io.IOException; import java.io.OutputStream; -import java.lang.invoke.MethodHandles; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.apache.solr.common.params.CommonParams; import org.apache.solr.handler.admin.MetricsHandler; import org.apache.solr.request.SolrQueryRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** Response writer for Prometheus metrics. This is used only by the {@link MetricsHandler} */ @SuppressWarnings(value = "unchecked") @@ -49,23 +36,17 @@ public class PrometheusResponseWriter implements QueryResponseWriter { private static final String CONTENT_TYPE_OPEN_METRICS = "application/openmetrics-text; version=1.0.0; charset=utf-8"; - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @Override public void write( OutputStream out, SolrQueryRequest request, SolrQueryResponse response, String contentType) throws IOException { - Map<String, PrometheusMetricReader> readers = - (Map<String, PrometheusMetricReader>) response.getValues().get("metrics"); - - List<MetricSnapshot> snapshots = - readers.values().stream().flatMap(r -> r.collect().stream()).toList(); - + var metrics = response.getValues().get("metrics"); + MetricSnapshots snapshots = (MetricSnapshots) metrics; if (writeOpenMetricsFormat(request)) { - new OpenMetricsTextFormatWriter(false, true).write(out, mergeSnapshots(snapshots)); + new OpenMetricsTextFormatWriter(false, true).write(out, snapshots); } else { - new PrometheusTextFormatWriter(false).write(out, mergeSnapshots(snapshots)); + new PrometheusTextFormatWriter(false).write(out, snapshots); } } @@ -92,90 +73,4 @@ public class PrometheusResponseWriter implements QueryResponseWriter { return acceptHeader.contains("application/openmetrics-text") && (acceptHeader.contains("version=1.0.0")); } - - /** - * Merge a collection of individual {@link MetricSnapshot} instances into one {@link - * MetricSnapshots}. This is necessary because we create a {@link SdkMeterProvider} per Solr core - * resulting in duplicate metric names across cores which is an illegal format if not 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 -> { - log.warn( - "Unexpected snapshot type: {} for metric {}", - snapshot.getClass().getName(), - snapshot.getMetadata().getName()); - } - } - } - - 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(); - } } diff --git a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java index 0878bf2c8cd..6cb09d3d95d 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java @@ -17,33 +17,19 @@ package org.apache.solr.handler.admin; -import com.codahale.metrics.Counter; import io.opentelemetry.api.common.Attributes; -import io.prometheus.metrics.model.snapshots.CounterSnapshot; -import io.prometheus.metrics.model.snapshots.GaugeSnapshot; -import io.prometheus.metrics.model.snapshots.Labels; -import io.prometheus.metrics.model.snapshots.MetricSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.SummarySnapshot; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.solr.SolrTestCaseJ4; -import org.apache.solr.common.MapWriter; import org.apache.solr.common.params.CommonParams; -import org.apache.solr.common.util.NamedList; -import org.apache.solr.common.util.SimpleOrderedMap; -import org.apache.solr.core.PluginBag; -import org.apache.solr.core.PluginInfo; import org.apache.solr.handler.RequestHandlerBase; import org.apache.solr.metrics.MetricsMap; import org.apache.solr.metrics.SolrMetricsContext; import org.apache.solr.request.SolrQueryRequest; -import org.apache.solr.request.SolrRequestHandler; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.security.AuthorizationContext; -import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -51,699 +37,228 @@ import org.junit.Test; public class MetricsHandlerTest extends SolrTestCaseJ4 { @BeforeClass public static void beforeClass() throws Exception { - initCore("solrconfig-minimal.xml", "schema.xml"); h.getCoreContainer().waitForLoadingCoresToFinish(30000); - - // manually register & seed some metrics in solr.jvm and solr.jetty for testing via handler - // (use "solrtest_" prefix just in case the jvm or jetty adds a "foo" metric at some point) - Counter c = h.getCoreContainer().getMetricManager().counter(null, "solr.jvm", "solrtest_foo"); - c.inc(); - c = h.getCoreContainer().getMetricManager().counter(null, "solr.jetty", "solrtest_foo"); - c.inc(2); - // test escapes - c = h.getCoreContainer().getMetricManager().counter(null, "solr.jetty", "solrtest_foo:bar"); - c.inc(3); - - h.getCoreContainer() - .getMetricManager() - .meter(null, "solr.jetty", "org.eclipse.jetty.server.handler.DefaultHandler.2xx-responses"); - h.getCoreContainer() - .getMetricManager() - .counter( - null, "solr.jetty", "org.eclipse.jetty.server.handler.DefaultHandler.active-requests"); - h.getCoreContainer() - .getMetricManager() - .timer(null, "solr.jetty", "org.eclipse.jetty.server.handler.DefaultHandler.dispatches"); - } - - @AfterClass - public static void cleanupMetrics() { - if (null != h) { - h.getCoreContainer().getMetricManager().registry("solr.jvm").remove("solrtest_foo"); - h.getCoreContainer().getMetricManager().registry("solr.jetty").remove("solrtest_foo"); - h.getCoreContainer().getMetricManager().registry("solr.jetty").remove("solrtest_foo:bar"); - } } - // NOCOMMIT: This test does a bunch of /admin/metrics calls with various params, with various - // filters and parameters. We have not migrated all the metrics to otel yet or even created any - // filters. Once that is done, we should revisit this test and assert the prometheus response. @Test - @BadApple(bugUrl = "https://issues.apache.org/jira/browse/SOLR-17458") - public void test() throws Exception { + public void testMetricNamesFiltering() throws Exception { + String expectedRequestsMetricName = "solr_core_requests"; MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); + assertQ(req("*:*"), "//result[@numFound='0']"); + SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", CommonParams.WT, - "json"), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.METRIC_NAME_PARAM, + expectedRequestsMetricName), resp); - NamedList<?> values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - NamedList<?> nl = (NamedList<?>) values.get("solr.core.collection1"); - assertNotNull(nl); - Object o = nl.get("SEARCHER.new.errors"); - assertNotNull(o); // counter type - assertTrue(o instanceof MapWriter); - // response wasn't serialized, so we get here whatever MetricUtils produced instead of NamedList - assertNotNull(((MapWriter) o)._get("count")); - assertEquals(0L, ((MapWriter) nl.get("SEARCHER.new.errors"))._get("count")); - assertNotNull(nl.get("INDEX.segments")); // int gauge - assertTrue((int) ((MapWriter) nl.get("INDEX.segments"))._get("value") >= 0); - assertNotNull(nl.get("INDEX.sizeInBytes")); // long gauge - assertTrue((long) ((MapWriter) nl.get("INDEX.sizeInBytes"))._get("value") >= 0); - nl = (NamedList<?>) values.get("solr.node"); - assertNotNull(nl.get("CONTAINER.cores.loaded")); // int gauge - assertEquals(1, ((MapWriter) nl.get("CONTAINER.cores.loaded"))._get("value")); - assertNotNull(nl.get("ADMIN./admin/authorization.clientErrors")); // timer type - Map<String, Object> map = new HashMap<>(); - ((MapWriter) nl.get("ADMIN./admin/authorization.clientErrors")).toMap(map); - assertEquals(5, map.size()); - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "group", - "jvm,jetty"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(2, values.size()); - assertNotNull(values.get("solr.jetty")); - assertNotNull(values.get("solr.jvm")); - - resp = new SolrQueryResponse(); - // "collection" works too, because it's a prefix for "collection1" - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "registry", - "solr.core.collection,solr.jvm"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(2, values.size()); - assertNotNull(values.get("solr.core.collection1")); - assertNotNull(values.get("solr.jvm")); - - resp = new SolrQueryResponse(); - // "collection" works too, because it's a prefix for "collection1" - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "registry", - "solr.core.collection", - "registry", - "solr.jvm"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(2, values.size()); - assertNotNull(values.get("solr.core.collection1")); - assertNotNull(values.get("solr.jvm")); - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "group", - "jvm,jetty"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(2, values.size()); - assertNotNull(values.get("solr.jetty")); - assertNotNull(values.get("solr.jvm")); - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "group", - "jvm", - "group", - "jetty"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(2, values.size()); - assertNotNull(values.get("solr.jetty")); - assertNotNull(values.get("solr.jvm")); - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "group", - "node", - "type", - "counter"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(1, values.size()); - values = (NamedList<?>) values.get("solr.node"); - assertNotNull(values); - assertNull(values.get("ADMIN./admin/authorization.errors")); // this is a timer node - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "prefix", - "CONTAINER.cores,CONTAINER.threadPool"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(1, values.size()); - assertNotNull(values.get("solr.node")); - values = (NamedList<?>) values.get("solr.node"); - assertEquals(15, values.size()); - assertNotNull(values.get("CONTAINER.cores.lazy")); // this is a gauge node - assertNotNull(values.get("CONTAINER.threadPool.coreLoadExecutor.completed")); - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "prefix", - "CONTAINER.cores", - "regex", - "C.*thread.*completed"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertNotNull(values.get("solr.node")); - values = (NamedList<?>) values.get("solr.node"); - assertEquals(5, values.size()); - assertNotNull(values.get("CONTAINER.threadPool.coreLoadExecutor.completed")); - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - CommonParams.WT, - "json", - "prefix", - "CACHE.core.fieldCache", - "property", - "entries_count", - MetricsHandler.COMPACT_PARAM, - "true"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertNotNull(values.get("solr.core.collection1")); - values = (NamedList<?>) values.get("solr.core.collection1"); - assertEquals(1, values.size()); - MapWriter writer = (MapWriter) values.get("CACHE.core.fieldCache"); - assertNotNull(writer); - assertNotNull(writer._get("entries_count")); - - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "group", - "jvm", - "prefix", - "CONTAINER.cores"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - assertEquals(0, values.size()); + var metrics = resp.getValues().get("metrics"); + MetricSnapshots snapshots = (MetricSnapshots) metrics; + assertEquals(1, snapshots.size()); + assertEquals(expectedRequestsMetricName, snapshots.get(0).getMetadata().getPrometheusName()); - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - MetricsHandler.COMPACT_PARAM, - "false", - CommonParams.WT, - "json", - "group", - "node", - "type", - "timer", - "prefix", - "CONTAINER.cores"), - resp); - values = resp.getValues(); - assertNotNull(values.get("metrics")); - SimpleOrderedMap<?> map1 = (SimpleOrderedMap<?>) values.get("metrics"); - assertEquals(0, map1.size()); handler.close(); } @Test - public void testPropertyFilter() throws Exception { - assertQ(req("*:*"), "//result[@numFound='0']"); - + public void testMultipleMetricNamesFiltering() throws Exception { + String expectedRequestsMetricName = "solr_core_requests"; + String expectedSearcherMetricName = "solr_core_searcher_new"; + var expected = Set.of(expectedRequestsMetricName, expectedSearcherMetricName); MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); + assertQ(req("*:*"), "//result[@numFound='0']"); + SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", CommonParams.WT, - "json", - MetricsHandler.COMPACT_PARAM, - "true", - "group", - "core", - "prefix", - "CACHE.searcher"), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.METRIC_NAME_PARAM, + expectedRequestsMetricName + "," + expectedSearcherMetricName), resp); - NamedList<?> values = resp.getValues(); - assertNotNull(values.get("metrics")); - values = (NamedList<?>) values.get("metrics"); - NamedList<?> nl = (NamedList<?>) values.get("solr.core.collection1"); - assertNotNull(nl); - assertTrue(nl.size() > 0); - nl.forEach( - (k, v) -> { - assertTrue(v instanceof MapWriter); - Map<String, Object> map = new HashMap<>(); - ((MapWriter) v).toMap(map); - assertTrue(map.size() > 2); - }); - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - CommonParams.WT, - "json", - MetricsHandler.COMPACT_PARAM, - "true", - "group", - "core", - "prefix", - "CACHE.searcher", - "property", - "inserts", - "property", - "size"), - resp); - values = resp.getValues(); - values = (NamedList<?>) values.get("metrics"); - nl = (NamedList<?>) values.get("solr.core.collection1"); - assertNotNull(nl); - assertTrue(nl.size() > 0); - nl.forEach( - (k, v) -> { - assertTrue(v instanceof MapWriter); - Map<String, Object> map = new HashMap<>(); - ((MapWriter) v).toMap(map); - assertEquals("k=" + k + ", v=" + map, 2, map.size()); - assertNotNull(map.get("inserts")); - assertNotNull(map.get("size")); - }); + var metrics = (MetricSnapshots) resp.getValues().get("metrics"); + assertEquals(2, metrics.size()); + Set<String> actual = + metrics.stream().map(m -> m.getMetadata().getPrometheusName()).collect(Collectors.toSet()); + assertEquals(expected, actual); + handler.close(); } - // NOCOMMIT: Have not implemented any kind of filtering for OTEL yet @Test - @BadApple(bugUrl = "https://issues.apache.org/jira/browse/SOLR-17458") - public void testKeyMetrics() throws Exception { + public void testNonExistentMetricNameFiltering() throws Exception { + String nonexistentMetricName = "nonexistent_metric_name"; MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); - String key1 = "solr.core.collection1:CACHE.core.fieldCache"; SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - key1), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.METRIC_NAME_PARAM, + nonexistentMetricName), resp); - NamedList<?> values = resp.getValues(); - Object val = values._get(List.of("metrics", key1), null); - assertNotNull(val); - assertTrue(val instanceof MapWriter); - assertTrue(((MapWriter) val)._size() >= 2); - - String key2 = "solr.core.collection1:CACHE.core.fieldCache:entries_count"; - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - key2), - resp); - val = resp.getValues()._get("metrics/" + key2); - assertNotNull(val); - assertTrue(val instanceof Number); + var metrics = (MetricSnapshots) resp.getValues().get("metrics"); + assertEquals(0, metrics.size()); + handler.close(); + } - String key3 = "solr.jetty:solrtest_foo\\:bar"; - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - key3), - resp); + @Test + public void testLabelFiltering() throws Exception { + MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); - val = resp.getValues()._get("metrics/" + key3); - assertNotNull(val); - assertTrue(val instanceof Number); - assertEquals(3, ((Number) val).intValue()); + assertQ(req("*:*"), "//result[@numFound='0']"); - // test multiple keys - resp = new SolrQueryResponse(); + SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - key1, - MetricsHandler.KEY_PARAM, - key2, - MetricsHandler.KEY_PARAM, - key3), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.CATEGORY_PARAM, + "QUERY"), resp); + var metrics = (MetricSnapshots) resp.getValues().get("metrics"); + + metrics.forEach( + (ms) -> { + ms.getDataPoints() + .forEach( + (dp) -> { + assertEquals("QUERY", dp.getLabels().get(MetricsHandler.CATEGORY_PARAM)); + }); + }); - val = resp.getValues()._get("metrics/" + key1); - assertNotNull(val); - val = resp.getValues()._get("metrics/" + key2); - assertNotNull(val); - val = resp.getValues()._get("metrics/" + key3); - assertNotNull(val); + handler.close(); + } - String key4 = "solr.core.collection1:QUERY./select.requestTimes:1minRate"; - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - key4), - resp); - // the key contains a slash, need explicit list of path elements - val = resp.getValues()._get(Arrays.asList("metrics", key4), null); - assertNotNull(val); - assertTrue(val instanceof Number); + @Test + public void testMultipleLabelFiltering() throws Exception { + MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); - // test errors + assertQ(req("*:*"), "//result[@numFound='0']"); - // invalid keys - resp = new SolrQueryResponse(); + SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - "foo", - MetricsHandler.KEY_PARAM, - "foo:bar:baz:xyz"), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.CATEGORY_PARAM, + "QUERY" + "," + "SEARCHER"), resp); - values = resp.getValues(); - NamedList<?> metrics = (NamedList<?>) values.get("metrics"); - assertEquals(0, metrics.size()); - assertNotNull(values._get(List.of("errors", "foo"), null)); - assertNotNull(values._get(List.of("errors", "foo:bar:baz:xyz"), null)); - // unknown registry - resp = new SolrQueryResponse(); + var metrics = (MetricSnapshots) resp.getValues().get("metrics"); + metrics.forEach( + (ms) -> { + ms.getDataPoints() + .forEach( + (dp) -> { + assertTrue( + dp.getLabels().get(MetricsHandler.CATEGORY_PARAM).equals("QUERY") + || dp.getLabels() + .get(MetricsHandler.CATEGORY_PARAM) + .equals("SEARCHER")); + }); + }); + + handler.close(); + } + + @Test + public void testNonExistentLabelFiltering() throws Exception { + MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); + + SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - "foo:bar:baz"), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.CORE_PARAM, + "nonexistent_core_name"), resp); - values = resp.getValues(); - metrics = (NamedList<?>) values.get("metrics"); + + var metrics = (MetricSnapshots) resp.getValues().get("metrics"); assertEquals(0, metrics.size()); - assertNotNull(values._get(List.of("errors", "foo:bar:baz"), null)); + handler.close(); + } + + @Test + public void testMixedLabelFiltering() throws Exception { + MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); + + assertQ(req("*:*"), "//result[@numFound='0']"); - // unknown metric - resp = new SolrQueryResponse(); + SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", CommonParams.WT, - "json", - MetricsHandler.KEY_PARAM, - "solr.jetty:unknown:baz"), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.CORE_PARAM, + "collection1", + MetricsHandler.CATEGORY_PARAM, + "SEARCHER"), resp); - values = resp.getValues(); - metrics = (NamedList<?>) values.get("metrics"); - assertEquals(0, metrics.size()); - assertNotNull(values._get(List.of("errors", "solr.jetty:unknown:baz"), null)); + + var metrics = (MetricSnapshots) resp.getValues().get("metrics"); + metrics.forEach( + (ms) -> { + ms.getDataPoints() + .forEach( + (dp) -> { + assertTrue( + dp.getLabels().get(MetricsHandler.CATEGORY_PARAM).equals("SEARCHER") + && dp.getLabels().get(MetricsHandler.CORE_PARAM).equals("collection1")); + }); + }); handler.close(); } - // NOCOMMIT: Have not implemented any kind of filtering for OTEL yet @Test - @BadApple(bugUrl = "https://issues.apache.org/jira/browse/SOLR-17458") - @SuppressWarnings("unchecked") - public void testExprMetrics() throws Exception { + public void testMetricNamesAndLabelFiltering() throws Exception { + String expectedMetricName = "solr_core_segments"; MetricsHandler handler = new MetricsHandler(h.getCoreContainer()); - String key1 = "solr\\.core\\..*:.*/select\\.request.*:.*Rate"; + assertQ(req("*:*"), "//result[@numFound='0']"); + SolrQueryResponse resp = new SolrQueryResponse(); handler.handleRequestBody( req( CommonParams.QT, "/admin/metrics", CommonParams.WT, - "json", - MetricsHandler.EXPR_PARAM, - key1), - resp); - // response structure is like in the case of non-key params - Object val = - resp.getValues() - ._get(List.of("metrics", "solr.core.collection1", "QUERY./select.requestTimes"), null); - assertNotNull(val); - assertTrue(val instanceof MapWriter); - Map<String, Object> map = new HashMap<>(); - ((MapWriter) val).toMap(map); - assertEquals(map.toString(), 4, map.size()); // mean, 1, 5, 15 - assertNotNull(map.toString(), map.get("meanRate")); - assertNotNull(map.toString(), map.get("1minRate")); - assertNotNull(map.toString(), map.get("5minRate")); - assertNotNull(map.toString(), map.get("15minRate")); - assertEquals(map.toString(), ((Number) map.get("1minRate")).doubleValue(), 0.0, 0.0); - map.clear(); - - String key2 = "solr\\.core\\..*:.*/select\\.request.*"; - resp = new SolrQueryResponse(); - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - CommonParams.WT, - "json", - MetricsHandler.EXPR_PARAM, - key2), + MetricsHandler.PROMETHEUS_METRICS_WT, + MetricsHandler.CATEGORY_PARAM, + "CORE", + MetricsHandler.METRIC_NAME_PARAM, + expectedMetricName), resp); - // response structure is like in the case of non-key params - val = resp.getValues()._get(List.of("metrics", "solr.core.collection1"), null); - assertNotNull(val); - Object v = ((SimpleOrderedMap<Object>) val).get("QUERY./select.requestTimes"); - assertNotNull(v); - assertTrue(v instanceof MapWriter); - ((MapWriter) v).toMap(map); - assertEquals(map.toString(), 14, map.size()); - assertNotNull(map.toString(), map.get("1minRate")); - assertEquals(map.toString(), ((Number) map.get("1minRate")).doubleValue(), 0.0, 0.0); - map.clear(); - // select requests counter - v = ((SimpleOrderedMap<Object>) val).get("QUERY./select.requests"); - assertNotNull(v); - assertTrue(v instanceof Number); - - // test multiple expressions producing overlapping metrics - should be no dupes - - // this key matches also sub-metrics of /select, eg. /select[shard], ... - String key3 = "solr\\.core\\..*:.*/select.*\\.requestTimes:count"; - resp = new SolrQueryResponse(); - // ORDER OF PARAMS MATTERS HERE! see the refguide - handler.handleRequestBody( - req( - CommonParams.QT, - "/admin/metrics", - CommonParams.WT, - "json", - MetricsHandler.EXPR_PARAM, - key2, - MetricsHandler.EXPR_PARAM, - key1, - MetricsHandler.EXPR_PARAM, - key3), - resp); - val = resp.getValues()._get(List.of("metrics", "solr.core.collection1"), null); - assertNotNull(val); - // for requestTimes only the full set of values from the first expr should be present - assertNotNull(val); - SimpleOrderedMap<Object> values = (SimpleOrderedMap<Object>) val; - assertEquals(values.jsonStr(), 3, values.size()); - v = values.get("QUERY./select.requestTimes"); - assertTrue(v instanceof MapWriter); - ((MapWriter) v).toMap(map); - assertTrue(map.toString(), map.containsKey("count")); - map.clear(); - v = values.get("QUERY./select[shard].requestTimes"); - assertTrue(v instanceof MapWriter); - ((MapWriter) v).toMap(map); - assertTrue(map.toString(), map.containsKey("count")); - } - private MetricSnapshot getMetricSnapshot(MetricSnapshots snapshots, String metricName) { - return snapshots.stream() - .filter(ss -> ss.getMetadata().getPrometheusName().equals(metricName)) - .findAny() - .get(); - } - - private GaugeSnapshot.GaugeDataPointSnapshot getGaugeDatapointSnapshot( - MetricSnapshot snapshot, Labels labels) { - return (GaugeSnapshot.GaugeDataPointSnapshot) - snapshot.getDataPoints().stream() - .filter(ss -> ss.getLabels().hasSameValues(labels)) - .findAny() - .get(); - } - - private CounterSnapshot.CounterDataPointSnapshot getCounterDatapointSnapshot( - MetricSnapshot snapshot, Labels labels) { - return (CounterSnapshot.CounterDataPointSnapshot) - snapshot.getDataPoints().stream() - .filter(ss -> ss.getLabels().hasSameValues(labels)) - .findAny() - .get(); - } - - private SummarySnapshot.SummaryDataPointSnapshot getSummaryDataPointSnapshot( - MetricSnapshot snapshot, Labels labels) { - return (SummarySnapshot.SummaryDataPointSnapshot) - snapshot.getDataPoints().stream() - .filter(ss -> ss.getLabels().hasSameValues(labels)) - .findAny() - .get(); - } - - static class RefreshablePluginHolder extends PluginBag.PluginHolder<SolrRequestHandler> { - - private DumpRequestHandler rh; - private SolrMetricsContext metricsInfo; - - public RefreshablePluginHolder(PluginInfo info, DumpRequestHandler rh) { - super(info); - this.rh = rh; - } - - @Override - public boolean isLoaded() { - return true; - } - - void closeHandler() throws Exception { - this.metricsInfo = rh.getSolrMetricsContext(); - // if(metricsInfo.tag.contains(String.valueOf(rh.hashCode()))){ - // //this created a new child metrics - // metricsInfo = metricsInfo.getParent(); - // } - this.rh.close(); - } - - void reset(DumpRequestHandler rh) { - this.rh = rh; - if (metricsInfo != null) - this.rh.initializeMetrics(metricsInfo, Attributes.empty(), "/dumphandler"); - } - - @Override - public SolrRequestHandler get() { - return rh; - } + var metrics = (MetricSnapshots) resp.getValues().get("metrics"); + assertEquals(1, metrics.size()); + var actualDatapoint = metrics.get(0).getDataPoints().getFirst(); + assertEquals(expectedMetricName, metrics.get(0).getMetadata().getPrometheusName()); + assertEquals("CORE", actualDatapoint.getLabels().get(MetricsHandler.CATEGORY_PARAM)); + handler.close(); } public static class DumpRequestHandler extends RequestHandlerBase { diff --git a/solr/core/src/test/org/apache/solr/metrics/otel/FilterablePrometheusMetricReaderTest.java b/solr/core/src/test/org/apache/solr/metrics/otel/FilterablePrometheusMetricReaderTest.java new file mode 100644 index 00000000000..ec91e149b4b --- /dev/null +++ b/solr/core/src/test/org/apache/solr/metrics/otel/FilterablePrometheusMetricReaderTest.java @@ -0,0 +1,95 @@ +/* + * 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.metrics.otel; + +import io.prometheus.metrics.model.snapshots.Labels; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import org.apache.solr.SolrTestCaseJ4; +import org.junit.Test; + +/** + * Test class for FilterablePrometheusMetricReader which focuses on requiredLabelsFilter method + * which filters metric data points based on label requirements. + */ +public class FilterablePrometheusMetricReaderTest extends SolrTestCaseJ4 { + + Labels actualLabels = Labels.of("key1", "value1", "key2", "value2", "key3", "value3"); + + @Test + public void testFilterMatchingLabel() { + SortedMap<String, Set<String>> requiredLabels = new TreeMap<>(Map.of("key1", Set.of("value1"))); + assertTrue(FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } + + @Test + public void testFilterOneMatchingLabelValue() { + SortedMap<String, Set<String>> requiredLabels = + new TreeMap<>(Map.of("key1", Set.of("value1", "value123", "value456", "value789"))); + assertTrue(FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } + + @Test + public void testFilterNoMatchingLabel() { + SortedMap<String, Set<String>> requiredLabels = + new TreeMap<>(Map.of("dummyKey", Set.of("dummyValue"))); + assertFalse( + FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } + + @Test + public void testFilterAllMultipleMatchingLabels() { + SortedMap<String, Set<String>> requiredLabels = + new TreeMap<>(Map.of("key1", Set.of("value1"), "key2", Set.of("value2"))); + assertTrue(FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } + + @Test + public void testFilterMultipleWithOneLabelValueNotMatching() { + SortedMap<String, Set<String>> requiredLabels = + new TreeMap<>(Map.of("key1", Set.of("value1"), "key2", Set.of("value999"))); + assertFalse( + FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } + + @Test + public void testFilterMultipleWithOneLabelMissing() { + SortedMap<String, Set<String>> requiredLabels = + new TreeMap<>(Map.of("key1", Set.of("value1"), "key999", Set.of("value2"))); + assertFalse( + FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } + + @Test + public void testFilterLabelsWithMixedValues() { + // Scenario with multiple label values some matching and some not + SortedMap<String, Set<String>> requiredLabels = + new TreeMap<>( + Map.of("key1", Set.of("value1", "value123"), "key2", Set.of("value456", "value2"))); + assertTrue(FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } + + @Test + public void testFilterEmptyLabelValues() { + // Edge case where label has empty set of allowed values + SortedMap<String, Set<String>> requiredLabels = new TreeMap<>(Map.of("key1", Set.of())); + assertFalse( + FilterablePrometheusMetricReader.requiredLabelsFilter(actualLabels, requiredLabels)); + } +} diff --git a/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java b/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java index 09aa0489949..ecfd83c02d4 100644 --- a/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java +++ b/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java @@ -102,7 +102,7 @@ public class SolrIndexMetricsTest extends SolrTestCaseJ4 { var segmentSize = SolrMetricTestUtils.getGaugeDatapoint( core, - "solr_core_segment_count", + "solr_core_segments", SolrMetricTestUtils.newStandaloneLabelsBuilder(core) .label("category", "CORE") .build());
