This is an automated email from the ASF dual-hosted git repository.
mlbiscoc pushed a commit to branch feature/SOLR-17458
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/feature/SOLR-17458 by this
push:
new c7a5950ea33 SOLR-17793: Create dedicated SdkMeterProviders for Solr
core metrics (#3402)
c7a5950ea33 is described below
commit c7a5950ea331464886908cfbfebe4b9d5fc02d41
Author: Matthew Biscocho <[email protected]>
AuthorDate: Mon Jun 30 12:50:14 2025 -0400
SOLR-17793: Create dedicated SdkMeterProviders for Solr core metrics (#3402)
* Create dynamic SDK Meter Providers and tests
* Bad merge from feature branch
* Wrap meter providers and metric readers
* Test cleanup
* Cleanup
* Fix double lookups
* Create a separate test suite for prometheus
* Move back to registry name
* Changes from comments
---
.../java/org/apache/solr/core/CoreContainer.java | 7 +-
.../solr/core/OpenTelemetryConfigurator.java | 25 +-
.../apache/solr/handler/RequestHandlerBase.java | 2 +-
.../apache/solr/handler/admin/MetricsHandler.java | 2 +-
.../apache/solr/metrics/SolrCoreMetricManager.java | 1 +
.../org/apache/solr/metrics/SolrMetricManager.java | 160 ++++++++++---
.../apache/solr/metrics/SolrMetricsContext.java | 22 +-
.../solr/response/PrometheusResponseWriter.java | 112 ++++++++-
.../org/apache/solr/util/stats/MetricUtils.java | 25 --
.../apache/solr/core/TestTracerConfigurator.java | 2 +-
.../solr/handler/admin/MetricsHandlerTest.java | 1 +
.../org/apache/solr/metrics/MetricsConfigTest.java | 10 +-
.../apache/solr/metrics/SolrMetricManagerTest.java | 262 +++++++++++++++++++++
.../response/TestPrometheusResponseWriter.java | 46 +---
.../TestPrometheusResponseWriterCloud.java | 164 +++++++++++++
.../src/java/org/apache/solr/SolrTestCaseJ4.java | 4 +-
.../apache/solr/cloud/MiniSolrCloudCluster.java | 1 +
17 files changed, 705 insertions(+), 141 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 06505611d62..4a021e82379 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -33,7 +33,6 @@ import com.github.benmanes.caffeine.cache.Interner;
import com.google.common.annotations.VisibleForTesting;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Tracer;
-import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
@@ -426,9 +425,8 @@ public class CoreContainer {
this.solrHome = config.getSolrHome();
this.solrCores = SolrCores.newSolrCores(this);
this.nodeKeyPair = new SolrNodeKeyPair(cfg.getCloudConfig());
- PrometheusMetricReader metricReader = new PrometheusMetricReader(true,
null);
- OpenTelemetryConfigurator.initializeOpenTelemetrySdk(cfg, loader,
metricReader);
- this.metricManager = new SolrMetricManager(loader, cfg.getMetricsConfig(),
metricReader);
+ OpenTelemetryConfigurator.initializeOpenTelemetrySdk(cfg, loader);
+ this.metricManager = new SolrMetricManager(loader, cfg.getMetricsConfig());
this.tracer = TraceUtils.getGlobalTracer();
containerHandlers.put(PublicKeyHandler.PATH, new
PublicKeyHandler(nodeKeyPair));
@@ -849,6 +847,7 @@ public class CoreContainer {
new SolrMetricsContext(
metricManager,
SolrMetricManager.getRegistryName(SolrInfoBean.Group.node), metricTag);
+ // NOCOMMIT: Do we need this for OTEL or is this specific for reporters?
coreContainerWorkExecutor =
MetricUtils.instrumentedExecutorService(
coreContainerWorkExecutor,
diff --git
a/solr/core/src/java/org/apache/solr/core/OpenTelemetryConfigurator.java
b/solr/core/src/java/org/apache/solr/core/OpenTelemetryConfigurator.java
index 46ca732c7b1..7301bc7022f 100644
--- a/solr/core/src/java/org/apache/solr/core/OpenTelemetryConfigurator.java
+++ b/solr/core/src/java/org/apache/solr/core/OpenTelemetryConfigurator.java
@@ -19,16 +19,12 @@ package org.apache.solr.core;
import com.google.common.annotations.VisibleForTesting;
import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
-import io.opentelemetry.sdk.OpenTelemetrySdkBuilder;
-import io.opentelemetry.sdk.metrics.SdkMeterProvider;
-import io.opentelemetry.sdk.metrics.export.MetricReader;
-import io.opentelemetry.sdk.trace.SdkTracerProvider;
-import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.lang.invoke.MethodHandles;
import java.util.Locale;
import java.util.Map;
@@ -62,7 +58,7 @@ public abstract class OpenTelemetryConfigurator implements
NamedListInitializedP
* SDK.
*/
public static synchronized void initializeOpenTelemetrySdk(
- NodeConfig cfg, SolrResourceLoader loader, MetricReader metricReader) {
+ NodeConfig cfg, SolrResourceLoader loader) {
PluginInfo info = (cfg != null) ? cfg.getTracerConfiguratorPluginInfo() :
null;
if (info != null && info.isEnabled()) {
@@ -71,28 +67,21 @@ public abstract class OpenTelemetryConfigurator implements
NamedListInitializedP
} else if (OpenTelemetryConfigurator.shouldAutoConfigOTEL()) {
OpenTelemetryConfigurator.autoConfigureOpenTelemetrySdk(loader);
} else {
- // Initializing sampler as always off to replicate no-op Tracer provider
- OpenTelemetryConfigurator.configureOpenTelemetrySdk(
-
SdkMeterProvider.builder().registerMetricReader(metricReader).build(),
- SdkTracerProvider.builder().setSampler(Sampler.alwaysOff()).build());
+ OpenTelemetryConfigurator.configureOpenTelemetrySdk();
}
}
- private static void configureOpenTelemetrySdk(
- SdkMeterProvider sdkMeterProvider, SdkTracerProvider sdkTracerProvider) {
+ private static void configureOpenTelemetrySdk() {
if (loaded) return;
- OpenTelemetrySdkBuilder builder = OpenTelemetrySdk.builder();
if (TRACE_ID_GEN_ENABLED) {
log.info("OpenTelemetry tracer enabled with simple propagation only.");
ExecutorUtil.addThreadLocalProvider(new ContextThreadLocalProvider());
-
builder.setPropagators(ContextPropagators.create(SimplePropagator.getInstance()));
}
- if (sdkMeterProvider != null) builder.setMeterProvider(sdkMeterProvider);
- if (sdkTracerProvider != null)
builder.setTracerProvider(sdkTracerProvider);
-
- GlobalOpenTelemetry.set(builder.build());
+ OpenTelemetry otel =
+
OpenTelemetry.propagating(ContextPropagators.create(SimplePropagator.getInstance()));
+ GlobalOpenTelemetry.set(otel);
loaded = true;
}
diff --git a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
index 0888b417e92..9ac85a27802 100644
--- a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
@@ -189,7 +189,7 @@ public abstract class RequestHandlerBase
new HandlerMetrics(
new SolrMetricsContext(
new SolrMetricManager(
- null, new
MetricsConfig.MetricsConfigBuilder().setEnabled(false).build(), null),
+ null, new
MetricsConfig.MetricsConfigBuilder().setEnabled(false).build()),
"NO_OP",
"NO_OP"),
Attributes.empty());
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 f2b501b8a41..ba7f6bba34b 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
@@ -128,7 +128,7 @@ public class MetricsHandler extends RequestHandlerBase
implements PermissionName
// TODO SOLR-17458: Make this the default option after dropwizard removal
if (PROMETHEUS_METRICS_WT.equals(params.get(CommonParams.WT))) {
- consumer.accept("metrics", metricManager.getMetricReader().collect());
+ consumer.accept("metrics", metricManager.getPrometheusMetricReaders());
return;
}
diff --git
a/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
b/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
index 62a342180f2..9d79ada8a85 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
@@ -108,6 +108,7 @@ public class SolrCoreMetricManager implements Closeable {
* and will be used under the new core name. This method also reloads
reporters so that they use
* the new core name.
*/
+ // NOCOMMIT SOLR-17458: Update for core renaming
public void afterCoreRename() {
assert core.getCoreDescriptor().getCloudDescriptor() == null;
String oldRegistryName = solrMetricsContext.getRegistryName();
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 12108e25002..fa600e5c3cd 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
@@ -42,8 +42,8 @@ import io.opentelemetry.api.metrics.LongHistogram;
import io.opentelemetry.api.metrics.LongHistogramBuilder;
import io.opentelemetry.api.metrics.LongUpDownCounter;
import io.opentelemetry.api.metrics.LongUpDownCounterBuilder;
-import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.api.metrics.ObservableDoubleCounter;
+import io.opentelemetry.api.metrics.ObservableDoubleGauge;
import io.opentelemetry.api.metrics.ObservableDoubleMeasurement;
import io.opentelemetry.api.metrics.ObservableDoubleUpDownCounter;
import io.opentelemetry.api.metrics.ObservableLongCounter;
@@ -51,6 +51,7 @@ import io.opentelemetry.api.metrics.ObservableLongGauge;
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
import io.opentelemetry.api.metrics.ObservableLongUpDownCounter;
import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
@@ -81,7 +82,6 @@ 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.util.stats.MetricUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
@@ -115,6 +115,8 @@ public class SolrMetricManager {
private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ public static final String OTEL_SCOPE_NAME = "org.apache.solr";
+
/** Common prefix for all registry names that Solr uses. */
public static final String REGISTRY_NAME_PREFIX = "solr.";
@@ -147,9 +149,8 @@ public class SolrMetricManager {
private final MetricRegistry.MetricSupplier<Timer> timerSupplier;
private final MetricRegistry.MetricSupplier<Histogram> histogramSupplier;
- private final MeterProvider meterProvider;
- private final Map<String, io.opentelemetry.api.metrics.Meter> meters = new
ConcurrentHashMap<>();
- private final PrometheusMetricReader metricReader;
+ private final ConcurrentMap<String, MeterProviderAndReaders>
meterProviderAndReaders =
+ new ConcurrentHashMap<>();
public SolrMetricManager() {
metricsConfig = new MetricsConfig.MetricsConfigBuilder().build();
@@ -157,15 +158,10 @@ public class SolrMetricManager {
meterSupplier = MetricSuppliers.meterSupplier(null, null);
timerSupplier = MetricSuppliers.timerSupplier(null, null);
histogramSupplier = MetricSuppliers.histogramSupplier(null, null);
- meterProvider = MetricUtils.getMeterProvider();
- metricReader = null;
}
- public SolrMetricManager(
- SolrResourceLoader loader, MetricsConfig metricsConfig,
PrometheusMetricReader metricReader) {
+ public SolrMetricManager(SolrResourceLoader loader, MetricsConfig
metricsConfig) {
this.metricsConfig = metricsConfig;
- this.metricReader = metricReader;
- this.meterProvider = MetricUtils.getMeterProvider();
counterSupplier = MetricSuppliers.counterSupplier(loader,
metricsConfig.getCounterSupplier());
meterSupplier = MetricSuppliers.meterSupplier(loader,
metricsConfig.getMeterSupplier());
timerSupplier = MetricSuppliers.timerSupplier(loader,
metricsConfig.getTimerSupplier());
@@ -176,7 +172,10 @@ public class SolrMetricManager {
public LongCounter longCounter(
String registry, String counterName, String description, String unit) {
LongCounterBuilder builder =
-
meterProvider.get(registry).counterBuilder(counterName).setDescription(description);
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .counterBuilder(counterName)
+ .setDescription(description);
if (unit != null) builder.setUnit(unit);
return builder.build();
@@ -185,7 +184,10 @@ public class SolrMetricManager {
public LongUpDownCounter longUpDownCounter(
String registry, String counterName, String description, String unit) {
LongUpDownCounterBuilder builder =
-
meterProvider.get(registry).upDownCounterBuilder(counterName).setDescription(description);
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .upDownCounterBuilder(counterName)
+ .setDescription(description);
if (unit != null) builder.setUnit(unit);
return builder.build();
@@ -194,8 +196,8 @@ public class SolrMetricManager {
public DoubleUpDownCounter doubleUpDownCounter(
String registry, String counterName, String description, String unit) {
DoubleUpDownCounterBuilder builder =
- meterProvider
- .get(registry)
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
.upDownCounterBuilder(counterName)
.setDescription(description)
.ofDoubles();
@@ -207,8 +209,8 @@ public class SolrMetricManager {
public DoubleCounter doubleCounter(
String registry, String counterName, String description, String unit) {
DoubleCounterBuilder builder =
- meterProvider
- .get(registry)
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
.counterBuilder(counterName)
.setDescription(description)
.ofDoubles();
@@ -220,7 +222,10 @@ public class SolrMetricManager {
public DoubleHistogram doubleHistogram(
String registry, String histogramName, String description, String unit) {
DoubleHistogramBuilder builder =
-
meterProvider.get(registry).histogramBuilder(histogramName).setDescription(description);
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .histogramBuilder(histogramName)
+ .setDescription(description);
if (unit != null) builder.setUnit(unit);
return builder.build();
@@ -229,8 +234,8 @@ public class SolrMetricManager {
public LongHistogram longHistogram(
String registry, String histogramName, String description, String unit) {
LongHistogramBuilder builder =
- meterProvider
- .get(registry)
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
.histogramBuilder(histogramName)
.setDescription(description)
.ofLongs();
@@ -243,7 +248,10 @@ public class SolrMetricManager {
public DoubleGauge doubleGauge(
String registry, String gaugeName, String description, String unit) {
DoubleGaugeBuilder builder =
-
meterProvider.get(registry).gaugeBuilder(gaugeName).setDescription(description);
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .gaugeBuilder(gaugeName)
+ .setDescription(description);
if (unit != null) builder.setUnit(unit);
return builder.build();
@@ -251,7 +259,11 @@ public class SolrMetricManager {
public LongGauge longGauge(String registry, String gaugeName, String
description, String unit) {
LongGaugeBuilder builder =
-
meterProvider.get(registry).gaugeBuilder(gaugeName).setDescription(description).ofLongs();
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .gaugeBuilder(gaugeName)
+ .setDescription(description)
+ .ofLongs();
if (unit != null) builder.setUnit(unit);
return builder.build();
@@ -264,7 +276,10 @@ public class SolrMetricManager {
Consumer<ObservableLongMeasurement> callback,
String unit) {
LongCounterBuilder builder =
-
meterProvider.get(registry).counterBuilder(counterName).setDescription(description);
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .counterBuilder(counterName)
+ .setDescription(description);
if (unit != null) builder.setUnit(unit);
return builder.buildWithCallback(callback);
@@ -277,8 +292,8 @@ public class SolrMetricManager {
Consumer<ObservableDoubleMeasurement> callback,
String unit) {
DoubleCounterBuilder builder =
- meterProvider
- .get(registry)
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
.counterBuilder(counterName)
.setDescription(description)
.ofDoubles();
@@ -294,7 +309,28 @@ public class SolrMetricManager {
Consumer<ObservableLongMeasurement> callback,
String unit) {
LongGaugeBuilder builder =
-
meterProvider.get(registry).gaugeBuilder(gaugeName).setDescription(description).ofLongs();
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .gaugeBuilder(gaugeName)
+ .setDescription(description)
+ .ofLongs();
+ if (unit != null) builder.setUnit(unit);
+
+ return builder.buildWithCallback(callback);
+ }
+
+ public ObservableDoubleGauge observableDoubleGauge(
+ String registry,
+ String gaugeName,
+ String description,
+ Consumer<ObservableDoubleMeasurement> callback,
+ String unit) {
+ DoubleGaugeBuilder builder =
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .gaugeBuilder(gaugeName)
+ .setDescription(description);
+
if (unit != null) builder.setUnit(unit);
return builder.buildWithCallback(callback);
@@ -307,7 +343,10 @@ public class SolrMetricManager {
Consumer<ObservableLongMeasurement> callback,
String unit) {
LongUpDownCounterBuilder builder =
-
meterProvider.get(registry).upDownCounterBuilder(counterName).setDescription(description);
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
+ .upDownCounterBuilder(counterName)
+ .setDescription(description);
if (unit != null) builder.setUnit(unit);
return builder.buildWithCallback(callback);
@@ -320,8 +359,8 @@ public class SolrMetricManager {
Consumer<ObservableDoubleMeasurement> callback,
String unit) {
DoubleUpDownCounterBuilder builder =
- meterProvider
- .get(registry)
+ meterProvider(registry)
+ .get(OTEL_SCOPE_NAME)
.upDownCounterBuilder(counterName)
.setDescription(description)
.ofDoubles();
@@ -330,10 +369,6 @@ public class SolrMetricManager {
return builder.buildWithCallback(callback);
}
- public io.opentelemetry.api.metrics.Meter getOtelRegistry(String
registryName) {
- return meters.get(registryName);
- }
-
// for unit tests
public MetricRegistry.MetricSupplier<Counter> getCounterSupplier() {
return counterSupplier;
@@ -578,6 +613,7 @@ public class SolrMetricManager {
}
/** Return a set of existing registry names. */
+ // NOCOMMIT: Remove for OTEL
public Set<String> registryNames() {
Set<String> set = new HashSet<>();
set.addAll(registries.keySet());
@@ -598,6 +634,10 @@ public class SolrMetricManager {
return names.contains(name);
}
+ public boolean hasMeterProvider(String name) {
+ return meterProviderAndReaders.containsKey(enforcePrefix(name));
+ }
+
/**
* Return set of existing registry names that match a regex pattern
*
@@ -653,7 +693,7 @@ public class SolrMetricManager {
* @param registry name of the registry
* @return existing or newly created registry
*/
- // TODO SOLR-17458: We may not need
+ // NOCOMMIT SOLR-17458: We may not need
public MetricRegistry registry(String registry) {
registry = enforcePrefix(registry);
if (isSharedRegistry(registry)) {
@@ -668,6 +708,29 @@ public class SolrMetricManager {
}
}
+ /**
+ * Get (or create if not present) a named {@link SdkMeterProvider} under
{@link
+ * MeterProviderAndReaders}. This also registers a corresponding {@link
PrometheusMetricReader}
+ * Dropwizards's {@link SolrMetricManager#registry(String)} equivalent
+ *
+ * @param providerName name of the meter provider and prometheus metric
reader
+ * @return existing or newly created meter provider
+ */
+ public SdkMeterProvider meterProvider(String providerName) {
+ providerName = enforcePrefix(providerName);
+ return meterProviderAndReaders
+ .computeIfAbsent(
+ providerName,
+ key -> {
+ var reader = new PrometheusMetricReader(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).build();
+ return new MeterProviderAndReaders(provider, reader);
+ })
+ .sdkMeterProvider();
+ }
+
// TODO SOLR-17458: We may not need
private static MetricRegistry getOrCreateRegistry(
ConcurrentMap<String, MetricRegistry> map, String registry) {
@@ -686,12 +749,14 @@ public class SolrMetricManager {
}
/**
- * Remove a named registry.
+ * Remove a named registry and close an existing {@link SdkMeterProvider}.
Upon closing of
+ * provider, all metric readers registered to it are closed.
*
* @param registry name of the registry to remove
*/
// TODO SOLR-17458: You can't delete OTEL meters
public void removeRegistry(String registry) {
+ // NOCOMMIT Remove all closing Dropwizard registries
// close any reporters for this registry first
closeReporters(registry, null);
// make sure we use a name with prefix
@@ -706,6 +771,12 @@ public class SolrMetricManager {
swapLock.unlock();
}
}
+ meterProviderAndReaders.computeIfPresent(
+ registry,
+ (key, meterAndReader) -> {
+ meterAndReader.sdkMeterProvider().close();
+ return null;
+ });
}
/**
@@ -811,8 +882,9 @@ public class SolrMetricManager {
* start with the prefix will be removed.
* @return set of metrics names that have been removed.
*/
- // TODO SOLR-17458: This is not supported in otel. Metrics are immutable. We
can at best filter
- // them
+ // NOCOMMIT SOLR-17458: This is not supported in otel. Metrics are
immutable. We can at best
+ // filter
+ // them or delete the meterProvider entirely
public Set<String> clearMetrics(String registry, String... metricPath) {
PrefixFilter filter;
if (metricPath == null || metricPath.length == 0) {
@@ -1587,7 +1659,17 @@ public class SolrMetricManager {
return metricsConfig;
}
- public PrometheusMetricReader getMetricReader() {
- return this.metricReader;
+ /** Get a shallow copied map of {@link PrometheusMetricReader}. */
+ public Map<String, PrometheusMetricReader> getPrometheusMetricReaders() {
+ return meterProviderAndReaders.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e ->
e.getValue().prometheusMetricReader()));
+ }
+
+ public PrometheusMetricReader getPrometheusMetricReader(String providerName)
{
+ MeterProviderAndReaders mpr =
meterProviderAndReaders.get(enforcePrefix(providerName));
+ return (mpr != null) ? mpr.prometheusMetricReader() : null;
}
+
+ private record MeterProviderAndReaders(
+ SdkMeterProvider sdkMeterProvider, PrometheusMetricReader
prometheusMetricReader) {}
}
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
b/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
index 269575798ec..222ce2e0e8a 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricsContext.java
@@ -30,6 +30,8 @@ import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.LongGauge;
import io.opentelemetry.api.metrics.LongHistogram;
import io.opentelemetry.api.metrics.LongUpDownCounter;
+import io.opentelemetry.api.metrics.ObservableDoubleGauge;
+import io.opentelemetry.api.metrics.ObservableDoubleMeasurement;
import io.opentelemetry.api.metrics.ObservableLongGauge;
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
import java.util.Map;
@@ -206,17 +208,31 @@ public class SolrMetricsContext {
public ObservableLongGauge observableLongGauge(
String metricName, String description,
Consumer<ObservableLongMeasurement> callback) {
- return metricManager.observableLongGauge(registryName, metricName,
description, callback, null);
+ return observableLongGauge(metricName, description, callback, null);
}
public ObservableLongGauge observableLongGauge(
String metricName,
String description,
- String unit,
- Consumer<ObservableLongMeasurement> callback) {
+ Consumer<ObservableLongMeasurement> callback,
+ String unit) {
return metricManager.observableLongGauge(registryName, metricName,
description, callback, unit);
}
+ public ObservableDoubleGauge observableDoubleGauge(
+ String metricName, String description,
Consumer<ObservableDoubleMeasurement> callback) {
+ return observableDoubleGauge(metricName, description, callback, null);
+ }
+
+ public ObservableDoubleGauge observableDoubleGauge(
+ String metricName,
+ String description,
+ Consumer<ObservableDoubleMeasurement> callback,
+ String unit) {
+ return metricManager.observableDoubleGauge(
+ registryName, metricName, description, callback, unit);
+ }
+
/**
* Convenience method for {@link SolrMetricManager#meter(SolrMetricsContext,
String, String,
* String...)}.
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 5a375bdb408..77fd3642b2a 100644
--- a/solr/core/src/java/org/apache/solr/response/PrometheusResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/PrometheusResponseWriter.java
@@ -16,19 +16,27 @@
*/
package org.apache.solr.response;
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
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.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
- * org.apache.solr.handler.admin.MetricsHandler}
- */
+/** Response writer for Prometheus metrics. This is used only by the {@link
MetricsHandler} */
@SuppressWarnings(value = "unchecked")
public class PrometheusResponseWriter implements QueryResponseWriter {
// not TextQueryResponseWriter because Prometheus libs work with an
OutputStream
@@ -40,12 +48,104 @@ public class PrometheusResponseWriter implements
QueryResponseWriter {
public void write(
OutputStream out, SolrQueryRequest request, SolrQueryResponse response,
String contentType)
throws IOException {
- var prometheusTextFormatWriter = new PrometheusTextFormatWriter(false);
- prometheusTextFormatWriter.write(out, (MetricSnapshots)
response.getValues().get("metrics"));
+
+ Map<String, PrometheusMetricReader> readers =
+ (Map<String, PrometheusMetricReader>)
response.getValues().get("metrics");
+
+ List<MetricSnapshot> snapshots =
+ readers.values().stream().flatMap(r -> r.collect().stream()).toList();
+
+ new PrometheusTextFormatWriter(false).write(out,
mergeSnapshots(snapshots));
}
@Override
public String getContentType(SolrQueryRequest request, SolrQueryResponse
response) {
return CONTENT_TYPE_PROMETHEUS;
}
+
+ /**
+ * 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/java/org/apache/solr/util/stats/MetricUtils.java
b/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java
index 8b79fc26b35..9cff065c06f 100644
--- a/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java
+++ b/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java
@@ -26,10 +26,6 @@ import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
-import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.common.AttributesBuilder;
-import io.opentelemetry.api.metrics.MeterProvider;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
@@ -55,7 +51,6 @@ import java.util.function.Predicate;
import org.apache.solr.common.ConditionalKeyMapWriter;
import org.apache.solr.common.IteratorWriter;
import org.apache.solr.common.MapWriter;
-import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrInfoBean;
@@ -857,24 +852,4 @@ public class MetricUtils {
}
}
}
-
- public static MeterProvider getMeterProvider() {
- return GlobalOpenTelemetry.getMeterProvider();
- }
-
- public static Attributes createAttributes(String... attributes) {
- if (attributes.length % 2 == 1) {
- throw new SolrException(
- SolrException.ErrorCode.SERVER_ERROR, "Odd number of field/value
strings passed");
- }
-
- AttributesBuilder builder = Attributes.builder();
- for (int i = 0; i < attributes.length; i += 2) {
- String key = attributes[i];
- String value = attributes[i + 1];
- builder.put(key, value);
- }
-
- return builder.build();
- }
}
diff --git
a/solr/core/src/test/org/apache/solr/core/TestTracerConfigurator.java
b/solr/core/src/test/org/apache/solr/core/TestTracerConfigurator.java
index ba453efc5d4..5aa8ffec1ed 100644
--- a/solr/core/src/test/org/apache/solr/core/TestTracerConfigurator.java
+++ b/solr/core/src/test/org/apache/solr/core/TestTracerConfigurator.java
@@ -41,7 +41,7 @@ public class TestTracerConfigurator extends SolrTestCaseJ4 {
public void configuratorClassDoesNotExistTest() {
assertTrue(OpenTelemetryConfigurator.shouldAutoConfigOTEL());
SolrResourceLoader loader = new
SolrResourceLoader(TEST_PATH().resolve("collection1"));
- OpenTelemetryConfigurator.initializeOpenTelemetrySdk(null, loader, null);
+ OpenTelemetryConfigurator.initializeOpenTelemetrySdk(null, loader);
assertEquals(
"Expecting noop otel after failure to auto-init",
TracerProvider.noop().get(null),
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 4c8e35d9eee..c54da18a329 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
@@ -46,6 +46,7 @@ import org.junit.BeforeClass;
import org.junit.Test;
/** Test for {@link MetricsHandler} */
+// NOCOMMIT SOLR-17785: Lets move this to SolrCloudTestCase
public class MetricsHandlerTest extends SolrTestCaseJ4 {
@BeforeClass
public static void beforeClass() throws Exception {
diff --git a/solr/core/src/test/org/apache/solr/metrics/MetricsConfigTest.java
b/solr/core/src/test/org/apache/solr/metrics/MetricsConfigTest.java
index 32245ffb4c6..eff63f4db72 100644
--- a/solr/core/src/test/org/apache/solr/metrics/MetricsConfigTest.java
+++ b/solr/core/src/test/org/apache/solr/metrics/MetricsConfigTest.java
@@ -35,7 +35,7 @@ public class MetricsConfigTest extends SolrTestCaseJ4 {
public void testDefaults() {
NodeConfig cfg = loadNodeConfig("solr-metricsconfig.xml");
SolrMetricManager mgr =
- new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig(), null);
+ new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig());
assertTrue(mgr.getCounterSupplier() instanceof
MetricSuppliers.DefaultCounterSupplier);
assertTrue(mgr.getMeterSupplier() instanceof
MetricSuppliers.DefaultMeterSupplier);
assertTrue(mgr.getTimerSupplier() instanceof
MetricSuppliers.DefaultTimerSupplier);
@@ -54,7 +54,7 @@ public class MetricsConfigTest extends SolrTestCaseJ4 {
System.setProperty("histogram.reservoir",
SlidingTimeWindowReservoir.class.getName());
NodeConfig cfg = loadNodeConfig("solr-metricsconfig.xml");
SolrMetricManager mgr =
- new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig(), null);
+ new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig());
assertTrue(mgr.getCounterSupplier() instanceof
MetricSuppliers.DefaultCounterSupplier);
assertTrue(mgr.getMeterSupplier() instanceof
MetricSuppliers.DefaultMeterSupplier);
assertTrue(mgr.getTimerSupplier() instanceof
MetricSuppliers.DefaultTimerSupplier);
@@ -73,7 +73,7 @@ public class MetricsConfigTest extends SolrTestCaseJ4 {
System.setProperty("histogram.class",
MockHistogramSupplier.class.getName());
NodeConfig cfg = loadNodeConfig("solr-metricsconfig.xml");
SolrMetricManager mgr =
- new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig(), null);
+ new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig());
assertTrue(mgr.getCounterSupplier() instanceof MockCounterSupplier);
assertTrue(mgr.getMeterSupplier() instanceof MockMeterSupplier);
assertTrue(mgr.getTimerSupplier() instanceof MockTimerSupplier);
@@ -100,7 +100,7 @@ public class MetricsConfigTest extends SolrTestCaseJ4 {
System.setProperty("metricsEnabled", "false");
NodeConfig cfg = loadNodeConfig("solr-metricsconfig.xml");
SolrMetricManager mgr =
- new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig(), null);
+ new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig());
assertTrue(mgr.getCounterSupplier() instanceof
MetricSuppliers.NoOpCounterSupplier);
assertTrue(mgr.getMeterSupplier() instanceof
MetricSuppliers.NoOpMeterSupplier);
assertTrue(mgr.getTimerSupplier() instanceof
MetricSuppliers.NoOpTimerSupplier);
@@ -111,7 +111,7 @@ public class MetricsConfigTest extends SolrTestCaseJ4 {
public void testMissingValuesConfig() {
NodeConfig cfg = loadNodeConfig("solr-metricsconfig1.xml");
SolrMetricManager mgr =
- new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig(), null);
+ new SolrMetricManager(cfg.getSolrResourceLoader(),
cfg.getMetricsConfig());
assertNull("nullNumber", mgr.nullNumber());
assertEquals("notANumber", -1, mgr.notANumber());
assertEquals("nullNumber", "", mgr.nullString());
diff --git
a/solr/core/src/test/org/apache/solr/metrics/SolrMetricManagerTest.java
b/solr/core/src/test/org/apache/solr/metrics/SolrMetricManagerTest.java
index 730fe0989aa..2c8f51aec6d 100644
--- a/solr/core/src/test/org/apache/solr/metrics/SolrMetricManagerTest.java
+++ b/solr/core/src/test/org/apache/solr/metrics/SolrMetricManagerTest.java
@@ -20,10 +20,28 @@ package org.apache.solr.metrics;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
+import com.google.common.util.concurrent.AtomicDouble;
+import io.opentelemetry.api.metrics.DoubleCounter;
+import io.opentelemetry.api.metrics.DoubleGauge;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.DoubleUpDownCounter;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.LongGauge;
+import io.opentelemetry.api.metrics.LongHistogram;
+import io.opentelemetry.api.metrics.LongUpDownCounter;
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+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.MetricSnapshot;
+import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.DoubleAdder;
+import java.util.concurrent.atomic.LongAdder;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.util.NamedList;
@@ -31,10 +49,24 @@ import org.apache.solr.core.PluginInfo;
import org.apache.solr.core.SolrInfoBean;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.metrics.reporters.MockMetricReporter;
+import org.junit.Before;
import org.junit.Test;
public class SolrMetricManagerTest extends SolrTestCaseJ4 {
+ final String METER_PROVIDER_NAME = "test_provider_name";
+ private SolrMetricManager metricManager;
+ private PrometheusMetricReader reader;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ this.metricManager = new SolrMetricManager();
+ // Initialize a metric reader for tests
+ metricManager.meterProvider(METER_PROVIDER_NAME);
+ this.reader = metricManager.getPrometheusMetricReader(METER_PROVIDER_NAME);
+ }
+ // NOCOMMIT: We might not be supported core swapping in 10. Maybe remove
this test
@Test
public void testSwapRegistries() {
Random r = random();
@@ -76,6 +108,8 @@ public class SolrMetricManagerTest extends SolrTestCaseJ4 {
}
}
+ // NOCOMMIT: Migration of this to OTEL isn't possible. You can't register
instruments to a
+ // meterprovider that the provider itself didn't create
@Test
public void testRegisterAll() throws Exception {
Random r = random();
@@ -104,6 +138,8 @@ public class SolrMetricManagerTest extends SolrTestCaseJ4 {
registryName, mr, SolrMetricManager.ResolutionStrategy.ERROR));
}
+ // NOCOMMIT: Migration of this to OTEL isn't possible. You can only delete
the whole
+ // sdkMeterProvider and all it's recorded metrics
@Test
public void testClearMetrics() {
Random r = random();
@@ -270,4 +306,230 @@ public class SolrMetricManagerTest extends SolrTestCaseJ4
{
initArgs.add("configurable", "true");
return new PluginInfo("SolrMetricReporter", attrs, initArgs, null);
}
+
+ @Test
+ public void testLongCounter() {
+ LongCounter counter =
+ metricManager.longCounter(METER_PROVIDER_NAME, "my_counter", "desc",
null);
+ counter.add(5);
+ counter.add(3);
+ CounterSnapshot actual = snapshot("my_counter", CounterSnapshot.class);
+ assertEquals(8.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testLongUpDownCounter() {
+ LongUpDownCounter counter =
+ metricManager.longUpDownCounter(METER_PROVIDER_NAME,
"long_updown_counter", "desc", null);
+ counter.add(10);
+ counter.add(-4);
+ GaugeSnapshot actual = snapshot("long_updown_counter",
GaugeSnapshot.class);
+ assertEquals(6.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testDoubleUpDownCounter() {
+ DoubleUpDownCounter counter =
+ metricManager.doubleUpDownCounter(
+ METER_PROVIDER_NAME, "double_updown_counter", "desc", null);
+ counter.add(10.0);
+ counter.add(-5.5);
+
+ GaugeSnapshot actual = snapshot("double_updown_counter",
GaugeSnapshot.class);
+ assertEquals(4.5, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testDoubleCounter() {
+ DoubleCounter counter =
+ metricManager.doubleCounter(METER_PROVIDER_NAME, "double_counter",
"desc", null);
+ counter.add(10.0);
+ counter.add(5.5);
+
+ CounterSnapshot actual = snapshot("double_counter", CounterSnapshot.class);
+ assertEquals(15.5, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testDoubleHistogram() {
+ DoubleHistogram histogram =
+ metricManager.doubleHistogram(METER_PROVIDER_NAME, "double_histogram",
"desc", null);
+ histogram.record(1.1);
+ histogram.record(2.2);
+ histogram.record(3.3);
+
+ HistogramSnapshot actual = snapshot("double_histogram",
HistogramSnapshot.class);
+ assertEquals(3, actual.getDataPoints().getFirst().getCount());
+ assertEquals(6.6, actual.getDataPoints().getFirst().getSum(), 0.0);
+ }
+
+ @Test
+ public void testLongHistogram() {
+ LongHistogram histogram =
+ metricManager.longHistogram(METER_PROVIDER_NAME, "long_histogram",
"desc", null);
+ histogram.record(1);
+ histogram.record(2);
+ histogram.record(3);
+
+ HistogramSnapshot actual = snapshot("long_histogram",
HistogramSnapshot.class);
+ assertEquals(3, actual.getDataPoints().getFirst().getCount());
+ assertEquals(6.0, actual.getDataPoints().getFirst().getSum(), 0.0);
+ }
+
+ @Test
+ public void testDoubleGauge() {
+ DoubleGauge gauge =
+ metricManager.doubleGauge(METER_PROVIDER_NAME, "double_gauge", "desc",
null);
+ gauge.set(10.0);
+ gauge.set(5.5);
+
+ GaugeSnapshot actual = snapshot("double_gauge", GaugeSnapshot.class);
+ assertEquals(5.5, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testLongGauge() {
+ LongGauge gauge = metricManager.longGauge(METER_PROVIDER_NAME,
"long_gauge", "desc", null);
+ gauge.set(10);
+ gauge.set(5);
+
+ GaugeSnapshot actual = snapshot("long_gauge", GaugeSnapshot.class);
+ assertEquals(5.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testObservableLongCounter() {
+ LongAdder val = new LongAdder();
+ metricManager.observableLongCounter(
+ METER_PROVIDER_NAME, "obs_long_counter", "desc", m ->
m.record(val.longValue()), null);
+ val.add(10);
+
+ CounterSnapshot actual = snapshot("obs_long_counter",
CounterSnapshot.class);
+ assertEquals(10.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+
+ val.add(20);
+ actual = snapshot("obs_long_counter", CounterSnapshot.class);
+
+ // Observable metrics value changes anytime metricReader collects() to
trigger callback
+ assertEquals(30.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testObservableDoubleCounter() {
+ DoubleAdder val = new DoubleAdder();
+ metricManager.observableDoubleCounter(
+ METER_PROVIDER_NAME, "obs_double_counter", "desc", m ->
m.record(val.doubleValue()), null);
+ val.add(10.0);
+
+ CounterSnapshot actual = snapshot("obs_double_counter",
CounterSnapshot.class);
+ assertEquals(10.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+
+ val.add(0.1);
+ actual = snapshot("obs_double_counter", CounterSnapshot.class);
+
+ // Observable metrics value changes anytime metricReader collects() to
trigger callback
+ assertEquals(10.1, actual.getDataPoints().getFirst().getValue(), 1e-6);
+ }
+
+ @Test
+ public void testObservableLongGauge() {
+ AtomicLong val = new AtomicLong();
+ metricManager.observableLongGauge(
+ METER_PROVIDER_NAME, "obs_long_gauge", "desc", m ->
m.record(val.get()), null);
+ val.set(10L);
+
+ GaugeSnapshot actual = snapshot("obs_long_gauge", GaugeSnapshot.class);
+ assertEquals(10.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+
+ val.set(20L);
+ actual = snapshot("obs_long_gauge", GaugeSnapshot.class);
+
+ // Observable metrics value changes anytime metricReader collects() to
trigger callback
+ assertEquals(20.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testObservableDoubleGauge() {
+ AtomicDouble val = new AtomicDouble();
+ metricManager.observableDoubleGauge(
+ METER_PROVIDER_NAME, "obs_double_gauge", "desc", m ->
m.record(val.get()), null);
+ val.set(10.0);
+
+ GaugeSnapshot actual = snapshot("obs_double_gauge", GaugeSnapshot.class);
+ assertEquals(10.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+
+ val.set(10.1);
+ actual = snapshot("obs_double_gauge", GaugeSnapshot.class);
+
+ // Observable metrics value changes anytime metricReader collects() to
trigger callback
+ assertEquals(10.1, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testObservableLongUpDownCounter() {
+ LongAdder val = new LongAdder();
+ metricManager.observableLongUpDownCounter(
+ METER_PROVIDER_NAME, "obs_long_updown_gauge", "desc", m ->
m.record(val.longValue()), null);
+ val.add(10L);
+
+ GaugeSnapshot actual = snapshot("obs_long_updown_gauge",
GaugeSnapshot.class);
+ assertEquals(10.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+
+ val.add(-20L);
+ actual = snapshot("obs_long_updown_gauge", GaugeSnapshot.class);
+
+ // Observable metrics value changes anytime metricReader collects() to
trigger callback
+ assertEquals(-10.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+ }
+
+ @Test
+ public void testObservableDoubleUpDownCounter() {
+ DoubleAdder val = new DoubleAdder();
+ metricManager.observableDoubleUpDownCounter(
+ METER_PROVIDER_NAME,
+ "obs_double_updown_gauge",
+ "desc",
+ m -> m.record(val.doubleValue()),
+ null);
+ val.add(10.0);
+ GaugeSnapshot actual = snapshot("obs_double_updown_gauge",
GaugeSnapshot.class);
+ assertEquals(10.0, actual.getDataPoints().getFirst().getValue(), 0.0);
+
+ val.add(-20.1);
+ actual = snapshot("obs_double_updown_gauge", GaugeSnapshot.class);
+
+ // Observable metrics value changes anytime metricReader collects() to
trigger callback
+ assertEquals(-10.1, actual.getDataPoints().getFirst().getValue(), 1e-6);
+ }
+
+ @Test
+ public void testCloseMeterProviders() {
+ LongCounter counter =
+ metricManager.longCounter(METER_PROVIDER_NAME, "my_counter", "desc",
null);
+ counter.add(5);
+
+ PrometheusMetricReader reader =
metricManager.getPrometheusMetricReader(METER_PROVIDER_NAME);
+ MetricSnapshots metrics = reader.collect();
+ CounterSnapshot data =
+ metrics.stream()
+ .filter(m ->
m.getMetadata().getPrometheusName().equals("my_counter"))
+ .map(CounterSnapshot.class::cast)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("LongCounter metric not
found"));
+ assertEquals(5, data.getDataPoints().getFirst().getValue(), 0.0);
+
+ metricManager.removeRegistry(METER_PROVIDER_NAME);
+
+ assertNull(metricManager.getPrometheusMetricReader(METER_PROVIDER_NAME));
+ }
+
+ // Helper to grab any snapshot by name and type
+ private <T extends MetricSnapshot> T snapshot(String name, Class<T> cls) {
+ return reader.collect().stream()
+ .filter(m -> m.getMetadata().getPrometheusName().equals(name))
+ .filter(cls::isInstance)
+ .map(cls::cast)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("MetricSnapshot not found: " +
name));
+ }
}
diff --git
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
index 4792be1021c..a0010476393 100644
---
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
+++
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
@@ -16,10 +16,6 @@
*/
package org.apache.solr.response;
-import com.codahale.metrics.Counter;
-import com.codahale.metrics.Gauge;
-import com.codahale.metrics.Meter;
-import com.codahale.metrics.SettableGauge;
import com.codahale.metrics.SharedMetricRegistries;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
@@ -36,10 +32,8 @@ import org.apache.solr.client.solrj.impl.NoOpResponseParser;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
-import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.util.ExternalPaths;
import org.apache.solr.util.SolrJettyTestRule;
-import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
@@ -57,25 +51,22 @@ public class TestPrometheusResponseWriter extends
SolrTestCaseJ4 {
solrClientTestRule.startSolr(LuceneTestCase.createTempDir());
solrClientTestRule
- .newCollection()
+ .newCollection("core1")
+ .withConfigSet(ExternalPaths.DEFAULT_CONFIGSET.toString())
+ .create();
+ solrClientTestRule
+ .newCollection("core2")
.withConfigSet(ExternalPaths.DEFAULT_CONFIGSET.toString())
.create();
var cc = solrClientTestRule.getCoreContainer();
cc.waitForLoadingCoresToFinish(30000);
- SolrMetricManager manager = cc.getMetricManager();
- Counter c = manager.counter(null, "solr.core.collection1",
"QUERY./dummy/metrics.requests");
- c.inc(10);
- c = manager.counter(null, "solr.node", "ADMIN./dummy/metrics.requests");
- c.inc(20);
- Meter m = manager.meter(null, "solr.jetty", "dummyMetrics.2xx-responses");
- m.mark(30);
- registerGauge(manager, "solr.jvm", "gc.dummyMetrics.count");
- }
+ // Populate request metrics on both cores
+ ModifiableSolrParams queryParams = new ModifiableSolrParams();
+ queryParams.set("q", "*:*");
- @AfterClass
- public static void clearMetricsRegistries() {
- SharedMetricRegistries.clear();
+ solrClientTestRule.getSolrClient("core1").query(queryParams);
+ solrClientTestRule.getSolrClient("core2").query(queryParams);
}
@Test
@@ -87,7 +78,6 @@ public class TestPrometheusResponseWriter extends
SolrTestCaseJ4 {
try (SolrClient adminClient =
getHttpSolrClient(solrClientTestRule.getBaseUrl())) {
NamedList<Object> res = adminClient.request(req);
- assertNotNull("null response from server", res);
String output = (String) res.get("response");
Set<String> seenTypeInfo = new HashSet<>();
@@ -130,20 +120,4 @@ public class TestPrometheusResponseWriter extends
SolrTestCaseJ4 {
});
}
}
-
- private static void registerGauge(
- SolrMetricManager metricManager, String registry, String metricName) {
- Gauge<Number> metric =
- new SettableGauge<>() {
- @Override
- public void setValue(Number value) {}
-
- @Override
- public Number getValue() {
- return 0;
- }
- };
- metricManager.registerGauge(
- null, registry, metric, "",
SolrMetricManager.ResolutionStrategy.IGNORE, metricName, "");
- }
}
diff --git
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriterCloud.java
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriterCloud.java
new file mode 100644
index 00000000000..4ed29e43199
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriterCloud.java
@@ -0,0 +1,164 @@
+/*
+ * 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.response;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrRequest.METHOD;
+import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
+import org.apache.solr.client.solrj.impl.InputStreamResponseParser;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestPrometheusResponseWriterCloud extends SolrCloudTestCase {
+
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ System.setProperty("metricsEnabled", "true");
+ configureCluster(1)
+ .addConfig(
+ "config",
TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
+ .configure();
+ }
+
+ @Before
+ public void ensureCollectionsExist() throws Exception {
+ SolrClient client = cluster.getSolrClient();
+ try {
+ CollectionAdminRequest.deleteCollection("collection1").process(client);
+ } catch (Exception ignored) {
+ }
+ try {
+ CollectionAdminRequest.deleteCollection("collection2").process(client);
+ } catch (Exception ignored) {
+ }
+
+ CollectionAdminRequest.createCollection("collection1", "config", 1,
1).process(client);
+ CollectionAdminRequest.createCollection("collection2", "config", 1,
1).process(client);
+ cluster.waitForActiveCollection("collection1", 1, 1);
+ cluster.waitForActiveCollection("collection2", 1, 1);
+ }
+
+ @Test
+ public void testPrometheusCloudLabels() throws Exception {
+ var solrClient = cluster.getSolrClient();
+
+ // Increment solr_metrics_core_requests metric for /select
+ SolrQuery query = new SolrQuery("*:*");
+ solrClient.query("collection1", query);
+
+ var req =
+ new GenericSolrRequest(
+ METHOD.GET,
+ "/admin/metrics",
+ SolrRequestType.ADMIN,
+ new ModifiableSolrParams().set("wt", "prometheus"));
+ req.setResponseParser(new InputStreamResponseParser("prometheus"));
+
+ NamedList<Object> resp = solrClient.request(req);
+ try (InputStream in = (InputStream) resp.get("stream")) {
+ String output = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ assertTrue(
+ "Missing expected Solr cloud mode prometheus metric with cloud
labels",
+ output
+ .lines()
+ .anyMatch(
+ line ->
+ line.startsWith("solr_metrics_core_requests_total")
+ && line.contains("handler=\"/select\"")
+ && line.contains("collection=\"collection1\"")
+ &&
line.contains("core=\"collection1_shard1_replica_n1\"")
+ && line.contains("replica=\"replica_n1\"")
+ && line.contains("shard=\"shard1\"")
+ && line.contains("type=\"requests\"")));
+ }
+ }
+
+ @Test
+ public void testCollectionDeletePrometheusOutput() throws Exception {
+ var solrClient = cluster.getSolrClient();
+
+ // Increment solr_metrics_core_requests metric for /select and assert it
exists
+ SolrQuery query = new SolrQuery("*:*");
+ solrClient.query("collection1", query);
+ solrClient.query("collection2", query);
+
+ var req =
+ new GenericSolrRequest(
+ METHOD.GET,
+ "/admin/metrics",
+ SolrRequestType.ADMIN,
+ new ModifiableSolrParams().set("wt", "prometheus"));
+ req.setResponseParser(new InputStreamResponseParser("prometheus"));
+
+ NamedList<Object> resp = solrClient.request(req);
+
+ try (InputStream in = (InputStream) resp.get("stream")) {
+ String output = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+
+ assertTrue(
+ "Prometheus output should contains solr_metrics_core_requests for
collection1",
+ output
+ .lines()
+ .anyMatch(
+ line ->
+ line.startsWith("solr_metrics_core_requests")
+ && line.contains("collection1")));
+ assertTrue(
+ "Prometheus output should contains solr_metrics_core_requests for
collection2",
+ output
+ .lines()
+ .anyMatch(
+ line ->
+ line.startsWith("solr_metrics_core_requests")
+ && line.contains("collection2")));
+ }
+
+ // Delete collection and assert metrics have been removed
+ var deleteRequest = CollectionAdminRequest.deleteCollection("collection1");
+ deleteRequest.process(solrClient);
+
+ resp = solrClient.request(req);
+ try (InputStream in = (InputStream) resp.get("stream")) {
+ String output = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ assertFalse(
+ "Prometheus output should not contain solr_metrics_core_requests
after collection was deleted",
+ output
+ .lines()
+ .anyMatch(
+ line ->
+ line.startsWith("solr_metrics_core_requests")
+ && line.contains("collection1")));
+ assertTrue(
+ "Prometheus output should contains solr_metrics_core_requests for
collection2",
+ output
+ .lines()
+ .anyMatch(
+ line ->
+ line.startsWith("solr_metrics_core_requests")
+ && line.contains("collection2")));
+ }
+ }
+}
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
index c5f882a6c1d..2d7a1d9a172 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
@@ -25,7 +25,6 @@ import static org.hamcrest.core.StringContains.containsString;
import com.carrotsearch.randomizedtesting.RandomizedContext;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule;
-import io.opentelemetry.api.GlobalOpenTelemetry;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
@@ -113,6 +112,7 @@ import org.apache.solr.common.util.XML;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.CoresLocator;
import org.apache.solr.core.NodeConfig;
+import org.apache.solr.core.OpenTelemetryConfigurator;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrXmlConfig;
@@ -250,7 +250,7 @@ public abstract class SolrTestCaseJ4 extends SolrTestCase {
@BeforeClass
public static void setupTestCases() {
- GlobalOpenTelemetry.resetForTest();
+ OpenTelemetryConfigurator.resetForTest();
resetExceptionIgnores();
testExecutor =
diff --git
a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
index 0841850c4ec..31756bfaf23 100644
---
a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
+++
b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
@@ -1224,6 +1224,7 @@ public class MiniSolrCloudCluster {
Boolean.toString(useDistributedClusterStateUpdate));
if (!disableTraceIdGeneration &&
OpenTelemetryConfigurator.TRACE_ID_GEN_ENABLED) {
+ OpenTelemetryConfigurator.initializeOpenTelemetrySdk(null, null);
injectRandomRecordingFlag();
}