diff --git a/CHANGELOG.md b/CHANGELOG.md index 22223ce98ec..45ba4a00508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - Add a new metric collector for counters which get their value from suppliers [#7894](https://github.com/hyperledger/besu/pull/7894) - Add account and state overrides to `eth_call` [#7801](https://github.com/hyperledger/besu/pull/7801) and `eth_estimateGas` [#7890](https://github.com/hyperledger/besu/pull/7890) - Prometheus Java Metrics library upgraded to version 1.3.3 [#7880](https://github.com/hyperledger/besu/pull/7880) +- Add histogram to Prometheus metrics system [#7944](https://github.com/hyperledger/besu/pull/7944) ### Bug fixes - Fix registering new metric categories from plugins [#7825](https://github.com/hyperledger/besu/pull/7825) diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/noop/NoOpMetricsSystem.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/noop/NoOpMetricsSystem.java index fd8a7c935f2..a4c95cbb819 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/noop/NoOpMetricsSystem.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/noop/NoOpMetricsSystem.java @@ -18,6 +18,7 @@ import org.hyperledger.besu.metrics.Observation; import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.ExternalSummary; +import org.hyperledger.besu.plugin.services.metrics.Histogram; import org.hyperledger.besu.plugin.services.metrics.LabelledGauge; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; @@ -63,6 +64,9 @@ public class NoOpMetricsSystem implements ObservableMetricsSystem { public static final LabelledMetric NO_OP_LABELLED_1_OPERATION_TIMER = new LabelCountingNoOpMetric<>(1, NO_OP_OPERATION_TIMER); + /** The constant NO_OP_HISTOGRAM. */ + public static final Histogram NO_OP_HISTOGRAM = d -> {}; + /** Default constructor */ public NoOpMetricsSystem() {} @@ -130,6 +134,26 @@ public void createGauge( final String help, final DoubleSupplier valueSupplier) {} + @Override + public LabelledMetric createLabelledHistogram( + final MetricCategory category, + final String name, + final String help, + final double[] buckets, + final String... labelNames) { + return getHistogramLabelledMetric(labelNames.length); + } + + /** + * Gets histogram labelled metric. + * + * @param labelCount the label count + * @return the histogram labelled metric + */ + public static LabelledMetric getHistogramLabelledMetric(final int labelCount) { + return new LabelCountingNoOpMetric<>(labelCount, NO_OP_HISTOGRAM); + } + @Override public void createGuavaCacheCollector( final MetricCategory category, final String name, final Cache cache) {} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java index 10da5f40e2e..33389828d3c 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java @@ -20,6 +20,7 @@ import org.hyperledger.besu.metrics.StandardMetricCategory; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.Histogram; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; @@ -289,6 +290,17 @@ public void createGauge( } } + @Override + public LabelledMetric createLabelledHistogram( + final MetricCategory category, + final String name, + final String help, + final double[] buckets, + final String... labelNames) { + // not yet supported + return NoOpMetricsSystem.getHistogramLabelledMetric(labelNames.length); + } + @Override public void createGuavaCacheCollector( final MetricCategory category, final String name, final Cache cache) {} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusHistogram.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusHistogram.java new file mode 100644 index 00000000000..74cc7db4e82 --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusHistogram.java @@ -0,0 +1,113 @@ +/* + * Copyright contributors to Besu. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.metrics.prometheus; + +import static org.hyperledger.besu.metrics.prometheus.PrometheusCollector.addLabelValues; +import static org.hyperledger.besu.metrics.prometheus.PrometheusCollector.getLabelValues; + +import org.hyperledger.besu.metrics.Observation; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +import java.util.ArrayList; +import java.util.stream.Stream; + +import io.prometheus.metrics.core.metrics.Histogram; +import io.prometheus.metrics.model.registry.PrometheusRegistry; + +/** + * Abstract base class for Prometheus histogram collectors. A histogram samples durations and counts + * them in configurable buckets. * It also provides a sum of all observed values. + */ +abstract class AbstractPrometheusHistogram extends CategorizedPrometheusCollector { + protected Histogram histogram; + + protected AbstractPrometheusHistogram( + final MetricCategory category, + final String name, + final String help, + final double[] buckets, + final String... labelNames) { + super(category, name); + this.histogram = + Histogram.builder() + .name(this.prefixedName) + .help(help) + .labelNames(labelNames) + .classicOnly() + .classicUpperBounds(buckets) + .build(); + } + + @Override + public String getIdentifier() { + return histogram.getPrometheusName(); + } + + @Override + public void register(final PrometheusRegistry registry) { + registry.register(histogram); + } + + @Override + public void unregister(final PrometheusRegistry registry) { + registry.unregister(histogram); + } + + @Override + public Stream streamObservations() { + final var snapshot = histogram.collect(); + return snapshot.getDataPoints().stream() + .flatMap( + dataPoint -> { + if (!dataPoint.hasClassicHistogramData()) { + throw new IllegalStateException("Only classic histogram are supported"); + } + + final var labelValues = getLabelValues(dataPoint.getLabels()); + final var classicBuckets = dataPoint.getClassicBuckets(); + final var observations = new ArrayList(classicBuckets.size() + 2); + + if (dataPoint.hasSum()) { + observations.add( + new Observation( + category, name, dataPoint.getSum(), addLabelValues(labelValues, "sum"))); + } + + if (dataPoint.hasCount()) { + observations.add( + new Observation( + category, + name, + dataPoint.getCount(), + addLabelValues(labelValues, "count"))); + } + + classicBuckets.stream() + .forEach( + bucket -> + observations.add( + new Observation( + category, + name, + bucket.getCount(), + addLabelValues( + labelValues, + "bucket", + Double.toString(bucket.getUpperBound()))))); + + return observations.stream(); + }); + } +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusHistogram.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusHistogram.java new file mode 100644 index 00000000000..53c315ee9b8 --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusHistogram.java @@ -0,0 +1,41 @@ +/* + * Copyright contributors to Besu. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.metrics.prometheus; + +import org.hyperledger.besu.plugin.services.metrics.Histogram; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +/** + * A Prometheus histogram. A histogram samples durations and counts them in configurable buckets. It + * also provides a sum of all observed values. + */ +class PrometheusHistogram extends AbstractPrometheusHistogram implements LabelledMetric { + + public PrometheusHistogram( + final MetricCategory category, + final String name, + final String help, + final double[] buckets, + final String... labelNames) { + super(category, name, help, buckets, labelNames); + } + + @Override + public Histogram labels(final String... labels) { + final var ddp = histogram.labelValues(labels); + return ddp::observe; + } +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystem.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystem.java index c4ff1f9cedd..a207cc5b70e 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystem.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystem.java @@ -21,6 +21,7 @@ import org.hyperledger.besu.metrics.StandardMetricCategory; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.Histogram; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; @@ -67,6 +68,9 @@ public class PrometheusMetricsSystem implements ObservableMetricsSystem { new ConcurrentHashMap<>(); private final Map> cachedTimers = new ConcurrentHashMap<>(); + private final Map> cachedHistograms = + new ConcurrentHashMap<>(); + private final PrometheusGuavaCache.Context guavaCacheCollectorContext = new PrometheusGuavaCache.Context(); @@ -164,6 +168,26 @@ public LabelledMetric createSimpleLabelledTimer( }); } + @Override + public LabelledMetric createLabelledHistogram( + final MetricCategory category, + final String name, + final String help, + final double[] buckets, + final String... labelNames) { + return cachedHistograms.computeIfAbsent( + CachedMetricKey.of(category, name), + k -> { + if (isCategoryEnabled(category)) { + final var histogram = + new PrometheusHistogram(category, name, help, buckets, labelNames); + registerCollector(category, histogram); + return histogram; + } + return NoOpMetricsSystem.getHistogramLabelledMetric(labelNames.length); + }); + } + @Override public LabelledSuppliedSummary createLabelledSuppliedSummary( final MetricCategory category, diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSimpleTimer.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSimpleTimer.java index 1a2693b7216..b3c47de9420 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSimpleTimer.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSimpleTimer.java @@ -14,43 +14,24 @@ */ package org.hyperledger.besu.metrics.prometheus; -import static org.hyperledger.besu.metrics.prometheus.PrometheusCollector.addLabelValues; -import static org.hyperledger.besu.metrics.prometheus.PrometheusCollector.getLabelValues; - -import org.hyperledger.besu.metrics.Observation; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; -import java.util.stream.Stream; - -import io.prometheus.metrics.core.metrics.Histogram; -import io.prometheus.metrics.model.registry.PrometheusRegistry; - /** * An implementation of Besu simple timer backed by a Prometheus histogram. The histogram samples * durations and counts them in configurable buckets. It also provides a sum of all observed values. */ -class PrometheusSimpleTimer extends CategorizedPrometheusCollector +class PrometheusSimpleTimer extends AbstractPrometheusHistogram implements LabelledMetric { - private final Histogram histogram; - public PrometheusSimpleTimer( final MetricCategory category, final String name, final String help, final double[] buckets, final String... labelNames) { - super(category, name); - this.histogram = - Histogram.builder() - .name(this.prefixedName) - .help(help) - .labelNames(labelNames) - .classicOnly() - .classicUpperBounds(buckets) - .build(); + super(category, name, help, buckets, labelNames); } @Override @@ -58,42 +39,4 @@ public OperationTimer labels(final String... labels) { final var ddp = histogram.labelValues(labels); return () -> ddp.startTimer()::observeDuration; } - - @Override - public String getIdentifier() { - return histogram.getPrometheusName(); - } - - @Override - public void register(final PrometheusRegistry registry) { - registry.register(histogram); - } - - @Override - public void unregister(final PrometheusRegistry registry) { - registry.unregister(histogram); - } - - @Override - public Stream streamObservations() { - final var snapshot = histogram.collect(); - return snapshot.getDataPoints().stream() - .flatMap( - dataPoint -> { - final var labelValues = getLabelValues(dataPoint.getLabels()); - if (!dataPoint.hasClassicHistogramData()) { - throw new IllegalStateException("Only classic histogram are supported"); - } - - return dataPoint.getClassicBuckets().stream() - .map( - bucket -> - new Observation( - category, - name, - bucket.getCount(), - addLabelValues( - labelValues, Double.toString(bucket.getUpperBound())))); - }); - } } diff --git a/metrics/core/src/test-support/java/org/hyperledger/besu/metrics/StubMetricsSystem.java b/metrics/core/src/test-support/java/org/hyperledger/besu/metrics/StubMetricsSystem.java index 4dfcd2fff9c..9a4668b3c0d 100644 --- a/metrics/core/src/test-support/java/org/hyperledger/besu/metrics/StubMetricsSystem.java +++ b/metrics/core/src/test-support/java/org/hyperledger/besu/metrics/StubMetricsSystem.java @@ -18,6 +18,7 @@ import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.Histogram; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; @@ -114,6 +115,16 @@ public void createGauge( gauges.put(name, valueSupplier); } + @Override + public LabelledMetric createLabelledHistogram( + final MetricCategory category, + final String name, + final String help, + final double[] buckets, + final String... labelNames) { + return NoOpMetricsSystem.getHistogramLabelledMetric(labelNames.length); + } + @Override public void createGuavaCacheCollector( final MetricCategory category, final String name, final Cache cache) {} diff --git a/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystemTest.java b/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystemTest.java index 9f1878f5998..60b125fe507 100644 --- a/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystemTest.java +++ b/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystemTest.java @@ -34,6 +34,7 @@ import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.Histogram; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; @@ -41,6 +42,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.stream.IntStream; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -171,6 +173,22 @@ public void shouldCreateObservationsFromTimer() { new Observation(RPC, "request", 1L, singletonList("count"))); } + @Test + public void shouldCreateObservationsFromHistogram() { + final Histogram histogram = + metricsSystem.createHistogram(RPC, "request", "Some help", new double[] {5.0, 9.0}); + + IntStream.rangeClosed(1, 10).forEach(histogram::observe); + + assertThat(metricsSystem.streamObservations()) + .containsExactlyInAnyOrder( + new Observation(RPC, "request", 5L, asList("bucket", "5.0")), + new Observation(RPC, "request", 4L, asList("bucket", "9.0")), + new Observation(RPC, "request", 1L, asList("bucket", "Infinity")), + new Observation(RPC, "request", 55.0, singletonList("sum")), + new Observation(RPC, "request", 10L, singletonList("count"))); + } + @Test public void shouldHandleDuplicateTimerCreation() { final LabelledMetric timer1 = diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 1204d921d43..067fe5bd7e0 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = '0suP4G0+vTbIvbBfaH+pOpNTEDaf2Hq+byXDyHc2i2E=' + knownHash = 'f6fi+lsYVZtFjmGOyiMPPCfNDie4SIPpj6HVgXRxF8Q=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/MetricsSystem.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/MetricsSystem.java index 7b466f85663..0516cccf073 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/MetricsSystem.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/MetricsSystem.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.ExternalSummary; +import org.hyperledger.besu.plugin.services.metrics.Histogram; import org.hyperledger.besu.plugin.services.metrics.LabelledGauge; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; @@ -225,6 +226,33 @@ default void createLongGauge( createGauge(category, name, help, () -> (double) valueSupplier.getAsLong()); } + /** + * Creates a histogram with assigned labels + * + * @param category The {@link MetricCategory} this histogram is assigned to. + * @param name A name for this metric. + * @param help A human-readable description of the metric. + * @param buckets An array of buckets to assign to the histogram + * @param labelNames An array of labels to assign to the histogram. + * @return The labelled histogram. + */ + LabelledMetric createLabelledHistogram( + MetricCategory category, String name, String help, double[] buckets, String... labelNames); + + /** + * Creates a histogram + * + * @param category The {@link MetricCategory} this histogram is assigned to. + * @param name A name for this metric. + * @param help A human-readable description of the metric. + * @param buckets An array of buckets to assign to the histogram + * @return The labelled histogram. + */ + default Histogram createHistogram( + final MetricCategory category, final String name, final String help, final double[] buckets) { + return createLabelledHistogram(category, name, help, buckets).labels(); + } + /** * Create a summary with assigned labels, that is computed externally to this metric system. * Useful when existing libraries calculate the summary data on their own, and we want to export diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/metrics/Histogram.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/metrics/Histogram.java new file mode 100644 index 00000000000..2a63e9c7254 --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/metrics/Histogram.java @@ -0,0 +1,29 @@ +/* + * Copyright contributors to Besu. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.metrics; + +/** + * A histogram samples observations (usually things like request durations or response sizes) and + * counts them in configurable buckets. It also provides a sum of all observed values. + */ +public interface Histogram { + + /** + * Observe the given amount. + * + * @param amount the amount + */ + void observe(double amount); +}