From 5b2da5a068d54e3afb2ef77d67f3b12a2e6c0291 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 27 Nov 2024 12:04:31 +0100 Subject: [PATCH] Migrate to Prometheus lib 1.x (#7880) * Upgrade to Promethus java client 1.x and adapt the code to the new version Signed-off-by: Fabio Di Fabio * Update CHANGELOG.md Co-authored-by: Sally MacFarlane Signed-off-by: Fabio Di Fabio --------- Signed-off-by: Fabio Di Fabio Co-authored-by: Sally MacFarlane --- CHANGELOG.md | 16 + .../org/hyperledger/besu/RunnerBuilder.java | 8 +- .../subcommands/blocks/BlocksSubCommand.java | 4 +- .../internal/methods/DebugMetrics.java | 12 +- .../ethereum/chain/DefaultBlockchain.java | 2 +- gradle/verification-metadata.xml | 546 +++++------------- metrics/core/build.gradle | 11 +- .../besu/metrics/MetricsService.java | 10 +- .../hyperledger/besu/metrics/Observation.java | 105 +--- .../besu/metrics/noop/NoOpMetricsSystem.java | 84 ++- .../opentelemetry/OpenTelemetrySystem.java | 12 +- .../prometheus/AbstractPrometheusSummary.java | 132 +++++ ...tractPrometheusSuppliedValueCollector.java | 96 +++ .../CategorizedPrometheusCollector.java | 50 ++ .../prometheus/CurrentValueCollector.java | 59 -- .../prometheus/MetricsHttpService.java | 210 ++----- .../prometheus/MetricsPushGatewayService.java | 14 +- .../prometheus/PrometheusCollector.java | 82 +++ .../metrics/prometheus/PrometheusCounter.java | 74 ++- .../prometheus/PrometheusGuavaCache.java | 171 ++++++ .../prometheus/PrometheusMetricsSystem.java | 374 ++++-------- .../prometheus/PrometheusSimpleTimer.java | 76 ++- .../prometheus/PrometheusSuppliedCounter.java | 73 +++ .../prometheus/PrometheusSuppliedGauge.java | 73 +++ .../prometheus/PrometheusSuppliedSummary.java | 83 +++ .../PrometheusSuppliedValueCollector.java | 81 --- .../metrics/prometheus/PrometheusTimer.java | 28 +- .../besu/metrics/StubMetricsSystem.java | 9 +- .../OpenTelemetryMetricsSystemTest.java | 10 +- .../prometheus/MetricsHttpServiceTest.java | 76 +-- .../PrometheusMetricsSystemTest.java | 135 ++--- .../besu/metrics/rocksdb/RocksDBStats.java | 2 +- platform/build.gradle | 2 +- plugin-api/build.gradle | 2 +- .../besu/plugin/services/MetricsSystem.java | 34 +- .../metrics/LabelledSuppliedSummary.java | 28 + 36 files changed, 1504 insertions(+), 1280 deletions(-) create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSummary.java create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSuppliedValueCollector.java create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CategorizedPrometheusCollector.java delete mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CurrentValueCollector.java create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCollector.java create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusGuavaCache.java create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedCounter.java create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedGauge.java create mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedSummary.java delete mode 100644 metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedValueCollector.java create mode 100644 plugin-api/src/main/java/org/hyperledger/besu/plugin/services/metrics/LabelledSuppliedSummary.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e02ad1bc026..22223ce98ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ ### Breaking Changes - Removed Retesteth rpc service and commands [#7833](https://github.com/hyperledger/besu/pull/7783) +- With the upgrade of the Prometheus Java Metrics library, there are the following changes: + - Gauge names are not allowed to end with `total`, therefore the metric `besu_blockchain_difficulty_total` is losing the `_total` suffix + - The `_created` timestamps are not returned by default, you can set the env var `BESU_OPTS="-Dio.prometheus.exporter.includeCreatedTimestamps=true"` to enable them + - Some JVM metrics have changed name to adhere to the OTEL standard (see the table below), [Besu Full Grafana dashboard](https://grafana.com/grafana/dashboards/16455-besu-full/) is updated to support both names + + | Old Name | New Name | + |---------------------------------|---------------------------------| + | jvm_memory_bytes_committed | jvm_memory_committed_bytes | + | jvm_memory_bytes_init | jvm_memory_init_bytes | + | jvm_memory_bytes_max | jvm_memory_max_bytes | + | jvm_memory_bytes_used | jvm_memory_used_bytes | + | jvm_memory_pool_bytes_committed | jvm_memory_pool_committed_bytes | + | jvm_memory_pool_bytes_init | jvm_memory_pool_init_bytes | + | jvm_memory_pool_bytes_max | jvm_memory_pool_max_bytes | + | jvm_memory_pool_bytes_used | jvm_memory_pool_used_bytes | ### Upcoming Breaking Changes - Plugin API will be deprecating the BesuContext interface to be replaced with the ServiceManager interface. @@ -26,6 +41,7 @@ - Add a method to check if a metric category is enabled to the plugin API [#7832](https://github.com/hyperledger/besu/pull/7832) - 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) ### Bug fixes - Fix registering new metric categories from plugins [#7825](https://github.com/hyperledger/besu/pull/7825) diff --git a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java index 06dca6c6f0a..0f398fae1eb 100644 --- a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -1034,8 +1034,7 @@ public Runner build() { subscriptionManager, privacyParameters, context.getBlockchain().getGenesisBlockHeader()); } - final Optional metricsService = - createMetricsService(vertx, metricsConfiguration); + final Optional metricsService = createMetricsService(metricsConfiguration); final Optional ethStatsService; if (isEthStatsEnabled()) { @@ -1469,9 +1468,8 @@ private WebSocketService createWebsocketService( vertx, configuration, websocketMessageHandler, authenticationService, metricsSystem); } - private Optional createMetricsService( - final Vertx vertx, final MetricsConfiguration configuration) { - return MetricsService.create(vertx, configuration, metricsSystem); + private Optional createMetricsService(final MetricsConfiguration configuration) { + return MetricsService.create(configuration, metricsSystem); } /** diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/blocks/BlocksSubCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/blocks/BlocksSubCommand.java index 95617fe8ca1..5862b11c19b 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/blocks/BlocksSubCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/blocks/BlocksSubCommand.java @@ -53,7 +53,6 @@ import java.util.function.Function; import java.util.function.Supplier; -import io.vertx.core.Vertx; import jakarta.validation.constraints.NotBlank; import org.apache.tuweni.bytes.Bytes; import org.slf4j.Logger; @@ -458,8 +457,7 @@ private static Optional initMetrics(final BlocksSubCommand paren parentCommand.parentCommand.metricsConfiguration(); Optional metricsService = - MetricsService.create( - Vertx.vertx(), metricsConfiguration, parentCommand.parentCommand.getMetricsSystem()); + MetricsService.create(metricsConfiguration, parentCommand.parentCommand.getMetricsSystem()); metricsService.ifPresent(MetricsService::start); return metricsService; } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugMetrics.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugMetrics.java index b5c74245ce4..c3eb1906f30 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugMetrics.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugMetrics.java @@ -50,9 +50,9 @@ public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { private void addObservation( final Map observations, final Observation observation) { final Map categoryObservations = - getNextMapLevel(observations, observation.getCategory().getName()); - if (observation.getLabels().isEmpty()) { - categoryObservations.put(observation.getMetricName(), observation.getValue()); + getNextMapLevel(observations, observation.category().getName()); + if (observation.labels().isEmpty()) { + categoryObservations.put(observation.metricName(), observation.value()); } else { addLabelledObservation(categoryObservations, observation); } @@ -60,12 +60,12 @@ private void addObservation( private void addLabelledObservation( final Map categoryObservations, final Observation observation) { - final List labels = observation.getLabels(); - Map values = getNextMapLevel(categoryObservations, observation.getMetricName()); + final List labels = observation.labels(); + Map values = getNextMapLevel(categoryObservations, observation.metricName()); for (int i = 0; i < labels.size() - 1; i++) { values = getNextMapLevel(values, labels.get(i)); } - values.put(labels.get(labels.size() - 1), observation.getValue()); + values.put(labels.get(labels.size() - 1), observation.value()); } @SuppressWarnings("unchecked") diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/DefaultBlockchain.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/DefaultBlockchain.java index f6e06f82d2e..8a354638f01 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/DefaultBlockchain.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/chain/DefaultBlockchain.java @@ -183,7 +183,7 @@ private void createGauges(final MetricsSystem metricsSystem) { metricsSystem.createGauge( BLOCKCHAIN, - "difficulty_total", + "difficulty", "Total difficulty of the chainhead", () -> this.getChainHead().getTotalDifficulty().toBigInteger().doubleValue()); diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 652bbb9e3d6..d6392fbe2ce 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -464,35 +464,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1924,14 +1895,6 @@ - - - - - - - - @@ -1945,11 +1908,6 @@ - - - - - @@ -1960,14 +1918,6 @@ - - - - - - - - @@ -1976,14 +1926,6 @@ - - - - - - - - @@ -1992,14 +1934,6 @@ - - - - - - - - @@ -2008,14 +1942,6 @@ - - - - - - - - @@ -2029,14 +1955,6 @@ - - - - - - - - @@ -2050,14 +1968,6 @@ - - - - - - - - @@ -2066,14 +1976,6 @@ - - - - - - - - @@ -2082,14 +1984,6 @@ - - - - - - - - @@ -2098,14 +1992,6 @@ - - - - - - - - @@ -2114,14 +2000,6 @@ - - - - - - - - @@ -2130,14 +2008,6 @@ - - - - - - - - @@ -2146,14 +2016,6 @@ - - - - - - - - @@ -2162,14 +2024,6 @@ - - - - - - - - @@ -2183,14 +2037,6 @@ - - - - - - - - @@ -2204,14 +2050,6 @@ - - - - - - - - @@ -2225,14 +2063,6 @@ - - - - - - - - @@ -2241,14 +2071,6 @@ - - - - - - - - @@ -2262,11 +2084,6 @@ - - - - - @@ -2277,14 +2094,6 @@ - - - - - - - - @@ -2298,14 +2107,6 @@ - - - - - - - - @@ -2314,14 +2115,6 @@ - - - - - - - - @@ -2330,17 +2123,6 @@ - - - - - - - - - - - @@ -2357,14 +2139,6 @@ - - - - - - - - @@ -2373,14 +2147,6 @@ - - - - - - - - @@ -2389,11 +2155,6 @@ - - - - - @@ -2404,14 +2165,6 @@ - - - - - - - - @@ -2420,14 +2173,6 @@ - - - - - - - - @@ -2436,14 +2181,6 @@ - - - - - - - - @@ -2457,23 +2194,6 @@ - - - - - - - - - - - - - - - - - @@ -2496,20 +2216,6 @@ - - - - - - - - - - - - - - @@ -2529,14 +2235,6 @@ - - - - - - - - @@ -2545,14 +2243,6 @@ - - - - - - - - @@ -2561,14 +2251,6 @@ - - - - - - - - @@ -2577,14 +2259,6 @@ - - - - - - - - @@ -2636,6 +2310,22 @@ + + + + + + + + + + + + + + + + @@ -2803,6 +2493,22 @@ + + + + + + + + + + + + + + + + @@ -2923,11 +2629,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2936,11 +2769,6 @@ - - - - - @@ -2949,22 +2777,6 @@ - - - - - - - - - - - - - - - - @@ -2973,14 +2785,6 @@ - - - - - - - - @@ -5576,14 +5380,6 @@ - - - - - - - - @@ -6108,14 +5904,6 @@ - - - - - - - - @@ -6525,14 +6313,6 @@ - - - - - - - - @@ -6673,14 +6453,6 @@ - - - - - - - - diff --git a/metrics/core/build.gradle b/metrics/core/build.gradle index 160093c7490..e10057d466f 100644 --- a/metrics/core/build.gradle +++ b/metrics/core/build.gradle @@ -55,11 +55,12 @@ dependencies { implementation 'io.opentelemetry:opentelemetry-sdk-extension-autoconfigure' implementation 'io.opentelemetry.semconv:opentelemetry-semconv' - implementation 'io.prometheus:simpleclient' - implementation 'io.prometheus:simpleclient_common' - implementation 'io.prometheus:simpleclient_guava' - implementation 'io.prometheus:simpleclient_hotspot' - implementation 'io.prometheus:simpleclient_pushgateway' + implementation 'io.prometheus:prometheus-metrics-core' + implementation 'io.prometheus:prometheus-metrics-instrumentation-guava' + implementation 'io.prometheus:prometheus-metrics-instrumentation-jvm' + implementation 'io.prometheus:prometheus-metrics-exporter-httpserver' + implementation 'io.prometheus:prometheus-metrics-exporter-pushgateway' + implementation 'io.vertx:vertx-core' implementation 'io.vertx:vertx-web' diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsService.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsService.java index 747ed9cc762..7eb37061503 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsService.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsService.java @@ -18,12 +18,12 @@ import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; import org.hyperledger.besu.metrics.prometheus.MetricsHttpService; import org.hyperledger.besu.metrics.prometheus.MetricsPushGatewayService; +import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem; import org.hyperledger.besu.plugin.services.MetricsSystem; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import io.vertx.core.Vertx; import org.slf4j.LoggerFactory; /** @@ -35,20 +35,18 @@ public interface MetricsService { /** * Create Metrics Service. * - * @param vertx the vertx * @param configuration the configuration * @param metricsSystem the metrics system * @return the optional Metrics Service */ static Optional create( - final Vertx vertx, - final MetricsConfiguration configuration, - final MetricsSystem metricsSystem) { + final MetricsConfiguration configuration, final MetricsSystem metricsSystem) { LoggerFactory.getLogger(MetricsService.class) .trace("Creating metrics service {}", configuration.getProtocol()); if (configuration.getProtocol() == MetricsProtocol.PROMETHEUS) { if (configuration.isEnabled()) { - return Optional.of(new MetricsHttpService(vertx, configuration, metricsSystem)); + return Optional.of( + new MetricsHttpService(configuration, (PrometheusMetricsSystem) metricsSystem)); } else if (configuration.isPushEnabled()) { return Optional.of(new MetricsPushGatewayService(configuration, metricsSystem)); } else { diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/Observation.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/Observation.java index 81ca8b5a75f..280eeef220c 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/Observation.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/Observation.java @@ -17,99 +17,14 @@ import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import java.util.List; -import java.util.Objects; -import com.google.common.base.MoreObjects; - -/** The Observation. */ -public class Observation { - private final MetricCategory category; - private final String metricName; - private final List labels; - private final Object value; - - /** - * Instantiates a new Observation. - * - * @param category the category - * @param metricName the metric name - * @param value the value - * @param labels the labels - */ - public Observation( - final MetricCategory category, - final String metricName, - final Object value, - final List labels) { - this.category = category; - this.metricName = metricName; - this.value = value; - this.labels = labels; - } - - /** - * Gets category. - * - * @return the category - */ - public MetricCategory getCategory() { - return category; - } - - /** - * Gets metric name. - * - * @return the metric name - */ - public String getMetricName() { - return metricName; - } - - /** - * Gets labels. - * - * @return the labels - */ - public List getLabels() { - return labels; - } - - /** - * Gets value. - * - * @return the value - */ - public Object getValue() { - return value; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final Observation that = (Observation) o; - return Objects.equals(category, that.category) - && Objects.equals(metricName, that.metricName) - && Objects.equals(labels, that.labels) - && Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(category, metricName, labels, value); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("category", category) - .add("metricName", metricName) - .add("labels", labels) - .add("value", value) - .toString(); - } -} +/** + * The Observation. + * + * @param category the category + * @param metricName the metric name + * @param value the value + * @param labels the labels + */ +public record Observation( + MetricCategory category, String metricName, Object value, List labels) {} 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 68b5d52ffa3..fd8a7c935f2 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 @@ -21,6 +21,7 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledGauge; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; @@ -41,9 +42,6 @@ public class NoOpMetricsSystem implements ObservableMetricsSystem { /** The constant NO_OP_COUNTER. */ public static final Counter NO_OP_COUNTER = new NoOpCounter(); - /** The constant NO_OP_GAUGE. */ - public static final LabelledSuppliedMetric NO_OP_GAUGE = new NoOpValueCollector(); - private static final OperationTimer.TimingContext NO_OP_TIMING_CONTEXT = () -> 0; /** The constant NO_OP_OPERATION_TIMER. */ @@ -65,18 +63,6 @@ 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_LABELLED_1_GAUGE. */ - public static final LabelledSuppliedMetric NO_OP_LABELLED_1_GAUGE = - new LabelledSuppliedNoOpMetric(1, NO_OP_GAUGE); - - /** The constant NO_OP_LABELLED_2_GAUGE. */ - public static final LabelledSuppliedMetric NO_OP_LABELLED_2_GAUGE = - new LabelledSuppliedNoOpMetric(2, NO_OP_GAUGE); - - /** The constant NO_OP_LABELLED_3_GAUGE. */ - public static final LabelledSuppliedMetric NO_OP_LABELLED_3_GAUGE = - new LabelledSuppliedNoOpMetric(3, NO_OP_GAUGE); - /** Default constructor */ public NoOpMetricsSystem() {} @@ -96,16 +82,7 @@ public LabelledMetric createLabelledCounter( * @return the counter labelled metric */ public static LabelledMetric getCounterLabelledMetric(final int labelCount) { - switch (labelCount) { - case 1: - return NO_OP_LABELLED_1_COUNTER; - case 2: - return NO_OP_LABELLED_2_COUNTER; - case 3: - return NO_OP_LABELLED_3_COUNTER; - default: - return new LabelCountingNoOpMetric<>(labelCount, NO_OP_COUNTER); - } + return new LabelCountingNoOpMetric<>(labelCount, NO_OP_COUNTER); } @Override @@ -118,11 +95,13 @@ public LabelledMetric createSimpleLabelledTimer( } @Override - public void trackExternalSummary( + public LabelledSuppliedSummary createLabelledSuppliedSummary( final MetricCategory category, final String name, final String help, - final Supplier summarySupplier) {} + final String... labelNames) { + return getLabelledSuppliedSummary(labelNames.length); + } @Override public LabelledMetric createLabelledTimer( @@ -141,11 +120,7 @@ public LabelledMetric createLabelledTimer( */ public static LabelledMetric getOperationTimerLabelledMetric( final int labelCount) { - if (labelCount == 1) { - return NO_OP_LABELLED_1_OPERATION_TIMER; - } else { - return new LabelCountingNoOpMetric<>(labelCount, NO_OP_OPERATION_TIMER); - } + return new LabelCountingNoOpMetric<>(labelCount, NO_OP_OPERATION_TIMER); } @Override @@ -184,16 +159,17 @@ public LabelledSuppliedMetric createLabelledSuppliedGauge( * @return the labelled gauge */ public static LabelledSuppliedMetric getLabelledSuppliedMetric(final int labelCount) { - switch (labelCount) { - case 1: - return NO_OP_LABELLED_1_GAUGE; - case 2: - return NO_OP_LABELLED_2_GAUGE; - case 3: - return NO_OP_LABELLED_3_GAUGE; - default: - return new LabelledSuppliedNoOpMetric(labelCount, NO_OP_GAUGE); - } + return new LabelledSuppliedNoOpMetric(labelCount); + } + + /** + * Gets labelled supplied histogram. + * + * @param labelCount the label count + * @return the labelled gauge + */ + public static LabelledSuppliedSummary getLabelledSuppliedSummary(final int labelCount) { + return new LabelledSuppliedNoOpMetric(labelCount); } @Override @@ -249,7 +225,8 @@ public T labels(final String... labels) { /** The Labelled supplied NoOp metric. */ @SuppressWarnings("removal") // remove when deprecated LabelledGauge is removed - public static class LabelledSuppliedNoOpMetric implements LabelledSuppliedMetric, LabelledGauge { + public static class LabelledSuppliedNoOpMetric + implements LabelledSuppliedMetric, LabelledGauge, LabelledSuppliedSummary { /** The Label count. */ final int labelCount; @@ -257,22 +234,26 @@ public static class LabelledSuppliedNoOpMetric implements LabelledSuppliedMetric final List labelValuesCache = new ArrayList<>(); /** - * Instantiates a new Labelled gauge NoOp metric. + * Instantiates a new Labelled supplied NoOp metric. * * @param labelCount the label count - * @param fakeMetric the fake metric */ - public LabelledSuppliedNoOpMetric( - final int labelCount, final LabelledSuppliedMetric fakeMetric) { + public LabelledSuppliedNoOpMetric(final int labelCount) { this.labelCount = labelCount; - this.fakeMetric = fakeMetric; } - /** The Fake metric. */ - final LabelledSuppliedMetric fakeMetric; - @Override public void labels(final DoubleSupplier valueSupplier, final String... labelValues) { + internalLabels(valueSupplier, labelValues); + } + + @Override + public void labels( + final Supplier summarySupplier, final String... labelValues) { + internalLabels(summarySupplier, labelValues); + } + + private void internalLabels(final Object valueSupplier, final String... labelValues) { final String labelValuesString = String.join(",", labelValues); Preconditions.checkArgument( !labelValuesCache.contains(labelValuesString), @@ -281,6 +262,7 @@ public void labels(final DoubleSupplier valueSupplier, final String... labelValu labelValues.length == labelCount, "The count of labels used must match the count of labels expected."); Preconditions.checkNotNull(valueSupplier, "No valueSupplier specified"); + labelValuesCache.add(labelValuesString); } } } 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 cc2174dff53..10da5f40e2e 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,9 +20,9 @@ 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.ExternalSummary; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; @@ -41,7 +41,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.DoubleSupplier; -import java.util.function.Supplier; import java.util.stream.Stream; import javax.inject.Singleton; @@ -134,7 +133,7 @@ public OpenTelemetrySystem( @Override public Stream streamObservations(final MetricCategory category) { - return streamObservations().filter(metricData -> metricData.getCategory().equals(category)); + return streamObservations().filter(metricData -> metricData.category().equals(category)); } @Override @@ -246,11 +245,14 @@ public LabelledMetric createSimpleLabelledTimer( } @Override - public void trackExternalSummary( + public LabelledSuppliedSummary createLabelledSuppliedSummary( final MetricCategory category, final String name, final String help, - final Supplier summarySupplier) {} + final String... labelNames) { + // not yet supported + return (LabelledSuppliedSummary) NoOpMetricsSystem.getLabelledSuppliedMetric(labelNames.length); + } @Override public LabelledMetric createLabelledTimer( diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSummary.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSummary.java new file mode 100644 index 00000000000..6de25970694 --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSummary.java @@ -0,0 +1,132 @@ +/* + * 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.model.registry.Collector; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.SummarySnapshot; + +/** + * Abstract base class for Prometheus summary collectors. A summary provides a total count of + * observations and a sum of all observed values, it calculates configurable quantiles over a + * sliding time window. + */ +abstract class AbstractPrometheusSummary extends CategorizedPrometheusCollector { + /** The Prometheus collector */ + protected Collector collector; + + /** + * Constructs a new AbstractPrometheusSummary. + * + * @param category The {@link MetricCategory} this collector is assigned to + * @param name The name of this collector + */ + protected AbstractPrometheusSummary(final MetricCategory category, final String name) { + super(category, name); + } + + /** + * Gets the identifier for this collector. + * + * @return The Prometheus name of the collector + */ + @Override + public String getIdentifier() { + return collector.getPrometheusName(); + } + + /** + * Registers this collector with the given Prometheus registry. + * + * @param registry The Prometheus registry to register this collector with + */ + @Override + public void register(final PrometheusRegistry registry) { + registry.register(collector); + } + + /** + * Unregisters this collector from the given Prometheus registry. + * + * @param registry The Prometheus registry to unregister this collector from + */ + @Override + public void unregister(final PrometheusRegistry registry) { + registry.unregister(collector); + } + + /** + * Collects the summary snapshot from the Prometheus collector. + * + * @return The collected summary snapshot + */ + private SummarySnapshot collect() { + return (SummarySnapshot) collector.collect(); + } + + /** + * Streams the observations from the collected summary snapshot. + * + * @return A stream of observations + */ + @Override + public Stream streamObservations() { + return collect().getDataPoints().stream() + .flatMap( + dataPoint -> { + final var labelValues = getLabelValues(dataPoint.getLabels()); + final var quantiles = dataPoint.getQuantiles(); + final var observations = new ArrayList(quantiles.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"))); + } + + quantiles.forEach( + quantile -> + observations.add( + new Observation( + category, + name, + quantile.getValue(), + addLabelValues( + labelValues, + "quantile", + Double.toString(quantile.getQuantile()))))); + + return observations.stream(); + }); + } +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSuppliedValueCollector.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSuppliedValueCollector.java new file mode 100644 index 00000000000..1295930a1bb --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/AbstractPrometheusSuppliedValueCollector.java @@ -0,0 +1,96 @@ +/* + * 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.metrics.Observation; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.DoubleSupplier; +import java.util.stream.Stream; + +import io.prometheus.metrics.model.registry.Collector; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.DataPointSnapshot; + +/** + * Abstract base class for Prometheus supplied value collectors. A supplied value collector is one + * which actual value is kept outside the metric system, for example in an external library or to + * calculate the value only on demand when a metric scrape occurs. This class provides common + * functionality for Prometheus supplied value collectors. + */ +abstract class AbstractPrometheusSuppliedValueCollector extends CategorizedPrometheusCollector + implements LabelledSuppliedMetric { + /** The collector */ + protected final Collector collector; + + /** Map label values with the collector callback data */ + protected final Map, CallbackData> labelledCallbackData = new ConcurrentHashMap<>(); + + protected AbstractPrometheusSuppliedValueCollector( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + super(category, name); + this.collector = createCollector(help, labelNames); + } + + protected abstract Collector createCollector(final String help, final String... labelNames); + + @Override + public void labels(final DoubleSupplier valueSupplier, final String... labelValues) { + final var valueList = List.of(labelValues); + if (labelledCallbackData.putIfAbsent(valueList, new CallbackData(valueSupplier, labelValues)) + != null) { + throw new IllegalArgumentException( + String.format("A collector has already been created for label values %s", valueList)); + } + } + + @Override + public String getIdentifier() { + return collector.getPrometheusName(); + } + + @Override + public void register(final PrometheusRegistry registry) { + registry.register(collector); + } + + @Override + public void unregister(final PrometheusRegistry registry) { + registry.unregister(collector); + } + + @Override + public Stream streamObservations() { + final var snapshot = collector.collect(); + return snapshot.getDataPoints().stream().map(this::convertToObservation); + } + + /** + * Convert the collected sample to an observation + * + * @param sample the collected sample + * @return an observation + */ + protected abstract Observation convertToObservation(final DataPointSnapshot sample); + + protected record CallbackData(DoubleSupplier valueSupplier, String[] labelValues) {} +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CategorizedPrometheusCollector.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CategorizedPrometheusCollector.java new file mode 100644 index 00000000000..753968966be --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CategorizedPrometheusCollector.java @@ -0,0 +1,50 @@ +/* + * 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.MetricCategory; + +/** A Prometheus collector that is assigned to a category */ +public abstract class CategorizedPrometheusCollector implements PrometheusCollector { + /** The {@link MetricCategory} this collector is assigned to */ + protected final MetricCategory category; + + /** The name of this collector */ + protected final String name; + + /** The prefixed name of this collector */ + protected final String prefixedName; + + /** + * Create a new collector assigned to the given category and with the given name, and computed the + * prefixed name. + * + * @param category The {@link MetricCategory} this collector is assigned to + * @param name The name of this collector + */ + protected CategorizedPrometheusCollector(final MetricCategory category, final String name) { + this.category = category; + this.name = name; + this.prefixedName = prefixedName(category, name); + } + + private static String categoryPrefix(final MetricCategory category) { + return category.getApplicationPrefix().orElse("") + category.getName() + "_"; + } + + private static String prefixedName(final MetricCategory category, final String name) { + return categoryPrefix(category) + name; + } +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CurrentValueCollector.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CurrentValueCollector.java deleted file mode 100644 index fdcc32bafdf..00000000000 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/CurrentValueCollector.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * 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 java.util.Collections.emptyList; -import static java.util.Collections.singletonList; - -import java.util.List; -import java.util.function.DoubleSupplier; - -import io.prometheus.client.Collector; -import io.prometheus.client.Collector.MetricFamilySamples.Sample; - -class CurrentValueCollector extends Collector { - - private final String metricName; - private final String help; - private final DoubleSupplier valueSupplier; - private final List labelNames; - private final List labelValues; - - public CurrentValueCollector( - final String metricName, final String help, final DoubleSupplier valueSupplier) { - this(metricName, help, emptyList(), emptyList(), valueSupplier); - } - - public CurrentValueCollector( - final String metricName, - final String help, - final List labelNames, - final List labelValues, - final DoubleSupplier valueSupplier) { - this.metricName = metricName; - this.help = help; - this.valueSupplier = valueSupplier; - this.labelNames = labelNames; - this.labelValues = labelValues; - } - - @Override - public List collect() { - final Sample sample = - new Sample(metricName, labelNames, labelValues, valueSupplier.getAsDouble()); - return singletonList( - new MetricFamilySamples(metricName, Type.GAUGE, help, singletonList(sample))); - } -} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpService.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpService.java index 94a198dc0e4..48045e4415b 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpService.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpService.java @@ -15,61 +15,46 @@ package org.hyperledger.besu.metrics.prometheus; import static com.google.common.base.Preconditions.checkArgument; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import org.hyperledger.besu.metrics.MetricsService; -import org.hyperledger.besu.plugin.services.MetricsSystem; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStreamWriter; import java.net.InetSocketAddress; -import java.net.SocketException; -import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; import java.util.concurrent.CompletableFuture; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.prometheus.client.exporter.common.TextFormat; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.http.HttpServerResponse; +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpPrincipal; +import io.prometheus.metrics.exporter.httpserver.DefaultHandler; +import io.prometheus.metrics.exporter.httpserver.HTTPServer; import io.vertx.core.net.HostAndPort; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** The Metrics http service. */ public class MetricsHttpService implements MetricsService { private static final Logger LOG = LoggerFactory.getLogger(MetricsHttpService.class); - + private static final Authenticator.Result AUTHORIZED = + new Authenticator.Success(new HttpPrincipal("metrics", "metrics")); + private static final Authenticator.Result NOT_AUTHORIZED = new Authenticator.Failure(403); private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0); - private final Vertx vertx; private final MetricsConfiguration config; - private final MetricsSystem metricsSystem; - - private HttpServer httpServer; + private final PrometheusMetricsSystem metricsSystem; + private HTTPServer httpServer; /** * Instantiates a new Metrics http service. * - * @param vertx the vertx * @param configuration the configuration * @param metricsSystem the metrics system */ public MetricsHttpService( - final Vertx vertx, - final MetricsConfiguration configuration, - final MetricsSystem metricsSystem) { + final MetricsConfiguration configuration, final PrometheusMetricsSystem metricsSystem) { validateConfig(configuration); - this.vertx = vertx; this.config = configuration; this.metricsSystem = metricsSystem; } @@ -85,78 +70,39 @@ private void validateConfig(final MetricsConfiguration config) { @Override public CompletableFuture start() { LOG.info("Starting metrics http service on {}:{}", config.getHost(), config.getPort()); - // Create the HTTP server and a router object. - httpServer = - vertx.createHttpServer( - new HttpServerOptions() - .setHost(config.getHost()) - .setPort(config.getPort()) - .setIdleTimeout(config.getIdleTimeout()) - .setHandle100ContinueAutomatically(true) - .setCompressionSupported(true)); - - final Router router = Router.router(vertx); - - // Verify Host header. - router.route().handler(checkAllowlistHostHeader()); - - // Endpoint for AWS health check. - router.route("/").method(HttpMethod.GET).handler(this::handleEmptyRequest); - // Endpoint for Prometheus metrics monitoring. - router.route("/metrics").method(HttpMethod.GET).handler(this::metricsRequest); + try { + httpServer = + HTTPServer.builder() + .hostname(config.getHost()) + .port(config.getPort()) + .registry(metricsSystem.getRegistry()) + .authenticator( + new Authenticator() { + @Override + public Result authenticate(final HttpExchange exch) { + return checkAllowlistHostHeader(exch); + } + }) + .defaultHandler(new RestrictedDefaultHandler()) + .buildAndStart(); - final CompletableFuture resultFuture = new CompletableFuture<>(); - httpServer - .requestHandler(router) - .listen( - res -> { - if (!res.failed()) { - resultFuture.complete(null); - final int actualPort = httpServer.actualPort(); - config.setActualPort(actualPort); - LOG.info( - "Metrics service started and listening on {}:{}", config.getHost(), actualPort); - return; - } - httpServer = null; - final Throwable cause = res.cause(); - if (cause instanceof SocketException) { - resultFuture.completeExceptionally( - new RuntimeException( - String.format( - "Failed to bind metrics listener to %s:%s (actual port %s): %s", - config.getHost(), - config.getPort(), - config.getActualPort(), - cause.getMessage()))); - return; - } - resultFuture.completeExceptionally(cause); - }); - return resultFuture; + return CompletableFuture.completedFuture(null); + } catch (final Throwable e) { + return CompletableFuture.failedFuture(e); + } } - private Handler checkAllowlistHostHeader() { - return event -> { - final Optional hostHeader = getAndValidateHostHeader(event); - if (config.getHostsAllowlist().contains("*") - || (hostHeader.isPresent() && hostIsInAllowlist(hostHeader.get()))) { - event.next(); - } else { - final HttpServerResponse response = event.response(); - if (!response.closed()) { - response - .setStatusCode(403) - .putHeader("Content-Type", "application/json; charset=utf-8") - .end("{\"message\":\"Host not authorized.\"}"); - } - } - }; - } + private Authenticator.Result checkAllowlistHostHeader(final HttpExchange exch) { + if (config.getHostsAllowlist().contains("*")) { + return AUTHORIZED; + } - private Optional getAndValidateHostHeader(final RoutingContext event) { - return Optional.ofNullable(event.request().authority()).map(HostAndPort::host); + return Optional.ofNullable(exch.getRequestHeaders().getFirst("Host")) + .map(host -> HostAndPort.parseAuthority(host, -1).host()) + .filter(this::hostIsInAllowlist) + .map(unused -> AUTHORIZED) + .orElse(NOT_AUTHORIZED); } private boolean hostIsInAllowlist(final String hostHeader) { @@ -179,56 +125,12 @@ public CompletableFuture stop() { return CompletableFuture.completedFuture(null); } - final CompletableFuture resultFuture = new CompletableFuture<>(); - httpServer.close( - res -> { - if (res.failed()) { - resultFuture.completeExceptionally(res.cause()); - } else { - httpServer = null; - resultFuture.complete(null); - } - }); - return resultFuture; - } - - private void metricsRequest(final RoutingContext routingContext) { - final Set names = new TreeSet<>(routingContext.queryParam("name[]")); - final HttpServerResponse response = routingContext.response(); - vertx.executeBlocking( - future -> { - try { - final ByteArrayOutputStream metrics = new ByteArrayOutputStream(16 * 1024); - final OutputStreamWriter osw = new OutputStreamWriter(metrics, StandardCharsets.UTF_8); - TextFormat.write004( - osw, - ((PrometheusMetricsSystem) (metricsSystem)) - .getRegistry() - .filteredMetricFamilySamples(names)); - osw.flush(); - osw.close(); - metrics.flush(); - metrics.close(); - future.complete(metrics.toString(StandardCharsets.UTF_8.name())); - } catch (final IOException ioe) { - future.fail(ioe); - } - }, - false, - (res) -> { - if (response.closed()) { - // Request for metrics closed before response was generated - return; - } - if (res.failed()) { - LOG.error("Request for metrics failed", res.cause()); - response.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end(); - } else { - response.setStatusCode(HttpResponseStatus.OK.code()); - response.putHeader("Content-Type", TextFormat.CONTENT_TYPE_004); - response.end(res.result()); - } - }); + try { + httpServer.stop(); + return CompletableFuture.completedFuture(null); + } catch (final Throwable e) { + return CompletableFuture.failedFuture(e); + } } /** @@ -240,7 +142,7 @@ InetSocketAddress socketAddress() { if (httpServer == null) { return EMPTY_SOCKET_ADDRESS; } - return new InetSocketAddress(config.getHost(), httpServer.actualPort()); + return new InetSocketAddress(config.getHost(), httpServer.getPort()); } @Override @@ -248,11 +150,21 @@ public Optional getPort() { if (httpServer == null) { return Optional.empty(); } - return Optional.of(httpServer.actualPort()); + return Optional.of(httpServer.getPort()); } - // Facilitate remote health-checks in AWS, inter alia. - private void handleEmptyRequest(final RoutingContext routingContext) { - routingContext.response().setStatusCode(201).end(); + private static class RestrictedDefaultHandler extends DefaultHandler { + @Override + public void handle(final HttpExchange exchange) throws IOException { + if (!exchange.getRequestURI().getPath().equals("/")) { + try { + exchange.sendResponseHeaders(HTTP_NOT_FOUND, -1); + } finally { + exchange.close(); + } + } else { + super.handle(exchange); + } + } } } diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsPushGatewayService.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsPushGatewayService.java index fa9cf53e809..ad60a2fd203 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsPushGatewayService.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsPushGatewayService.java @@ -26,7 +26,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import io.prometheus.client.exporter.PushGateway; +import io.prometheus.metrics.exporter.pushgateway.PushGateway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,7 +71,12 @@ public CompletableFuture start() { config.getPushHost(), config.getPushPort()); - pushGateway = new PushGateway(config.getPushHost() + ":" + config.getPushPort()); + pushGateway = + PushGateway.builder() + .registry(((PrometheusMetricsSystem) metricsSystem).getRegistry()) + .address(config.getPushHost() + ":" + config.getPushPort()) + .job(config.getPrometheusJob()) + .build(); // Create the executor scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); @@ -91,7 +96,7 @@ public CompletableFuture stop() { scheduledExecutorService.shutdownNow(); scheduledExecutorService.awaitTermination(30, TimeUnit.SECONDS); try { - pushGateway.delete(config.getPrometheusJob()); + pushGateway.delete(); } catch (final Exception e) { LOG.error("Could not clean up results on the Prometheus Push Gateway.", e); // Do not complete exceptionally, the gateway may be down and failures @@ -112,8 +117,7 @@ public Optional getPort() { private void pushMetrics() { try { - pushGateway.pushAdd( - ((PrometheusMetricsSystem) metricsSystem).getRegistry(), config.getPrometheusJob()); + pushGateway.pushAdd(); } catch (final IOException e) { LOG.warn("Could not push metrics", e); } diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCollector.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCollector.java new file mode 100644 index 00000000000..1e2da8ff00b --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCollector.java @@ -0,0 +1,82 @@ +/* + * 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.metrics.Observation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.Label; +import io.prometheus.metrics.model.snapshots.Labels; + +/** Wraps a native Prometheus collector inside the metric system */ +public interface PrometheusCollector { + + /** + * Get the identifier of the collector + * + * @return the identifier of the collector + */ + String getIdentifier(); + + /** + * Register this collector to the specified registry + * + * @param registry the registry + */ + void register(final PrometheusRegistry registry); + + /** + * Unregister this collector from the specified registry + * + * @param registry the registry + */ + void unregister(final PrometheusRegistry registry); + + /** + * Stream the data points of this collector + * + * @return a stream of the data points of this collector + */ + Stream streamObservations(); + + /** + * Utility to get the label values as strings from native Prometheus labels + * + * @param labels the Prometheus labels + * @return the label values as strings + */ + static List getLabelValues(final Labels labels) { + return labels.stream().map(Label::getValue).toList(); + } + + /** + * Add new values to an existing list of label values + * + * @param labelValues existing list of label values + * @param values the values to add + * @return a new list with new values appended to the original list + */ + static List addLabelValues(final List labelValues, final String... values) { + final var newList = new ArrayList(labelValues.size() + values.length); + newList.addAll(labelValues); + Collections.addAll(newList, values); + return newList; + } +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCounter.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCounter.java index 85b3750b778..b712c29a732 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCounter.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusCounter.java @@ -14,34 +14,88 @@ */ package org.hyperledger.besu.metrics.prometheus; +import static org.hyperledger.besu.metrics.prometheus.PrometheusCollector.getLabelValues; + +import org.hyperledger.besu.metrics.Observation; import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +import java.util.stream.Stream; -class PrometheusCounter implements LabelledMetric { +import io.prometheus.metrics.core.datapoints.CounterDataPoint; +import io.prometheus.metrics.model.registry.PrometheusRegistry; - private final io.prometheus.client.Counter counter; +/** + * A Prometheus counter implementation for Besu metrics. This class provides a Prometheus counter + * where the actual value is kept internally by the collector and methods are provided to increase + * the value when needed. + */ +class PrometheusCounter extends CategorizedPrometheusCollector implements LabelledMetric { + private final io.prometheus.metrics.core.metrics.Counter counter; - public PrometheusCounter(final io.prometheus.client.Counter counter) { - this.counter = counter; + public PrometheusCounter( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + super(category, name); + this.counter = + io.prometheus.metrics.core.metrics.Counter.builder() + .name(this.prefixedName) + .help(help) + .labelNames(labelNames) + .build(); } @Override public Counter labels(final String... labels) { - return new UnlabelledCounter(counter.labels(labels)); + return new UnlabelledCounter(counter.labelValues(labels)); } - private static class UnlabelledCounter implements Counter { - private final io.prometheus.client.Counter.Child counter; + @Override + public String getIdentifier() { + return counter.getPrometheusName(); + } - private UnlabelledCounter(final io.prometheus.client.Counter.Child counter) { - this.counter = counter; - } + @Override + public void register(final PrometheusRegistry registry) { + registry.register(counter); + } + + @Override + public void unregister(final PrometheusRegistry registry) { + registry.unregister(counter); + } + + /** + * Streams the observations from the collected counter data points. + * + * @return A stream of observations + */ + @Override + public Stream streamObservations() { + return counter.collect().getDataPoints().stream() + .map( + sample -> + new Observation( + category, name, sample.getValue(), getLabelValues(sample.getLabels()))); + } + + /** A private record class representing an unlabelled counter. */ + private record UnlabelledCounter(CounterDataPoint counter) implements Counter { + /** Increments the counter by one. */ @Override public void inc() { counter.inc(); } + /** + * Increments the counter by the specified amount. + * + * @param amount The amount to increment the counter by + */ @Override public void inc(final long amount) { counter.inc((double) amount); diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusGuavaCache.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusGuavaCache.java new file mode 100644 index 00000000000..4ca101abe1d --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusGuavaCache.java @@ -0,0 +1,171 @@ +/* + * 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.getLabelValues; + +import org.hyperledger.besu.metrics.Observation; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.ToDoubleFunction; +import java.util.stream.Stream; + +import com.google.common.cache.Cache; +import io.prometheus.metrics.instrumentation.guava.CacheMetricsCollector; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +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.MetricSnapshot; +import io.vertx.core.impl.ConcurrentHashSet; + +/** + * A Prometheus Guava cache collector implementation for Besu metrics. This class provides a way to + * expose metrics from Guava caches, it behaves differently from other collectors, since instead of + * having one collector per cache, Prometheus provides only one collector for all caches, so we need + * a Context that wraps the Prometheus single collector and handles its registration, while here we + * keep the abstraction of one Prometheus collector for one Guava cache, and we also verify that + * there is no collector name clash. + */ +class PrometheusGuavaCache extends CategorizedPrometheusCollector { + /** Use to reduce the possibility of a name clash with other collectors */ + private static final String NAME_PREFIX = "__guavaCacheMetricsCollector__"; + + private final Cache cache; + private final Context context; + + public PrometheusGuavaCache( + final MetricCategory category, + final Context context, + final String name, + final Cache cache) { + super(category, name); + if (context.alreadyExists(name)) { + throw new IllegalStateException("Cache already registered: " + name); + } + this.cache = cache; + this.context = context; + } + + @Override + public String getIdentifier() { + return category.getName() + "." + NAME_PREFIX + "." + name; + } + + @Override + public void register(final PrometheusRegistry registry) { + context.registerCache(registry, name, cache); + } + + @Override + public void unregister(final PrometheusRegistry registry) { + context.unregisterCache(registry, name); + } + + @Override + public Stream streamObservations() { + return context.streamObservations(category, name); + } + + /** + * Since Prometheus provides only one collector for all Guava caches, we only need to register + * that collector once when the first Besu Guava cache collector is created, and unregister it + * when the last is unregistered, so we have this context to keep track of that and also manage + * the observations stream. + */ + static class Context { + private static final Map> + COLLECTOR_VALUE_EXTRACTORS = + Map.of( + "guava_cache_eviction", Context::counterValueExtractor, + "guava_cache_hit", Context::counterValueExtractor, + "guava_cache_miss", Context::counterValueExtractor, + "guava_cache_requests", Context::counterValueExtractor, + "guava_cache_size", Context::gaugeValueExtractor); + + private final CacheMetricsCollector cacheMetricsCollector = new CacheMetricsCollector(); + private final Set cacheNames = new ConcurrentHashSet<>(); + private final AtomicBoolean collectorRegistered = new AtomicBoolean(false); + + boolean alreadyExists(final String name) { + return cacheNames.contains(name); + } + + void registerCache( + final PrometheusRegistry registry, final String name, final Cache cache) { + cacheMetricsCollector.addCache(name, cache); + cacheNames.add(name); + if (collectorRegistered.compareAndSet(false, true)) { + registry.register(cacheMetricsCollector); + } + } + + void unregisterCache(final PrometheusRegistry registry, final String name) { + cacheMetricsCollector.removeCache(name); + cacheNames.remove(name); + if (cacheNames.isEmpty() && collectorRegistered.compareAndSet(true, false)) { + registry.unregister(cacheMetricsCollector); + } + } + + void clear() { + cacheNames.forEach(cacheMetricsCollector::removeCache); + cacheNames.clear(); + collectorRegistered.set(false); + } + + private Stream streamObservations( + final MetricCategory category, final String cacheName) { + return cacheMetricsCollector.collect().stream() + .flatMap(ms -> convertToObservations(category, cacheName, ms)); + } + + private static Stream convertToObservations( + final MetricCategory category, final String cacheName, final MetricSnapshot snapshot) { + final var prometheusName = snapshot.getMetadata().getPrometheusName(); + if (COLLECTOR_VALUE_EXTRACTORS.containsKey(prometheusName)) { + return snapshotToObservations(category, cacheName, prometheusName, snapshot); + } + return Stream.empty(); + } + + private static Stream snapshotToObservations( + final MetricCategory category, + final String cacheName, + final String prometheusName, + final MetricSnapshot snapshot) { + return snapshot.getDataPoints().stream() + .filter(gdps -> gdps.getLabels().get("cache").equals(cacheName)) + .map( + gdps -> + new Observation( + category, + prometheusName, + COLLECTOR_VALUE_EXTRACTORS.get(prometheusName).applyAsDouble(gdps), + getLabelValues(gdps.getLabels()))); + } + + private static double gaugeValueExtractor(final DataPointSnapshot snapshot) { + return ((GaugeSnapshot.GaugeDataPointSnapshot) snapshot).getValue(); + } + + private static double counterValueExtractor(final DataPointSnapshot snapshot) { + return ((CounterSnapshot.CounterDataPointSnapshot) snapshot).getValue(); + } + } +} 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 9263383b8f0..c4ff1f9cedd 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 @@ -14,59 +14,61 @@ */ package org.hyperledger.besu.metrics.prometheus; +import static java.util.Map.entry; + import org.hyperledger.besu.metrics.ObservableMetricsSystem; import org.hyperledger.besu.metrics.Observation; import org.hyperledger.besu.metrics.StandardMetricCategory; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; -import org.hyperledger.besu.plugin.services.metrics.ExternalSummary; +import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.DoubleSupplier; -import java.util.function.Supplier; import java.util.stream.Stream; import com.google.common.cache.Cache; import com.google.common.collect.ImmutableSet; -import io.prometheus.client.Collector; -import io.prometheus.client.Collector.MetricFamilySamples; -import io.prometheus.client.Collector.MetricFamilySamples.Sample; -import io.prometheus.client.CollectorRegistry; -import io.prometheus.client.Counter; -import io.prometheus.client.Histogram; -import io.prometheus.client.Summary; -import io.prometheus.client.guava.cache.CacheMetricsCollector; -import io.prometheus.client.hotspot.BufferPoolsExports; -import io.prometheus.client.hotspot.ClassLoadingExports; -import io.prometheus.client.hotspot.GarbageCollectorExports; -import io.prometheus.client.hotspot.MemoryPoolsExports; -import io.prometheus.client.hotspot.StandardExports; -import io.prometheus.client.hotspot.ThreadExports; +import io.prometheus.metrics.instrumentation.jvm.JvmBufferPoolMetrics; +import io.prometheus.metrics.instrumentation.jvm.JvmClassLoadingMetrics; +import io.prometheus.metrics.instrumentation.jvm.JvmCompilationMetrics; +import io.prometheus.metrics.instrumentation.jvm.JvmGarbageCollectorMetrics; +import io.prometheus.metrics.instrumentation.jvm.JvmMemoryMetrics; +import io.prometheus.metrics.instrumentation.jvm.JvmMemoryPoolAllocationMetrics; +import io.prometheus.metrics.instrumentation.jvm.JvmNativeMemoryMetrics; +import io.prometheus.metrics.instrumentation.jvm.JvmRuntimeInfoMetric; +import io.prometheus.metrics.instrumentation.jvm.JvmThreadsMetrics; +import io.prometheus.metrics.instrumentation.jvm.ProcessMetrics; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.vertx.core.impl.ConcurrentHashSet; /** The Prometheus metrics system. */ public class PrometheusMetricsSystem implements ObservableMetricsSystem { - private static final List EXTERNAL_SUMMARY_LABELS = List.of("quantile"); - - private final Map> collectors = new ConcurrentHashMap<>(); - private final CollectorRegistry registry = new CollectorRegistry(true); - private final Map> - cachedCounters = new ConcurrentHashMap<>(); - private final Map> cachedTimers = + private static final Map DEFAULT_SUMMARY_QUANTILES = + Map.ofEntries( + entry(0.2, 0.02), + entry(0.5, 0.05), + entry(0.8, 0.02), + entry(0.95, 0.005), + entry(0.99, 0.001), + entry(1.0, 0.0)); + + private final Map> collectors = + new ConcurrentHashMap<>(); + private final PrometheusRegistry registry = PrometheusRegistry.defaultRegistry; + private final Map> cachedCounters = new ConcurrentHashMap<>(); - private final Set totalSuffixedCounters = new ConcurrentHashSet<>(); - private final Map guavaCacheCollectors = + private final Map> cachedTimers = new ConcurrentHashMap<>(); - private final Set guavaCacheNames = new ConcurrentHashSet<>(); + private final PrometheusGuavaCache.Context guavaCacheCollectorContext = + new PrometheusGuavaCache.Context(); private final Set enabledCategories; private final boolean timersEnabled; @@ -85,15 +87,19 @@ public PrometheusMetricsSystem( /** Init. */ public void init() { - if (isCategoryEnabled(StandardMetricCategory.PROCESS)) { - registerCollector(StandardMetricCategory.PROCESS, new StandardExports()); - } if (isCategoryEnabled(StandardMetricCategory.JVM)) { - registerCollector(StandardMetricCategory.JVM, new MemoryPoolsExports()); - registerCollector(StandardMetricCategory.JVM, new BufferPoolsExports()); - registerCollector(StandardMetricCategory.JVM, new GarbageCollectorExports()); - registerCollector(StandardMetricCategory.JVM, new ThreadExports()); - registerCollector(StandardMetricCategory.JVM, new ClassLoadingExports()); + JvmThreadsMetrics.builder().register(registry); + JvmBufferPoolMetrics.builder().register(registry); + JvmClassLoadingMetrics.builder().register(registry); + JvmCompilationMetrics.builder().register(registry); + JvmGarbageCollectorMetrics.builder().register(registry); + JvmMemoryMetrics.builder().register(registry); + JvmMemoryPoolAllocationMetrics.builder().register(registry); + JvmNativeMemoryMetrics.builder().register(registry); + JvmRuntimeInfoMetric.builder().register(registry); + } + if (isCategoryEnabled(StandardMetricCategory.PROCESS)) { + ProcessMetrics.builder().register(registry); } } @@ -103,22 +109,20 @@ public Set getEnabledCategories() { } @Override - public LabelledMetric createLabelledCounter( + public LabelledMetric createLabelledCounter( final MetricCategory category, final String name, final String help, final String... labelNames) { - final String metricName = convertToPrometheusCounterName(category, name); return cachedCounters.computeIfAbsent( - metricName, - (k) -> { + CachedMetricKey.of(category, name), + k -> { if (isCategoryEnabled(category)) { - final Counter counter = Counter.build(metricName, help).labelNames(labelNames).create(); + final var counter = new PrometheusCounter(category, name, help, labelNames); registerCollector(category, counter); - return new PrometheusCounter(counter); - } else { - return NoOpMetricsSystem.getCounterLabelledMetric(labelNames.length); + return counter; } + return NoOpMetricsSystem.getCounterLabelledMetric(labelNames.length); }); } @@ -128,26 +132,16 @@ public LabelledMetric createLabelledTimer( final String name, final String help, final String... labelNames) { - final String metricName = convertToPrometheusName(category, name); return cachedTimers.computeIfAbsent( - metricName, - (k) -> { + CachedMetricKey.of(category, name), + k -> { if (timersEnabled && isCategoryEnabled(category)) { - final Summary summary = - Summary.build(metricName, help) - .quantile(0.2, 0.02) - .quantile(0.5, 0.05) - .quantile(0.8, 0.02) - .quantile(0.95, 0.005) - .quantile(0.99, 0.001) - .quantile(1.0, 0) - .labelNames(labelNames) - .create(); + final var summary = + new PrometheusTimer(category, name, help, DEFAULT_SUMMARY_QUANTILES, labelNames); registerCollector(category, summary); - return new PrometheusTimer(summary); - } else { - return NoOpMetricsSystem.getOperationTimerLabelledMetric(labelNames.length); + return summary; } + return NoOpMetricsSystem.getOperationTimerLabelledMetric(labelNames.length); }); } @@ -157,85 +151,41 @@ public LabelledMetric createSimpleLabelledTimer( final String name, final String help, final String... labelNames) { - final String metricName = convertToPrometheusName(category, name); return cachedTimers.computeIfAbsent( - metricName, - (k) -> { + CachedMetricKey.of(category, name), + k -> { if (timersEnabled && isCategoryEnabled(category)) { - final Histogram histogram = - Histogram.build(metricName, help).labelNames(labelNames).buckets(1D).create(); + final var histogram = + new PrometheusSimpleTimer(category, name, help, new double[] {1D}, labelNames); registerCollector(category, histogram); - return new PrometheusSimpleTimer(histogram); - } else { - return NoOpMetricsSystem.getOperationTimerLabelledMetric(labelNames.length); + return histogram; } + return NoOpMetricsSystem.getOperationTimerLabelledMetric(labelNames.length); }); } @Override - public void createGauge( + public LabelledSuppliedSummary createLabelledSuppliedSummary( final MetricCategory category, final String name, final String help, - final DoubleSupplier valueSupplier) { - final String metricName = convertToPrometheusName(category, name); + final String... labelNames) { if (isCategoryEnabled(category)) { - final Collector collector = new CurrentValueCollector(metricName, help, valueSupplier); - registerCollector(category, collector); - } - } - - @Override - public void trackExternalSummary( - final MetricCategory category, - final String name, - final String help, - final Supplier summarySupplier) { - if (isCategoryEnabled(category)) { - final var externalSummaryCollector = - new Collector() { - @Override - public List collect() { - final var externalSummary = summarySupplier.get(); - - final var quantileValues = - externalSummary.quantiles().stream() - .map( - quantile -> - new Sample( - name, - EXTERNAL_SUMMARY_LABELS, - List.of(Double.toString(quantile.quantile())), - quantile.value())) - .toList(); - - return List.of( - new MetricFamilySamples( - name, Type.SUMMARY, "RocksDB histogram for " + name, quantileValues)); - } - }; - - registerCollector(category, externalSummaryCollector); + final PrometheusSuppliedSummary summary = + new PrometheusSuppliedSummary(category, name, help, labelNames); + registerCollector(category, summary); + return summary; } + return NoOpMetricsSystem.getLabelledSuppliedSummary(labelNames.length); } @Override public void createGuavaCacheCollector( final MetricCategory category, final String name, final Cache cache) { if (isCategoryEnabled(category)) { - if (guavaCacheNames.contains(name)) { - throw new IllegalStateException("Cache already registered: " + name); - } - guavaCacheNames.add(name); - final var guavaCacheCollector = - guavaCacheCollectors.computeIfAbsent( - category, - unused -> { - final var cmc = new CacheMetricsCollector(); - registerCollector(category, cmc); - return cmc; - }); - guavaCacheCollector.addCache(name, cache); + final var cacheCollector = + new PrometheusGuavaCache(category, guavaCacheCollectorContext, name, cache); + registerCollector(category, cacheCollector); } } @@ -245,7 +195,13 @@ public LabelledSuppliedMetric createLabelledSuppliedCounter( final String name, final String help, final String... labelNames) { - return createLabelledSuppliedMetric(category, Collector.Type.COUNTER, name, help, labelNames); + if (isCategoryEnabled(category)) { + final PrometheusSuppliedCounter counter = + new PrometheusSuppliedCounter(category, name, help, labelNames); + registerCollector(category, counter); + return counter; + } + return NoOpMetricsSystem.getLabelledSuppliedMetric(labelNames.length); } @Override @@ -254,53 +210,38 @@ public LabelledSuppliedMetric createLabelledSuppliedGauge( final String name, final String help, final String... labelNames) { - return createLabelledSuppliedMetric(category, Collector.Type.GAUGE, name, help, labelNames); - } - - private LabelledSuppliedMetric createLabelledSuppliedMetric( - final MetricCategory category, - final Collector.Type type, - final String name, - final String help, - final String... labelNames) { - final String metricName = convertToPrometheusName(category, name); if (isCategoryEnabled(category)) { - final PrometheusSuppliedValueCollector suppliedValueCollector = - new PrometheusSuppliedValueCollector(type, metricName, help, List.of(labelNames)); - registerCollector(category, suppliedValueCollector); - return suppliedValueCollector; + final PrometheusSuppliedGauge gauge = + new PrometheusSuppliedGauge(category, name, help, labelNames); + registerCollector(category, gauge); + return gauge; } return NoOpMetricsSystem.getLabelledSuppliedMetric(labelNames.length); } - private void registerCollector(final MetricCategory category, final Collector collector) { - final Collection categoryCollectors = - this.collectors.computeIfAbsent( - category, key -> Collections.newSetFromMap(new ConcurrentHashMap<>())); - - final List newSamples = - collector.collect().stream().map(metricFamilySamples -> metricFamilySamples.name).toList(); + private void registerCollector( + final MetricCategory category, final PrometheusCollector collector) { + final Collection categoryCollectors = + this.collectors.computeIfAbsent(category, key -> new ConcurrentHashSet<>()); + // unregister if already present categoryCollectors.stream() - .filter( - c -> - c.collect().stream() - .anyMatch(metricFamilySamples -> newSamples.contains(metricFamilySamples.name))) + .filter(c -> c.getIdentifier().equals(collector.getIdentifier())) .findFirst() .ifPresent( c -> { categoryCollectors.remove(c); - registry.unregister(c); + c.unregister(registry); }); - categoryCollectors.add(collector.register(registry)); + collector.register(registry); + categoryCollectors.add(collector); } @Override public Stream streamObservations(final MetricCategory category) { return collectors.getOrDefault(category, Collections.emptySet()).stream() - .flatMap(collector -> collector.collect().stream()) - .flatMap(familySamples -> convertSamplesToObservations(category, familySamples)); + .flatMap(PrometheusCollector::streamObservations); } @Override @@ -308,137 +249,22 @@ public Stream streamObservations() { return collectors.keySet().stream().flatMap(this::streamObservations); } + PrometheusRegistry getRegistry() { + return registry; + } + @Override public void shutdown() { registry.clear(); collectors.clear(); cachedCounters.clear(); cachedTimers.clear(); - guavaCacheCollectors.clear(); - guavaCacheNames.clear(); - } - - private Stream convertSamplesToObservations( - final MetricCategory category, final MetricFamilySamples familySamples) { - return familySamples.samples.stream() - .map(sample -> createObservationFromSample(category, sample, familySamples)); + guavaCacheCollectorContext.clear(); } - private Observation createObservationFromSample( - final MetricCategory category, final Sample sample, final MetricFamilySamples familySamples) { - if (familySamples.type == Collector.Type.HISTOGRAM) { - return convertHistogramSampleNamesToLabels(category, sample, familySamples); - } - if (familySamples.type == Collector.Type.SUMMARY) { - return convertSummarySampleNamesToLabels(category, sample, familySamples); + private record CachedMetricKey(MetricCategory category, String name) { + static CachedMetricKey of(final MetricCategory category, final String name) { + return new CachedMetricKey(category, name); } - if (familySamples.type == Collector.Type.COUNTER) { - return convertCounterNamesToLabels(category, sample, familySamples); - } - return new Observation( - category, - convertFromPrometheusName(category, sample.name), - sample.value, - sample.labelValues); - } - - private Observation convertCounterNamesToLabels( - final MetricCategory category, final Sample sample, final MetricFamilySamples familySamples) { - final List labelValues = new ArrayList<>(sample.labelValues); - if (sample.name.endsWith("_created")) { - labelValues.add("created"); - } - - return new Observation( - category, - convertFromPrometheusCounterName(category, familySamples.name), - sample.value, - labelValues); - } - - private Observation convertHistogramSampleNamesToLabels( - final MetricCategory category, final Sample sample, final MetricFamilySamples familySamples) { - final List labelValues = new ArrayList<>(sample.labelValues); - if (sample.name.endsWith("_bucket")) { - labelValues.add(labelValues.size() - 1, "bucket"); - } else { - labelValues.add(sample.name.substring(sample.name.lastIndexOf("_") + 1)); - } - return new Observation( - category, - convertFromPrometheusName(category, familySamples.name), - sample.value, - labelValues); - } - - private Observation convertSummarySampleNamesToLabels( - final MetricCategory category, final Sample sample, final MetricFamilySamples familySamples) { - final List labelValues = new ArrayList<>(sample.labelValues); - if (sample.name.endsWith("_sum")) { - labelValues.add("sum"); - } else if (sample.name.endsWith("_count")) { - labelValues.add("count"); - } else if (sample.name.endsWith("_created")) { - labelValues.add("created"); - } else { - labelValues.add(labelValues.size() - 1, "quantile"); - } - return new Observation( - category, - convertFromPrometheusName(category, familySamples.name), - sample.value, - labelValues); - } - - /** - * Convert to prometheus name. - * - * @param category the category - * @param name the name - * @return the name as string - */ - public String convertToPrometheusName(final MetricCategory category, final String name) { - return prometheusPrefix(category) + name; - } - - /** - * Convert to prometheus counter name. Prometheus adds a _total suffix to the name if not present, - * so we remember if the original name already has it, to be able to convert back correctly - * - * @param category the category - * @param name the name - * @return the name as string - */ - public String convertToPrometheusCounterName(final MetricCategory category, final String name) { - if (name.endsWith("_total")) { - totalSuffixedCounters.add(name); - } - return convertToPrometheusName(category, name); - } - - private String convertFromPrometheusName(final MetricCategory category, final String metricName) { - final String prefix = prometheusPrefix(category); - return metricName.startsWith(prefix) ? metricName.substring(prefix.length()) : metricName; - } - - private String convertFromPrometheusCounterName( - final MetricCategory category, final String metricName) { - final String unPrefixedName = convertFromPrometheusName(category, metricName); - return totalSuffixedCounters.contains(unPrefixedName + "_total") - ? unPrefixedName + "_total" - : unPrefixedName; - } - - private String prometheusPrefix(final MetricCategory category) { - return category.getApplicationPrefix().orElse("") + category.getName() + "_"; - } - - /** - * Gets registry. - * - * @return the registry - */ - CollectorRegistry getRegistry() { - return registry; } } 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 24799cb33d2..1a2693b7216 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,22 +14,86 @@ */ 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 io.prometheus.client.Histogram; +import java.util.stream.Stream; + +import io.prometheus.metrics.core.metrics.Histogram; +import io.prometheus.metrics.model.registry.PrometheusRegistry; -class PrometheusSimpleTimer implements LabelledMetric { +/** + * 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 + implements LabelledMetric { private final Histogram histogram; - public PrometheusSimpleTimer(final Histogram histogram) { - this.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(); } @Override public OperationTimer labels(final String... labels) { - final Histogram.Child metric = histogram.labels(labels); - return () -> metric.startTimer()::observeDuration; + 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/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedCounter.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedCounter.java new file mode 100644 index 00000000000..59a91d3f1c1 --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedCounter.java @@ -0,0 +1,73 @@ +/* + * 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.getLabelValues; + +import org.hyperledger.besu.metrics.Observation; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +import java.util.List; + +import io.prometheus.metrics.core.metrics.CounterWithCallback; +import io.prometheus.metrics.model.registry.Collector; +import io.prometheus.metrics.model.snapshots.CounterSnapshot; +import io.prometheus.metrics.model.snapshots.DataPointSnapshot; + +/** + * A Prometheus supplied counter collector. A supplied counter collector is one which actual value + * is kept outside the metric system, for example in an external library or to calculate the value + * only on demand when a metric scrape occurs. + */ +class PrometheusSuppliedCounter extends AbstractPrometheusSuppliedValueCollector { + + public PrometheusSuppliedCounter( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + super(category, name, help, labelNames); + } + + @Override + protected Collector createCollector(final String help, final String... labelNames) { + return CounterWithCallback.builder() + .name(this.prefixedName) + .help(help) + .labelNames(labelNames) + .callback(this::callback) + .build(); + } + + private void callback(final CounterWithCallback.Callback callback) { + labelledCallbackData + .values() + .forEach( + callbackData -> + callback.call( + callbackData.valueSupplier().getAsDouble(), callbackData.labelValues())); + } + + @Override + protected Observation convertToObservation(final DataPointSnapshot sample) { + final List labelValues = getLabelValues(sample.getLabels()); + + return new Observation( + category, + name, + ((CounterSnapshot.CounterDataPointSnapshot) sample).getValue(), + labelValues); + } +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedGauge.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedGauge.java new file mode 100644 index 00000000000..1de04a5697d --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedGauge.java @@ -0,0 +1,73 @@ +/* + * 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.getLabelValues; + +import org.hyperledger.besu.metrics.Observation; +import org.hyperledger.besu.plugin.services.metrics.LabelledGauge; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +import java.util.List; + +import io.prometheus.metrics.core.metrics.GaugeWithCallback; +import io.prometheus.metrics.model.registry.Collector; +import io.prometheus.metrics.model.snapshots.DataPointSnapshot; +import io.prometheus.metrics.model.snapshots.GaugeSnapshot; + +/** + * A Prometheus supplied gauge collector. A supplied gauge collector is one which actual value is + * kept outside the metric system, for example in an external library or to calculate the value only + * on demand when a metric scrape occurs. + */ +@SuppressWarnings("removal") +class PrometheusSuppliedGauge extends AbstractPrometheusSuppliedValueCollector + implements LabelledGauge { + + public PrometheusSuppliedGauge( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + super(category, name, help, labelNames); + } + + @Override + protected Collector createCollector(final String help, final String... labelNames) { + return GaugeWithCallback.builder() + .name(this.prefixedName) + .help(help) + .labelNames(labelNames) + .callback(this::callback) + .build(); + } + + private void callback(final GaugeWithCallback.Callback callback) { + labelledCallbackData + .values() + .forEach( + callbackData -> + callback.call( + callbackData.valueSupplier().getAsDouble(), callbackData.labelValues())); + } + + @Override + protected Observation convertToObservation(final DataPointSnapshot sample) { + final List labelValues = getLabelValues(sample.getLabels()); + + return new Observation( + category, name, ((GaugeSnapshot.GaugeDataPointSnapshot) sample).getValue(), labelValues); + } +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedSummary.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedSummary.java new file mode 100644 index 00000000000..9ea975040c2 --- /dev/null +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedSummary.java @@ -0,0 +1,83 @@ +/* + * 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.ExternalSummary; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import io.prometheus.metrics.core.metrics.SummaryWithCallback; +import io.prometheus.metrics.model.snapshots.Quantile; +import io.prometheus.metrics.model.snapshots.Quantiles; + +/** + * A Prometheus supplied summary collector. A supplied summary collector is one which actual value + * is kept outside the metric system, for example in an external library or to calculate the value + * only on demand when a metric scrape occurs. + */ +class PrometheusSuppliedSummary extends AbstractPrometheusSummary + implements LabelledSuppliedSummary { + /** Map label values with the collector callback data */ + protected final Map, CallbackData> labelledCallbackData = new ConcurrentHashMap<>(); + + public PrometheusSuppliedSummary( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + super(category, name); + this.collector = + SummaryWithCallback.builder() + .name(name) + .help(help) + .labelNames(labelNames) + .callback(this::callback) + .build(); + } + + private void callback(final SummaryWithCallback.Callback callback) { + labelledCallbackData + .values() + .forEach( + callbackData -> { + final var externalSummary = callbackData.summarySupplier().get(); + final var quantilesBuilder = Quantiles.builder(); + externalSummary.quantiles().stream() + .map(pq -> new Quantile(pq.quantile(), pq.value())) + .forEach(quantilesBuilder::quantile); + callback.call( + externalSummary.count(), externalSummary.sum(), quantilesBuilder.build()); + }); + } + + @Override + public synchronized void labels( + final Supplier summarySupplier, final String... labelValues) { + final var valueList = List.of(labelValues); + if (labelledCallbackData.containsKey(valueList)) { + throw new IllegalArgumentException( + String.format("A collector has already been created for label values %s", valueList)); + } + + labelledCallbackData.put(valueList, new CallbackData(summarySupplier, labelValues)); + } + + protected record CallbackData(Supplier summarySupplier, String[] labelValues) {} +} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedValueCollector.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedValueCollector.java deleted file mode 100644 index 47394ffed4d..00000000000 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusSuppliedValueCollector.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.LabelledGauge; -import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.DoubleSupplier; - -import io.prometheus.client.Collector; - -/** The Prometheus supplied value collector. */ -@SuppressWarnings("removal") // remove when deprecated LabelledGauge is removed -public class PrometheusSuppliedValueCollector extends Collector - implements LabelledSuppliedMetric, LabelledGauge { - private final Type type; - private final String metricName; - private final String help; - private final List labelNames; - private final Map, DoubleSupplier> observationsMap = new ConcurrentHashMap<>(); - - /** - * Instantiates a new Prometheus supplied value collector. - * - * @param type the type of the collector - * @param metricName the metric name - * @param help the help - * @param labelNames the label names - */ - public PrometheusSuppliedValueCollector( - final Type type, final String metricName, final String help, final List labelNames) { - this.type = type; - this.metricName = metricName; - this.help = help; - this.labelNames = labelNames; - } - - @Override - public synchronized void labels(final DoubleSupplier valueSupplier, final String... labelValues) { - validateLabelsCardinality(labelValues); - if (observationsMap.putIfAbsent(List.of(labelValues), valueSupplier) != null) { - final String labelValuesString = String.join(",", labelValues); - throw new IllegalArgumentException( - String.format("A gauge has already been created for label values %s", labelValuesString)); - } - } - - @Override - public List collect() { - final List samples = new ArrayList<>(); - observationsMap.forEach( - (labels, valueSupplier) -> - samples.add( - new MetricFamilySamples.Sample( - metricName, labelNames, labels, valueSupplier.getAsDouble()))); - return List.of(new MetricFamilySamples(metricName, type, help, samples)); - } - - private void validateLabelsCardinality(final String... labelValues) { - if (labelValues.length != labelNames.size()) { - throw new IllegalArgumentException( - "Label values and label names must be the same cardinality"); - } - } -} diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusTimer.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusTimer.java index ad50ab7149b..ccbe9c1b699 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusTimer.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusTimer.java @@ -15,21 +15,37 @@ package org.hyperledger.besu.metrics.prometheus; 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 io.prometheus.client.Summary; +import java.util.Map; -class PrometheusTimer implements LabelledMetric { +import io.prometheus.metrics.core.datapoints.DistributionDataPoint; +import io.prometheus.metrics.core.metrics.Summary; - private final Summary summary; +/** + * An implementation of Besu timer backed by a Prometheus summary. The summary provides a total + * count of durations and a sum of all observed durations, it calculates configurable quantiles over + * a sliding time window. + */ +class PrometheusTimer extends AbstractPrometheusSummary implements LabelledMetric { - public PrometheusTimer(final Summary summary) { - this.summary = summary; + public PrometheusTimer( + final MetricCategory category, + final String name, + final String help, + final Map quantiles, + final String... labelNames) { + super(category, name); + final var summaryBuilder = + Summary.builder().name(this.prefixedName).help(help).labelNames(labelNames); + quantiles.forEach(summaryBuilder::quantile); + this.collector = summaryBuilder.build(); } @Override public OperationTimer labels(final String... labels) { - final Summary.Child metric = summary.labels(labels); + final DistributionDataPoint metric = ((Summary) collector).labelValues(labels); return () -> metric.startTimer()::observeDuration; } } 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 c10c62132c1..4dfcd2fff9c 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,9 +18,9 @@ import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; -import org.hyperledger.besu.plugin.services.metrics.ExternalSummary; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; @@ -30,7 +30,6 @@ import java.util.Map; import java.util.Set; import java.util.function.DoubleSupplier; -import java.util.function.Supplier; import java.util.stream.Stream; import com.google.common.cache.Cache; @@ -98,11 +97,13 @@ public LabelledMetric createSimpleLabelledTimer( } @Override - public void trackExternalSummary( + public LabelledSuppliedSummary createLabelledSuppliedSummary( final MetricCategory category, final String name, final String help, - final Supplier summarySupplier) {} + final String... labelNames) { + return NoOpMetricsSystem.getLabelledSuppliedSummary(labelNames.length); + } @Override public void createGauge( diff --git a/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java b/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java index 8c6dd8261d5..5324d8cf3f9 100644 --- a/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java +++ b/metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java @@ -50,9 +50,9 @@ public class OpenTelemetryMetricsSystemTest { private static final Comparator IGNORE_VALUES = - Comparator.comparing(observation -> observation.getCategory().getName()) - .thenComparing(Observation::getMetricName) - .thenComparing((o1, o2) -> o1.getLabels().equals(o2.getLabels()) ? 0 : 1); + Comparator.comparing(observation -> observation.category().getName()) + .thenComparing(Observation::metricName) + .thenComparing((o1, o2) -> o1.labels().equals(o2.labels()) ? 0 : 1); @BeforeEach public void resetGlobalOpenTelemetry() { @@ -266,7 +266,7 @@ public void shouldOnlyObserveEnabledMetrics() throws InterruptedException { final LabelledMetric counterN = localMetricSystem.createLabelledCounter( NETWORK, "ABC", "Not that kind of network", "show"); - assertThat(counterN).isSameAs(NoOpMetricsSystem.NO_OP_LABELLED_1_COUNTER); + assertThat(counterN).isInstanceOf(NoOpMetricsSystem.LabelCountingNoOpMetric.class); counterN.labels("show").inc(); assertThat(localMetricSystem.streamObservations()).isEmpty(); @@ -274,7 +274,7 @@ public void shouldOnlyObserveEnabledMetrics() throws InterruptedException { // do a category we are watching final LabelledMetric counterR = localMetricSystem.createLabelledCounter(RPC, "name", "Not useful", "method"); - assertThat(counterR).isNotSameAs(NoOpMetricsSystem.NO_OP_LABELLED_1_COUNTER); + assertThat(counterR).isNotInstanceOf(NoOpMetricsSystem.LabelCountingNoOpMetric.class); counterR.labels("op").inc(); assertThat(getObservation(localMetricSystem)) diff --git a/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpServiceTest.java b/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpServiceTest.java index 064dabfe331..b9bf700929a 100644 --- a/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpServiceTest.java +++ b/metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpServiceTest.java @@ -24,43 +24,50 @@ import java.util.Properties; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.prometheus.client.exporter.common.TextFormat; -import io.vertx.core.Vertx; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; public class MetricsHttpServiceTest { - private static final Vertx vertx = Vertx.vertx(); - - private static MetricsHttpService service; - private static OkHttpClient client; - private static String baseUrl; - - @BeforeAll - public static void initServerAndClient() { - service = createMetricsHttpService(); - service.start().join(); + private PrometheusMetricsSystem metricsSystem; + private MetricsHttpService service; + private OkHttpClient client; + private String baseUrl; + + private void initServerAndClient( + final MetricsConfiguration metricsConfiguration, final boolean start) { + metricsSystem = (PrometheusMetricsSystem) MetricsSystemFactory.create(metricsConfiguration); + service = createMetricsHttpService(metricsConfiguration, metricsSystem); + if (start) { + service.start().join(); + } - // Build an OkHttp client. client = new OkHttpClient(); baseUrl = urlForSocketAddress("http", service.socketAddress()); } - private static MetricsHttpService createMetricsHttpService(final MetricsConfiguration config) { - GlobalOpenTelemetry.resetForTest(); - return new MetricsHttpService(vertx, config, MetricsSystemFactory.create(config)); + private void initServerAndClient(final boolean start) { + initServerAndClient(createMetricsConfig(), start); + } + + private void initServerAndClient() { + initServerAndClient(createMetricsConfig(), true); } - private static MetricsHttpService createMetricsHttpService() { + @AfterEach + public void stopServer() { + metricsSystem.shutdown(); + service.stop(); + } + + private MetricsHttpService createMetricsHttpService( + final MetricsConfiguration config, final PrometheusMetricsSystem metricsSystem) { GlobalOpenTelemetry.resetForTest(); - final MetricsConfiguration metricsConfiguration = createMetricsConfig(); - return new MetricsHttpService( - vertx, metricsConfiguration, MetricsSystemFactory.create(metricsConfiguration)); + return new MetricsHttpService(config, metricsSystem); } private static MetricsConfiguration createMetricsConfig() { @@ -71,15 +78,9 @@ private static MetricsConfiguration.Builder createMetricsConfigBuilder() { return MetricsConfiguration.builder().enabled(true).port(0).hostsAllowlist(singletonList("*")); } - /** Tears down the HTTP server. */ - @AfterAll - public static void shutdownServer() { - service.stop().join(); - vertx.close(); - } - @Test public void invalidCallToStart() { + initServerAndClient(); service .start() .whenComplete( @@ -88,6 +89,7 @@ public void invalidCallToStart() { @Test public void http404() throws Exception { + initServerAndClient(); try (final Response resp = client.newCall(buildGetRequest("/foo")).execute()) { assertThat(resp.code()).isEqualTo(404); } @@ -95,13 +97,15 @@ public void http404() throws Exception { @Test public void handleEmptyRequest() throws Exception { + initServerAndClient(); try (final Response resp = client.newCall(buildGetRequest("")).execute()) { - assertThat(resp.code()).isEqualTo(201); + assertThat(resp.code()).isEqualTo(200); } } @Test public void getSocketAddressWhenActive() { + initServerAndClient(); final InetSocketAddress socketAddress = service.socketAddress(); assertThat("127.0.0.1").isEqualTo(socketAddress.getAddress().getHostAddress()); assertThat(socketAddress.getPort() > 0).isTrue(); @@ -109,7 +113,7 @@ public void getSocketAddressWhenActive() { @Test public void getSocketAddressWhenStoppedIsEmpty() { - final MetricsHttpService service = createMetricsHttpService(); + initServerAndClient(false); final InetSocketAddress socketAddress = service.socketAddress(); assertThat("0.0.0.0").isEqualTo(socketAddress.getAddress().getHostAddress()); @@ -119,9 +123,7 @@ public void getSocketAddressWhenStoppedIsEmpty() { @Test public void getSocketAddressWhenBindingToAllInterfaces() { - final MetricsConfiguration config = createMetricsConfigBuilder().host("0.0.0.0").build(); - final MetricsHttpService service = createMetricsHttpService(config); - service.start().join(); + initServerAndClient(createMetricsConfigBuilder().host("0.0.0.0").build(), true); try { final InetSocketAddress socketAddress = service.socketAddress(); @@ -134,6 +136,7 @@ public void getSocketAddressWhenBindingToAllInterfaces() { @Test public void metricsArePresent() throws Exception { + initServerAndClient(); final Request metricsRequest = new Request.Builder().url(baseUrl + "/metrics").build(); try (final Response resp = client.newCall(metricsRequest).execute()) { assertThat(resp.code()).isEqualTo(200); @@ -148,6 +151,7 @@ public void metricsArePresent() throws Exception { @Test public void metricsArePresentWhenFiltered() throws Exception { + initServerAndClient(); final Request metricsRequest = new Request.Builder().url(baseUrl + "/metrics?name[]=jvm_threads_deadlocked").build(); try (final Response resp = client.newCall(metricsRequest).execute()) { @@ -163,6 +167,7 @@ public void metricsArePresentWhenFiltered() throws Exception { @Test public void metricsAreAbsentWhenFiltered() throws Exception { + initServerAndClient(); final Request metricsRequest = new Request.Builder().url(baseUrl + "/metrics?name[]=does_not_exist").build(); try (final Response resp = client.newCall(metricsRequest).execute()) { @@ -179,6 +184,7 @@ public void metricsAreAbsentWhenFiltered() throws Exception { @Test // There is only one available representation so content negotiation should not be used public void acceptHeaderIgnored() throws Exception { + initServerAndClient(); final Request metricsRequest = new Request.Builder().addHeader("Accept", "text/xml").url(baseUrl + "/metrics").build(); try (final Response resp = client.newCall(metricsRequest).execute()) { @@ -189,7 +195,7 @@ public void acceptHeaderIgnored() throws Exception { // We should have JVM metrics already loaded, verify a simple key. assertThat(props).containsKey("jvm_threads_deadlocked"); - assertThat(resp.header("Content-Type")).contains(TextFormat.CONTENT_TYPE_004); + assertThat(resp.header("Content-Type")).contains(PrometheusTextFormatWriter.CONTENT_TYPE); } } 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 ba875ab79b1..9f1878f5998 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 @@ -20,6 +20,7 @@ import static java.util.function.Predicate.not; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hyperledger.besu.metrics.BesuMetricCategory.BLOCKCHAIN; import static org.hyperledger.besu.metrics.BesuMetricCategory.DEFAULT_METRIC_CATEGORIES; import static org.hyperledger.besu.metrics.BesuMetricCategory.NETWORK; import static org.hyperledger.besu.metrics.BesuMetricCategory.PEERS; @@ -38,33 +39,31 @@ import org.hyperledger.besu.plugin.services.metrics.OperationTimer; import java.util.Collections; -import java.util.Comparator; import java.util.List; +import java.util.concurrent.ExecutionException; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableSet; import io.opentelemetry.api.GlobalOpenTelemetry; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class PrometheusMetricsSystemTest { - private static final Comparator IGNORE_VALUES = - Comparator.comparing(observation -> observation.getCategory().getName()) - .thenComparing(Observation::getMetricName) - .thenComparing((o1, o2) -> o1.getLabels().equals(o2.getLabels()) ? 0 : 1); - private static final Comparator WITH_VALUES = - Comparator.comparing(observation -> observation.getCategory().getName()) - .thenComparing(Observation::getMetricName) - .thenComparing((o1, o2) -> o1.getLabels().equals(o2.getLabels()) ? 0 : 1) - .thenComparing((o1, o2) -> o1.getValue().equals(o2.getValue()) ? 0 : 1); + private PrometheusMetricsSystem metricsSystem; @BeforeEach - public void resetGlobalOpenTelemetry() { + public void setUp() { + metricsSystem = new PrometheusMetricsSystem(DEFAULT_METRIC_CATEGORIES, true); GlobalOpenTelemetry.resetForTest(); } - private final ObservableMetricsSystem metricsSystem = - new PrometheusMetricsSystem(DEFAULT_METRIC_CATEGORIES, true); + @AfterEach + public void tearDown() { + metricsSystem.shutdown(); + } @Test public void shouldCreateObservationFromCounter() { @@ -72,17 +71,11 @@ public void shouldCreateObservationFromCounter() { counter.inc(); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(this::compareCounters) - .containsExactlyInAnyOrder( - new Observation(PEERS, "connected", 1.0, emptyList()), - new Observation(PEERS, "connected", null, List.of("created"))); + .containsExactlyInAnyOrder(new Observation(PEERS, "connected", 1.0, emptyList())); counter.inc(); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(this::compareCounters) - .containsExactly( - new Observation(PEERS, "connected", 2.0, emptyList()), - new Observation(PEERS, "connected", null, List.of("created"))); + .containsExactly(new Observation(PEERS, "connected", 2.0, emptyList())); } @Test @@ -95,17 +88,11 @@ public void shouldHandleDuplicateCounterCreation() { counter1.labels().inc(); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(this::compareCounters) - .containsExactly( - new Observation(PEERS, "connected", 1.0, emptyList()), - new Observation(PEERS, "connected", null, List.of("created"))); + .containsExactly(new Observation(PEERS, "connected", 1.0, emptyList())); counter2.labels().inc(); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(this::compareCounters) - .containsExactly( - new Observation(PEERS, "connected", 2.0, emptyList()), - new Observation(PEERS, "connected", null, List.of("created"))); + .containsExactly(new Observation(PEERS, "connected", 2.0, emptyList())); } @Test @@ -119,12 +106,9 @@ public void shouldCreateSeparateObservationsForEachCounterLabelValue() { counter.labels("value1").inc(); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(this::compareCounters) .containsExactlyInAnyOrder( new Observation(PEERS, "connected_total", 2.0, singletonList("value1")), - new Observation(PEERS, "connected_total", 1.0, singletonList("value2")), - new Observation(PEERS, "connected_total", null, List.of("value1", "created")), - new Observation(PEERS, "connected_total", null, List.of("value2", "created"))); + new Observation(PEERS, "connected_total", 1.0, singletonList("value2"))); } @Test @@ -160,18 +144,12 @@ public void shouldIncrementCounterBySpecifiedAmount() { counter.inc(5); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(this::compareCounters) - .containsExactly( - new Observation(PEERS, "connected", 5.0, emptyList()), - new Observation(PEERS, "connected", null, List.of("created"))); + .containsExactly(new Observation(PEERS, "connected", 5.0, emptyList())); counter.inc(6); assertThat(metricsSystem.streamObservations()) .usingDefaultElementComparator() - .usingElementComparator(this::compareCounters) - .containsExactly( - new Observation(PEERS, "connected", 11.0, emptyList()), - new Observation(PEERS, "connected", null, List.of("created"))); + .containsExactly(new Observation(PEERS, "connected", 11.0, emptyList())); } @Test @@ -179,20 +157,18 @@ public void shouldCreateObservationsFromTimer() { final OperationTimer timer = metricsSystem.createTimer(RPC, "request", "Some help"); final OperationTimer.TimingContext context = timer.startTimer(); - context.stopTimer(); + final var expected = context.stopTimer(); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(IGNORE_VALUES) .containsExactlyInAnyOrder( - new Observation(RPC, "request", null, asList("quantile", "0.2")), - new Observation(RPC, "request", null, asList("quantile", "0.5")), - new Observation(RPC, "request", null, asList("quantile", "0.8")), - new Observation(RPC, "request", null, asList("quantile", "0.95")), - new Observation(RPC, "request", null, asList("quantile", "0.99")), - new Observation(RPC, "request", null, asList("quantile", "1.0")), - new Observation(RPC, "request", null, singletonList("sum")), - new Observation(RPC, "request", null, singletonList("count")), - new Observation(RPC, "request", null, singletonList("created"))); + new Observation(RPC, "request", expected, asList("quantile", "0.2")), + new Observation(RPC, "request", expected, asList("quantile", "0.5")), + new Observation(RPC, "request", expected, asList("quantile", "0.8")), + new Observation(RPC, "request", expected, asList("quantile", "0.95")), + new Observation(RPC, "request", expected, asList("quantile", "0.99")), + new Observation(RPC, "request", expected, asList("quantile", "1.0")), + new Observation(RPC, "request", expected, singletonList("sum")), + new Observation(RPC, "request", 1L, singletonList("count"))); } @Test @@ -209,21 +185,19 @@ public void shouldCreateObservationsFromTimerWithLabels() { final LabelledMetric timer = metricsSystem.createLabelledTimer(RPC, "request", "Some help", "methodName"); - //noinspection EmptyTryBlock - try (final OperationTimer.TimingContext ignored = timer.labels("method").startTimer()) {} + final OperationTimer.TimingContext context = timer.labels("method").startTimer(); + final double expected = context.stopTimer(); assertThat(metricsSystem.streamObservations()) - .usingElementComparator(IGNORE_VALUES) // We don't know how long it will actually take. .containsExactlyInAnyOrder( - new Observation(RPC, "request", null, asList("method", "quantile", "0.2")), - new Observation(RPC, "request", null, asList("method", "quantile", "0.5")), - new Observation(RPC, "request", null, asList("method", "quantile", "0.8")), - new Observation(RPC, "request", null, asList("method", "quantile", "0.95")), - new Observation(RPC, "request", null, asList("method", "quantile", "0.99")), - new Observation(RPC, "request", null, asList("method", "quantile", "1.0")), - new Observation(RPC, "request", null, asList("method", "sum")), - new Observation(RPC, "request", null, asList("method", "count")), - new Observation(RPC, "request", null, asList("method", "created"))); + new Observation(RPC, "request", expected, asList("method", "quantile", "0.2")), + new Observation(RPC, "request", expected, asList("method", "quantile", "0.5")), + new Observation(RPC, "request", expected, asList("method", "quantile", "0.8")), + new Observation(RPC, "request", expected, asList("method", "quantile", "0.95")), + new Observation(RPC, "request", expected, asList("method", "quantile", "0.99")), + new Observation(RPC, "request", expected, asList("method", "quantile", "1.0")), + new Observation(RPC, "request", expected, asList("method", "sum")), + new Observation(RPC, "request", 1L, asList("method", "count"))); } @Test @@ -270,7 +244,7 @@ public void shouldOnlyObserveEnabledMetrics() { // do a category we are not watching final LabelledMetric counterN = localMetricSystem.createLabelledCounter(NETWORK, "ABC", "Not that kind of network", "show"); - assertThat(counterN).isSameAs(NoOpMetricsSystem.NO_OP_LABELLED_1_COUNTER); + assertThat(counterN).isInstanceOf(NoOpMetricsSystem.LabelCountingNoOpMetric.class); counterN.labels("show").inc(); assertThat(localMetricSystem.streamObservations()).isEmpty(); @@ -278,7 +252,7 @@ public void shouldOnlyObserveEnabledMetrics() { // do a category we are watching final LabelledMetric counterR = localMetricSystem.createLabelledCounter(RPC, "name", "Not useful", "method"); - assertThat(counterR).isNotSameAs(NoOpMetricsSystem.NO_OP_LABELLED_1_COUNTER); + assertThat(counterR).isNotInstanceOf(NoOpMetricsSystem.LabelCountingNoOpMetric.class); counterR.labels("op").inc(); assertThat(localMetricSystem.streamObservations()) @@ -314,17 +288,28 @@ public void returnsNoOpMetricsWhenPushEnabled() { assertThat(localMetricSystem).isInstanceOf(PrometheusMetricsSystem.class); } + @Test + public void shouldCreateObservationFromGuavaCache() throws ExecutionException { + final Cache guavaCache = + CacheBuilder.newBuilder().maximumSize(1).recordStats().build(); + metricsSystem.createGuavaCacheCollector(BLOCKCHAIN, "test", guavaCache); + + guavaCache.put("a", "b"); + guavaCache.get("a", () -> "b"); + guavaCache.get("z", () -> "x"); + + assertThat(metricsSystem.streamObservations()) + .containsExactlyInAnyOrder( + new Observation(BLOCKCHAIN, "guava_cache_size", 1.0, List.of("test")), + new Observation(BLOCKCHAIN, "guava_cache_requests", 2.0, List.of("test")), + new Observation(BLOCKCHAIN, "guava_cache_hit", 1.0, List.of("test")), + new Observation(BLOCKCHAIN, "guava_cache_miss", 1.0, List.of("test")), + new Observation(BLOCKCHAIN, "guava_cache_eviction", 1.0, List.of("test"))); + } + private boolean isCreatedSample(final Observation obs) { // Simple client 0.10.0 add a _created sample to every counter, histogram and summary, that we // may want to ignore - return obs.getLabels().contains("created"); - } - - private int compareCounters(final Observation obs1, final Observation obs2) { - // for created samples ignore values - if (obs1.getLabels().contains("created") && obs2.getLabels().contains("created")) { - return IGNORE_VALUES.compare(obs1, obs2); - } - return WITH_VALUES.compare(obs1, obs2); + return obs.labels().contains("created"); } } diff --git a/metrics/rocksdb/src/main/java/org/hyperledger/besu/metrics/rocksdb/RocksDBStats.java b/metrics/rocksdb/src/main/java/org/hyperledger/besu/metrics/rocksdb/RocksDBStats.java index 3af876945dd..2f101d5fe7a 100644 --- a/metrics/rocksdb/src/main/java/org/hyperledger/besu/metrics/rocksdb/RocksDBStats.java +++ b/metrics/rocksdb/src/main/java/org/hyperledger/besu/metrics/rocksdb/RocksDBStats.java @@ -171,7 +171,7 @@ public static void registerRocksDBMetrics( for (final var histogramType : HISTOGRAM_TYPES) { - metricsSystem.trackExternalSummary( + metricsSystem.createSummary( KVSTORE_ROCKSDB_STATS, KVSTORE_ROCKSDB_STATS.getName() + "_" + histogramType.name().toLowerCase(Locale.ROOT), "RocksDB histogram for " + histogramType.name(), diff --git a/platform/build.gradle b/platform/build.gradle index 4e3703ddd4c..79c2c5c5651 100644 --- a/platform/build.gradle +++ b/platform/build.gradle @@ -30,7 +30,7 @@ dependencies { api platform('io.grpc:grpc-bom:1.68.0') api platform('io.netty:netty-bom:4.1.115.Final') api platform('io.opentelemetry:opentelemetry-bom:1.43.0') - api platform('io.prometheus:simpleclient_bom:0.16.0') + api platform('io.prometheus:prometheus-metrics-bom:1.3.4') api platform('io.vertx:vertx-stack-depchain:4.5.10') api platform('org.apache.logging.log4j:log4j-bom:2.24.1') api platform('org.assertj:assertj-bom:3.26.3') diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index d504767138d..1204d921d43 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 = 'PKvPlngg7BfdZ4Jinh0IUsyFOLvNaQU72VD4BHia/WM=' + knownHash = '0suP4G0+vTbIvbBfaH+pOpNTEDaf2Hq+byXDyHc2i2E=' } 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 f3d3f91d0a5..7b466f85663 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 @@ -19,6 +19,7 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledGauge; import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedSummary; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import org.hyperledger.besu.plugin.services.metrics.OperationTimer; @@ -184,7 +185,13 @@ LabelledMetric createSimpleLabelledTimer( * @param help A human readable description of the metric. * @param valueSupplier A supplier for the double value to be presented. */ - void createGauge(MetricCategory category, String name, String help, DoubleSupplier valueSupplier); + default void createGauge( + final MetricCategory category, + final String name, + final String help, + final DoubleSupplier valueSupplier) { + createLabelledSuppliedGauge(category, name, help).labels(valueSupplier); + } /** * Creates a gauge for displaying integer values. @@ -219,7 +226,21 @@ default void createLongGauge( } /** - * Track a summary that is computed externally to this metric system. Useful when existing + * 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 + * that summary via the configured metric system. A notable example are RocksDB statistics. + * + * @param category The {@link MetricCategory} this external summary is assigned to. + * @param name A name for the metric. + * @param help A human readable description of the metric. + * @param labelNames An array of labels to assign to the supplier summary. + * @return The created labelled supplied summary + */ + LabelledSuppliedSummary createLabelledSuppliedSummary( + MetricCategory category, String name, String help, String... labelNames); + + /** + * Create a summary that is computed externally to this metric system. Useful when existing * libraries calculate the summary data on their own, and we want to export that summary via the * configured metric system. A notable example are RocksDB statistics. * @@ -228,8 +249,13 @@ default void createLongGauge( * @param help A human readable description of the metric. * @param summarySupplier A supplier to retrieve the summary data when needed. */ - void trackExternalSummary( - MetricCategory category, String name, String help, Supplier summarySupplier); + default void createSummary( + final MetricCategory category, + final String name, + final String help, + final Supplier summarySupplier) { + createLabelledSuppliedSummary(category, name, help).labels(summarySupplier); + } /** * Collect metrics from Guava cache. diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/metrics/LabelledSuppliedSummary.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/metrics/LabelledSuppliedSummary.java new file mode 100644 index 00000000000..de4cf7f933c --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/metrics/LabelledSuppliedSummary.java @@ -0,0 +1,28 @@ +/* + * 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; + +import java.util.function.Supplier; + +/** The interface Labelled supplied summary. */ +public interface LabelledSuppliedSummary { + /** + * Labels. + * + * @param summarySupplier the summary supplier + * @param labelValues the label values + */ + void labels(final Supplier summarySupplier, final String... labelValues); +}