Add OTLP metrics support (#1492)

* Add OTLP metrics support

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Don't automatically push to grpc. Only allow if pushEnabled is set to true

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Code review

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Missed refactoring

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Add missing header

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

*  Don't make otel depend on the push enabled flag

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Expose JUL logs to log4j2

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Code review fixes and make sure not to start push gateway if not set to prometheus

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* spotless

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Fix unit test

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* code review feedback

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>

* Add changelog entry

Signed-off-by: Antoine Toulme <antoine@lunar-ocean.com>
pull/1559/head
Antoine Toulme 4 years ago committed by GitHub
parent 94aec764ff
commit 45fd9f861e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 4
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java
  3. 1
      besu/build.gradle
  4. 2
      besu/src/main/java/org/hyperledger/besu/Runner.java
  5. 9
      besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
  6. 15
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  7. 17
      besu/src/main/java/org/hyperledger/besu/cli/subcommands/blocks/BlocksSubCommand.java
  8. 3
      besu/src/test/resources/everything_config.toml
  9. 2
      build.gradle
  10. 4
      ethereum/core/src/test/java/org/hyperledger/besu/ethereum/chain/DefaultBlockchainTest.java
  11. 2
      ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommand.java
  12. 4
      ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/MetricsSystemModule.java
  13. 8
      gradle/versions.gradle
  14. 6
      metrics/core/build.gradle
  15. 24
      metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsProtocol.java
  16. 34
      metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsService.java
  17. 59
      metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsSystemFactory.java
  18. 19
      metrics/core/src/main/java/org/hyperledger/besu/metrics/ObservableMetricsSystem.java
  19. 7
      metrics/core/src/main/java/org/hyperledger/besu/metrics/noop/NoOpMetricsSystem.java
  20. 67
      metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/MetricsOtelGrpcPushService.java
  21. 65
      metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryCounter.java
  22. 290
      metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java
  23. 53
      metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryTimer.java
  24. 20
      metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsConfiguration.java
  25. 5
      metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpService.java
  26. 5
      metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/MetricsPushGatewayService.java
  27. 51
      metrics/core/src/main/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystem.java
  28. 7
      metrics/core/src/test-support/java/org/hyperledger/besu/metrics/StubMetricsSystem.java
  29. 253
      metrics/core/src/test/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetryMetricsSystemTest.java
  30. 6
      metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/MetricsHttpServiceTest.java
  31. 12
      metrics/core/src/test/java/org/hyperledger/besu/metrics/prometheus/PrometheusMetricsSystemTest.java
  32. 5
      metrics/rocksdb/src/main/java/org/hyperledger/besu/metrics/rocksdb/RocksDBStats.java

@ -12,6 +12,7 @@
* Updated the libraries for secp256k1 and AltBN series precompiles. These updates provide significant performance improvements to those areas. [\#1499](https://github.com/hyperledger/besu/pull/1499)
* Provide MegaGas/second measurements in the log when doing a full block import, such as the catch up phase of a fast sync. [\#1512](https://github.com/hyperledger/besu/pull/1512)
* Added new endpoints to get miner data, `eth_getMinerDataByBlockHash` and `eth_getMinerDataByBlockNumber`. [\#1538](https://github.com/hyperledger/besu/pull/1538)
* Added direct support for OpenTelemetry metrics [\#1492](https://github.com/hyperledger/besu/pull/1492)
### Bug Fixes

@ -34,8 +34,8 @@ import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.permissioning.PermissioningConfiguration;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStorageProvider;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStorageProviderBuilder;
import org.hyperledger.besu.metrics.MetricsSystemFactory;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem;
import org.hyperledger.besu.plugin.services.BesuConfiguration;
import org.hyperledger.besu.plugin.services.BesuEvents;
import org.hyperledger.besu.plugin.services.PicoCLIOptions;
@ -128,7 +128,7 @@ public class ThreadBesuNodeRunner implements BesuNodeRunner {
node, storageService, securityModuleService, commonPluginConfiguration));
final ObservableMetricsSystem metricsSystem =
PrometheusMetricsSystem.init(node.getMetricsConfiguration());
MetricsSystemFactory.create(node.getMetricsConfiguration());
final List<EnodeURL> bootnodes =
node.getConfiguration().getBootnodes().stream()
.map(EnodeURL::fromURI)

@ -70,6 +70,7 @@ dependencies {
runtimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl'
runtimeOnly 'org.apache.logging.log4j:log4j-jul'
runtimeOnly 'com.splunk.logging:splunk-library-javalogging'
runtimeOnly 'org.fusesource.jansi:jansi' // for color logging in windows

@ -25,7 +25,7 @@ import org.hyperledger.besu.ethereum.p2p.network.NetworkRunner;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.stratum.StratumServer;
import org.hyperledger.besu.ethstats.EthStatsService;
import org.hyperledger.besu.metrics.prometheus.MetricsService;
import org.hyperledger.besu.metrics.MetricsService;
import org.hyperledger.besu.nat.NatService;
import java.io.File;

@ -93,9 +93,9 @@ import org.hyperledger.besu.ethereum.transaction.TransactionSimulator;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.ethstats.EthStatsService;
import org.hyperledger.besu.ethstats.util.NetstatsUrl;
import org.hyperledger.besu.metrics.MetricsService;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.metrics.prometheus.MetricsService;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.nat.core.NatManager;
@ -603,10 +603,7 @@ public class RunnerBuilder {
createPrivateTransactionObserver(subscriptionManager, privacyParameters);
}
Optional<MetricsService> metricsService = Optional.empty();
if (metricsConfiguration.isEnabled() || metricsConfiguration.isPushEnabled()) {
metricsService = Optional.of(createMetricsService(vertx, metricsConfiguration));
}
Optional<MetricsService> metricsService = createMetricsService(vertx, metricsConfiguration);
final Optional<EthStatsService> ethStatsService;
if (!Strings.isNullOrEmpty(ethstatsUrl)) {
@ -884,7 +881,7 @@ public class RunnerBuilder {
return new WebSocketService(vertx, configuration, websocketRequestHandler);
}
private MetricsService createMetricsService(
private Optional<MetricsService> createMetricsService(
final Vertx vertx, final MetricsConfiguration configuration) {
return MetricsService.create(vertx, configuration, metricsSystem);
}

@ -28,6 +28,7 @@ import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.DEFAULT_JSON_RPC
import static org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration.DEFAULT_WEBSOCKET_PORT;
import static org.hyperledger.besu.ethereum.permissioning.QuorumPermissioningConfiguration.QIP714_DEFAULT_BLOCK;
import static org.hyperledger.besu.metrics.BesuMetricCategory.DEFAULT_METRIC_CATEGORIES;
import static org.hyperledger.besu.metrics.MetricsProtocol.PROMETHEUS;
import static org.hyperledger.besu.metrics.prometheus.MetricsConfiguration.DEFAULT_METRICS_PORT;
import static org.hyperledger.besu.metrics.prometheus.MetricsConfiguration.DEFAULT_METRICS_PUSH_PORT;
import static org.hyperledger.besu.nat.kubernetes.KubernetesNatManager.DEFAULT_BESU_SERVICE_NAME_FILTER;
@ -116,10 +117,11 @@ import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStorageProviderBui
import org.hyperledger.besu.ethereum.worldstate.PrunerConfiguration;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.metrics.MetricCategoryRegistryImpl;
import org.hyperledger.besu.metrics.MetricsProtocol;
import org.hyperledger.besu.metrics.MetricsSystemFactory;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.StandardMetricCategory;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem;
import org.hyperledger.besu.metrics.vertx.VertxMetricsAdapterFactory;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.plugin.services.BesuConfiguration;
@ -648,6 +650,13 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
description = "Set to start the metrics exporter (default: ${DEFAULT-VALUE})")
private final Boolean isMetricsEnabled = false;
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@Option(
names = {"--metrics-protocol"},
description =
"Metrics protocol, one of PROMETHEUS, OPENTELEMETRY or NONE. (default: ${DEFAULT-VALUE})")
private MetricsProtocol metricsProtocol = PROMETHEUS;
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@Option(
names = {"--metrics-host"},
@ -1022,7 +1031,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private BesuController besuController;
private BesuConfiguration pluginCommonConfiguration;
private final Supplier<ObservableMetricsSystem> metricsSystem =
Suppliers.memoize(() -> PrometheusMetricsSystem.init(metricsConfiguration()));
Suppliers.memoize(() -> MetricsSystemFactory.create(metricsConfiguration()));
private Vertx vertx;
private EnodeDnsConfiguration enodeDnsConfiguration;
@ -1147,6 +1156,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
commandLine.registerConverter(Bytes.class, Bytes::fromHexString);
commandLine.registerConverter(Level.class, Level::valueOf);
commandLine.registerConverter(SyncMode.class, SyncMode::fromString);
commandLine.registerConverter(MetricsProtocol.class, MetricsProtocol::fromString);
commandLine.registerConverter(UInt256.class, (arg) -> UInt256.valueOf(new BigInteger(arg)));
commandLine.registerConverter(Wei.class, (arg) -> Wei.of(Long.parseUnsignedLong(arg)));
commandLine.registerConverter(PositiveNumber.class, PositiveNumber::fromString);
@ -1734,6 +1744,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
.enabled(isMetricsEnabled)
.host(metricsHost)
.port(metricsPort)
.protocol(metricsProtocol)
.metricCategories(metricCategories)
.pushEnabled(isMetricsPushEnabled)
.pushHost(metricsPushHost)

@ -33,8 +33,8 @@ import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.Wei;
import org.hyperledger.besu.metrics.MetricsService;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.metrics.prometheus.MetricsService;
import java.io.File;
import java.io.FileNotFoundException;
@ -434,18 +434,13 @@ public class BlocksSubCommand implements Runnable {
}
private static Optional<MetricsService> initMetrics(final BlocksSubCommand parentCommand) {
Optional<MetricsService> metricsService = Optional.empty();
final MetricsConfiguration metricsConfiguration =
parentCommand.parentCommand.metricsConfiguration();
if (metricsConfiguration.isEnabled() || metricsConfiguration.isPushEnabled()) {
metricsService =
Optional.of(
MetricsService.create(
Vertx.vertx(),
metricsConfiguration,
parentCommand.parentCommand.getMetricsSystem()));
metricsService.ifPresent(MetricsService::start);
}
Optional<MetricsService> metricsService =
MetricsService.create(
Vertx.vertx(), metricsConfiguration, parentCommand.parentCommand.getMetricsSystem());
metricsService.ifPresent(MetricsService::start);
return metricsService;
}
}

@ -92,6 +92,7 @@ rpc-ws-authentication-jwt-public-key-file="none"
# Prometheus Metrics Endpoint
metrics-enabled=false
metrics-protocol="prometheus"
metrics-host="8.6.7.5"
metrics-port=309
metrics-category=["RPC"]
@ -164,4 +165,4 @@ Xethstats-contact="contact@mail.n"
# feature flags
Xsecp256k1-native-enabled=false
Xaltbn128-native-enabled=false
Xaltbn128-native-enabled=false

@ -469,6 +469,8 @@ applicationDefaultJvmArgs = [
// We shutdown log4j ourselves, as otherwise this shutdown hook runs before our own and whatever
// happens during shutdown is not logged.
'-Dlog4j.shutdownHookEnabled=false',
// Redirect java.util.logging loggers to use log4j2.
'-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager',
// Suppress Java JPMS warnings. Document the reason for each suppression.
// Bouncy Castle needs access to sun.security.provider, which is not open by default.
'--add-opens',

@ -28,9 +28,9 @@ import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.TransactionReceipt;
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStoragePrefixedKeyBlockchainStorage;
import org.hyperledger.besu.metrics.MetricsSystemFactory;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem;
import org.hyperledger.besu.plugin.services.storage.KeyValueStorage;
import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage;
@ -164,7 +164,7 @@ public class DefaultBlockchainTest {
final Blockchain blockchain =
DefaultBlockchain.create(
createStorage(kvStore),
PrometheusMetricsSystem.init(MetricsConfiguration.builder().enabled(true).build()),
MetricsSystemFactory.create(MetricsConfiguration.builder().enabled(true).build()),
0);
for (int i = 0; i < blocks.size(); i++) {

@ -185,7 +185,7 @@ public class EvmToolCommand implements Runnable {
: GenesisFileModule.createGenesisModule(genesisFile)
: GenesisFileModule.createGenesisModule(network))
.evmToolCommandOptionsModule(daggerOptions)
.metricsSystemModule(new PrometheusMetricsSystemModule())
.metricsSystemModule(new MetricsSystemModule())
.build();
final BlockHeader blockHeader =

@ -16,6 +16,8 @@
package org.hyperledger.besu.evmtool;
import org.hyperledger.besu.metrics.MetricsSystemFactory;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import dagger.Module;
@ -27,6 +29,6 @@ public class MetricsSystemModule {
@Provides
MetricsSystem getMetricsSystem() {
throw new RuntimeException("Abstract");
return MetricsSystemFactory.create(MetricsConfiguration.builder().build());
}
}

@ -45,12 +45,19 @@ dependencyManagement {
dependency 'info.picocli:picocli:4.5.0'
dependency 'io.grpc:grpc-netty:1.33.0'
dependency 'io.kubernetes:client-java:5.0.0'
dependency 'io.pkts:pkts-core:3.0.7'
dependency group: 'io.opentelemetry.instrumentation.auto', name: 'opentelemetry-javaagent', version: '0.8.0', classifier: 'all'
dependency 'io.opentelemetry:opentelemetry-api:0.9.1'
dependency 'io.opentelemetry:opentelemetry-sdk:0.9.1'
dependency 'io.opentelemetry:opentelemetry-exporters-otlp:0.9.1'
dependency 'io.opentelemetry:opentelemetry-extension-runtime-metrics:0.9.1'
dependency 'io.prometheus:simpleclient:0.9.0'
dependency 'io.prometheus:simpleclient_common:0.9.0'
dependency 'io.prometheus:simpleclient_hotspot:0.9.0'
@ -75,6 +82,7 @@ dependencyManagement {
dependency 'org.apache.logging.log4j:log4j-api:2.13.3'
dependency 'org.apache.logging.log4j:log4j-core:2.13.3'
dependency 'org.apache.logging.log4j:log4j-jul:2.13.3'
dependency 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.3'
dependency 'org.fusesource.jansi:jansi:1.8'

@ -38,6 +38,12 @@ dependencies {
implementation project(':plugin-api')
implementation 'com.google.guava:guava'
implementation 'io.grpc:grpc-netty'
implementation 'io.opentelemetry:opentelemetry-api'
implementation 'io.opentelemetry:opentelemetry-sdk'
implementation 'io.opentelemetry:opentelemetry-extension-runtime-metrics'
implementation 'io.opentelemetry:opentelemetry-exporters-otlp'
implementation 'io.prometheus:simpleclient'
implementation 'io.prometheus:simpleclient_common'
implementation 'io.prometheus:simpleclient_hotspot'

@ -11,19 +11,21 @@
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
*/
package org.hyperledger.besu.metrics;
package org.hyperledger.besu.evmtool;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem;
import org.hyperledger.besu.plugin.services.MetricsSystem;
public class PrometheusMetricsSystemModule extends MetricsSystemModule {
/** Enumeration of metrics protocols supported by Besu. */
public enum MetricsProtocol {
PROMETHEUS,
OPENTELEMETRY,
NONE;
@Override
public MetricsSystem getMetricsSystem() {
return PrometheusMetricsSystem.init(MetricsConfiguration.builder().build());
public static MetricsProtocol fromString(final String str) {
for (final MetricsProtocol mode : MetricsProtocol.values()) {
if (mode.name().equalsIgnoreCase(str)) {
return mode;
}
}
return null;
}
}

@ -12,8 +12,13 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.metrics.prometheus;
package org.hyperledger.besu.metrics;
import org.hyperledger.besu.metrics.opentelemetry.MetricsOtelGrpcPushService;
import org.hyperledger.besu.metrics.opentelemetry.OpenTelemetrySystem;
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.plugin.services.MetricsSystem;
import java.util.Optional;
@ -21,18 +26,33 @@ import java.util.concurrent.CompletableFuture;
import io.vertx.core.Vertx;
/**
* Service responsible for exposing metrics to the outside, either through a port and network
* interface or pushing.
*/
public interface MetricsService {
static MetricsService create(
static Optional<MetricsService> create(
final Vertx vertx,
final MetricsConfiguration configuration,
final MetricsSystem metricsSystem) {
if (configuration.isEnabled()) {
return new MetricsHttpService(vertx, configuration, metricsSystem);
} else if (configuration.isPushEnabled()) {
return new MetricsPushGatewayService(configuration, metricsSystem);
if (configuration.getProtocol() == MetricsProtocol.PROMETHEUS) {
if (configuration.isEnabled()) {
return Optional.of(new MetricsHttpService(vertx, configuration, metricsSystem));
} else if (configuration.isPushEnabled()) {
return Optional.of(new MetricsPushGatewayService(configuration, metricsSystem));
} else {
return Optional.empty();
}
} else if (configuration.getProtocol() == MetricsProtocol.OPENTELEMETRY) {
if (configuration.isEnabled()) {
return Optional.of(
new MetricsOtelGrpcPushService(configuration, (OpenTelemetrySystem) metricsSystem));
} else {
return Optional.empty();
}
} else {
throw new RuntimeException("No metrics service enabled.");
return Optional.empty();
}
}

@ -0,0 +1,59 @@
/*
* 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;
import static org.hyperledger.besu.metrics.MetricsProtocol.OPENTELEMETRY;
import static org.hyperledger.besu.metrics.MetricsProtocol.PROMETHEUS;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.opentelemetry.OpenTelemetrySystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem;
/** Creates a new metric system based on configuration. */
public class MetricsSystemFactory {
private MetricsSystemFactory() {}
/**
* Creates and starts a new metric system to observe the behavior of the client
*
* @param metricsConfiguration the configuration of the metric system
* @return a new metric system
*/
public static ObservableMetricsSystem create(final MetricsConfiguration metricsConfiguration) {
if (!metricsConfiguration.isEnabled() && !metricsConfiguration.isPushEnabled()) {
return new NoOpMetricsSystem();
}
if (PROMETHEUS.equals(metricsConfiguration.getProtocol())) {
final PrometheusMetricsSystem metricsSystem =
new PrometheusMetricsSystem(
metricsConfiguration.getMetricCategories(), metricsConfiguration.isTimersEnabled());
metricsSystem.init();
return metricsSystem;
} else if (OPENTELEMETRY.equals(metricsConfiguration.getProtocol())) {
final OpenTelemetrySystem metricsSystem =
new OpenTelemetrySystem(
metricsConfiguration.getMetricCategories(),
metricsConfiguration.isTimersEnabled(),
metricsConfiguration.getPrometheusJob());
metricsSystem.initDefaults();
return metricsSystem;
} else {
throw new IllegalArgumentException(
"Invalid metrics protocol " + metricsConfiguration.getProtocol());
}
}
}

@ -17,6 +17,7 @@ package org.hyperledger.besu.metrics;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import java.util.Set;
import java.util.stream.Stream;
public interface ObservableMetricsSystem extends MetricsSystem {
@ -24,4 +25,22 @@ public interface ObservableMetricsSystem extends MetricsSystem {
Stream<Observation> streamObservations(MetricCategory category);
Stream<Observation> streamObservations();
/**
* Provides an immutable view into the metric categories enabled for metric collection.
*
* @return the set of enabled metric categories.
*/
Set<MetricCategory> getEnabledCategories();
/**
* Checks if a particular category of metrics is enabled.
*
* @param category the category to check
* @return true if the category is enabled, false otherwise
*/
default boolean isCategoryEnabled(final MetricCategory category) {
return getEnabledCategories().stream()
.anyMatch(metricCategory -> metricCategory.getName().equals(category.getName()));
}
}

@ -21,6 +21,8 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import java.util.Collections;
import java.util.Set;
import java.util.function.DoubleSupplier;
import java.util.stream.Stream;
@ -98,6 +100,11 @@ public class NoOpMetricsSystem implements ObservableMetricsSystem {
return Stream.empty();
}
@Override
public Set<MetricCategory> getEnabledCategories() {
return Collections.emptySet();
}
public static class LabelCountingNoOpMetric<T> implements LabelledMetric<T> {
final int labelCount;

@ -0,0 +1,67 @@
/*
* 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.opentelemetry;
import org.hyperledger.besu.metrics.MetricsService;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import io.opentelemetry.exporters.otlp.OtlpGrpcMetricExporter;
import io.opentelemetry.sdk.metrics.export.IntervalMetricReader;
public class MetricsOtelGrpcPushService implements MetricsService {
private final MetricsConfiguration configuration;
private final OpenTelemetrySystem metricsSystem;
private IntervalMetricReader periodicReader;
public MetricsOtelGrpcPushService(
final MetricsConfiguration configuration, final OpenTelemetrySystem metricsSystem) {
this.configuration = configuration;
this.metricsSystem = metricsSystem;
}
@Override
public CompletableFuture<?> start() {
OtlpGrpcMetricExporter exporter = OtlpGrpcMetricExporter.getDefault();
IntervalMetricReader.Builder builder =
IntervalMetricReader.builder()
.setExportIntervalMillis(configuration.getPushInterval() * 1000L)
.readEnvironmentVariables()
.readSystemProperties()
.setMetricProducers(
Collections.singleton(metricsSystem.getMeterSdkProvider().getMetricProducer()))
.setMetricExporter(exporter);
this.periodicReader = builder.build();
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<?> stop() {
if (periodicReader != null) {
periodicReader.shutdown();
}
return CompletableFuture.completedFuture(null);
}
@Override
public Optional<Integer> getPort() {
return Optional.empty();
}
}

@ -0,0 +1,65 @@
/*
* 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.opentelemetry;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import java.util.ArrayList;
import java.util.List;
import io.opentelemetry.common.Labels;
import io.opentelemetry.metrics.LongCounter;
public class OpenTelemetryCounter implements LabelledMetric<Counter> {
private final LongCounter counter;
private final String[] labelNames;
public OpenTelemetryCounter(final LongCounter counter, final String... labelNames) {
this.counter = counter;
this.labelNames = labelNames;
}
@Override
public Counter labels(final String... labelValues) {
List<String> labelKeysAndValues = new ArrayList<>();
for (int i = 0; i < labelNames.length; i++) {
labelKeysAndValues.add(labelNames[i]);
labelKeysAndValues.add(labelValues[i]);
}
final Labels labels = Labels.of(labelKeysAndValues.toArray(new String[] {}));
LongCounter.BoundLongCounter boundLongCounter = counter.bind(labels);
return new OpenTelemetryCounter.UnlabelledCounter(boundLongCounter);
}
private static class UnlabelledCounter implements Counter {
private final LongCounter.BoundLongCounter counter;
private UnlabelledCounter(final LongCounter.BoundLongCounter counter) {
this.counter = counter;
}
@Override
public void inc() {
counter.add(1);
}
@Override
public void inc(final long amount) {
counter.add(amount);
}
}
}

@ -0,0 +1,290 @@
/*
* 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.opentelemetry;
import org.hyperledger.besu.metrics.BesuMetricCategory;
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.Counter;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
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.stream.Stream;
import com.google.common.collect.ImmutableSet;
import io.opentelemetry.common.Attributes;
import io.opentelemetry.common.Labels;
import io.opentelemetry.metrics.DoubleValueObserver;
import io.opentelemetry.metrics.DoubleValueRecorder;
import io.opentelemetry.metrics.LongCounter;
import io.opentelemetry.metrics.LongSumObserver;
import io.opentelemetry.metrics.LongUpDownSumObserver;
import io.opentelemetry.metrics.Meter;
import io.opentelemetry.sdk.metrics.MeterSdkProvider;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.resources.ResourceAttributes;
/** Metrics system relying on the native OpenTelemetry format. */
public class OpenTelemetrySystem implements ObservableMetricsSystem {
private static final String TYPE_LABEL_KEY = "type";
private static final String AREA_LABEL_KEY = "area";
private static final String POOL_LABEL_KEY = "pool";
private static final String USED = "used";
private static final String COMMITTED = "committed";
private static final String MAX = "max";
private static final String HEAP = "heap";
private static final String NON_HEAP = "non_heap";
private final Set<MetricCategory> enabledCategories;
private final boolean timersEnabled;
private final Map<String, LabelledMetric<Counter>> cachedCounters = new ConcurrentHashMap<>();
private final Map<String, LabelledMetric<OperationTimer>> cachedTimers =
new ConcurrentHashMap<>();
private final MeterSdkProvider meterSdkProvider;
public OpenTelemetrySystem(
final Set<MetricCategory> enabledCategories,
final boolean timersEnabled,
final String jobName) {
this.enabledCategories = ImmutableSet.copyOf(enabledCategories);
this.timersEnabled = timersEnabled;
Resource resource =
Resource.getDefault()
.merge(
Resource.create(
Attributes.newBuilder()
.setAttribute(ResourceAttributes.SERVICE_NAME, jobName)
.build()));
this.meterSdkProvider = MeterSdkProvider.builder().setResource(resource).build();
}
MeterSdkProvider getMeterSdkProvider() {
return meterSdkProvider;
}
@Override
public Stream<Observation> streamObservations(final MetricCategory category) {
return streamObservations().filter(metricData -> metricData.getCategory().equals(category));
}
@Override
public Stream<Observation> streamObservations() {
Collection<MetricData> metricsList = meterSdkProvider.getMetricProducer().collectAllMetrics();
return metricsList.stream().map(this::convertToObservations).flatMap(stream -> stream);
}
private Stream<Observation> convertToObservations(final MetricData metricData) {
List<Observation> observations = new ArrayList<>();
MetricCategory category =
categoryNameToMetricCategory(metricData.getInstrumentationLibraryInfo().getName());
for (MetricData.Point point : metricData.getPoints()) {
List<String> labels = new ArrayList<>();
point.getLabels().forEach((k, v) -> labels.add(v));
observations.add(
new Observation(
category, metricData.getName(), extractValue(metricData.getType(), point), labels));
}
return observations.stream();
}
private MetricCategory categoryNameToMetricCategory(final String name) {
Set<MetricCategory> categories =
ImmutableSet.<MetricCategory>builder()
.addAll(EnumSet.allOf(BesuMetricCategory.class))
.addAll(EnumSet.allOf(StandardMetricCategory.class))
.build();
for (MetricCategory category : categories) {
if (category.getName().equals(name)) {
return category;
}
}
throw new IllegalArgumentException("Invalid metric category: " + name);
}
private Object extractValue(final MetricData.Type type, final MetricData.Point point) {
switch (type) {
case NON_MONOTONIC_LONG:
case MONOTONIC_LONG:
return ((MetricData.LongPoint) point).getValue();
case NON_MONOTONIC_DOUBLE:
case MONOTONIC_DOUBLE:
return ((MetricData.DoublePoint) point).getValue();
case SUMMARY:
return ((MetricData.SummaryPoint) point).getPercentileValues();
default:
throw new UnsupportedOperationException("Unsupported type " + type);
}
}
@Override
public LabelledMetric<Counter> createLabelledCounter(
final MetricCategory category,
final String name,
final String help,
final String... labelNames) {
return cachedCounters.computeIfAbsent(
name,
(k) -> {
if (isCategoryEnabled(category)) {
final Meter meter = meterSdkProvider.get(category.getName());
final LongCounter counter = meter.longCounterBuilder(name).setDescription(help).build();
return new OpenTelemetryCounter(counter, labelNames);
} else {
return NoOpMetricsSystem.getCounterLabelledMetric(labelNames.length);
}
});
}
@Override
public LabelledMetric<OperationTimer> createLabelledTimer(
final MetricCategory category,
final String name,
final String help,
final String... labelNames) {
return cachedTimers.computeIfAbsent(
name,
(k) -> {
if (timersEnabled && isCategoryEnabled(category)) {
final Meter meter = meterSdkProvider.get(category.getName());
final DoubleValueRecorder recorder =
meter.doubleValueRecorderBuilder(name).setDescription(help).build();
return new OpenTelemetryTimer(recorder, labelNames);
} else {
return NoOpMetricsSystem.getOperationTimerLabelledMetric(labelNames.length);
}
});
}
@Override
public void createGauge(
final MetricCategory category,
final String name,
final String help,
final DoubleSupplier valueSupplier) {
if (isCategoryEnabled(category)) {
final Meter meter = meterSdkProvider.get(category.getName());
DoubleValueObserver observer =
meter.doubleValueObserverBuilder(name).setDescription(help).build();
observer.setCallback(result -> result.observe(valueSupplier.getAsDouble(), Labels.empty()));
}
}
@Override
public Set<MetricCategory> getEnabledCategories() {
return enabledCategories;
}
public void initDefaults() {
if (isCategoryEnabled(StandardMetricCategory.JVM)) {
collectGC();
}
}
private void collectGC() {
final List<GarbageCollectorMXBean> garbageCollectors =
ManagementFactory.getGarbageCollectorMXBeans();
final MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
final List<MemoryPoolMXBean> poolBeans = ManagementFactory.getMemoryPoolMXBeans();
final Meter meter = meterSdkProvider.get(StandardMetricCategory.JVM.getName());
final LongSumObserver gcMetric =
meter
.longSumObserverBuilder("jvm.gc.collection")
.setDescription("Time spent in a given JVM garbage collector in milliseconds.")
.setUnit("ms")
.build();
final List<Labels> labelSets = new ArrayList<>(garbageCollectors.size());
for (final GarbageCollectorMXBean gc : garbageCollectors) {
labelSets.add(Labels.of("gc", gc.getName()));
}
gcMetric.setCallback(
resultLongObserver -> {
for (int i = 0; i < garbageCollectors.size(); i++) {
resultLongObserver.observe(
garbageCollectors.get(i).getCollectionTime(), labelSets.get(i));
}
});
final LongUpDownSumObserver areaMetric =
meter
.longUpDownSumObserverBuilder("jvm.memory.area")
.setDescription("Bytes of a given JVM memory area.")
.setUnit("By")
.build();
final Labels usedHeap = Labels.of(TYPE_LABEL_KEY, USED, AREA_LABEL_KEY, HEAP);
final Labels usedNonHeap = Labels.of(TYPE_LABEL_KEY, USED, AREA_LABEL_KEY, NON_HEAP);
final Labels committedHeap = Labels.of(TYPE_LABEL_KEY, COMMITTED, AREA_LABEL_KEY, HEAP);
final Labels committedNonHeap = Labels.of(TYPE_LABEL_KEY, COMMITTED, AREA_LABEL_KEY, NON_HEAP);
// TODO: Decide if max is needed or not. May be derived with some approximation from max(used).
final Labels maxHeap = Labels.of(TYPE_LABEL_KEY, MAX, AREA_LABEL_KEY, HEAP);
final Labels maxNonHeap = Labels.of(TYPE_LABEL_KEY, MAX, AREA_LABEL_KEY, NON_HEAP);
areaMetric.setCallback(
resultLongObserver -> {
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
resultLongObserver.observe(heapUsage.getUsed(), usedHeap);
resultLongObserver.observe(nonHeapUsage.getUsed(), usedNonHeap);
resultLongObserver.observe(heapUsage.getUsed(), committedHeap);
resultLongObserver.observe(nonHeapUsage.getUsed(), committedNonHeap);
resultLongObserver.observe(heapUsage.getUsed(), maxHeap);
resultLongObserver.observe(nonHeapUsage.getUsed(), maxNonHeap);
});
final LongUpDownSumObserver poolMetric =
meter
.longUpDownSumObserverBuilder("jvm.memory.pool")
.setDescription("Bytes of a given JVM memory pool.")
.setUnit("By")
.build();
final List<Labels> usedLabelSets = new ArrayList<>(poolBeans.size());
final List<Labels> committedLabelSets = new ArrayList<>(poolBeans.size());
final List<Labels> maxLabelSets = new ArrayList<>(poolBeans.size());
for (final MemoryPoolMXBean pool : poolBeans) {
usedLabelSets.add(Labels.of(TYPE_LABEL_KEY, USED, POOL_LABEL_KEY, pool.getName()));
committedLabelSets.add(Labels.of(TYPE_LABEL_KEY, COMMITTED, POOL_LABEL_KEY, pool.getName()));
maxLabelSets.add(Labels.of(TYPE_LABEL_KEY, MAX, POOL_LABEL_KEY, pool.getName()));
}
poolMetric.setCallback(
resultLongObserver -> {
for (int i = 0; i < poolBeans.size(); i++) {
MemoryUsage poolUsage = poolBeans.get(i).getUsage();
resultLongObserver.observe(poolUsage.getUsed(), usedLabelSets.get(i));
resultLongObserver.observe(poolUsage.getCommitted(), committedLabelSets.get(i));
// TODO: Decide if max is needed or not. May be derived with some approximation from
// max(used).
resultLongObserver.observe(poolUsage.getMax(), maxLabelSets.get(i));
}
});
}
}

@ -0,0 +1,53 @@
/*
* 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.opentelemetry;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import java.util.ArrayList;
import java.util.List;
import io.opentelemetry.common.Labels;
import io.opentelemetry.metrics.DoubleValueRecorder;
public class OpenTelemetryTimer implements LabelledMetric<OperationTimer> {
private final DoubleValueRecorder recorder;
private final String[] labelNames;
public OpenTelemetryTimer(final DoubleValueRecorder recorder, final String... labelNames) {
this.recorder = recorder;
this.labelNames = labelNames;
}
@Override
public OperationTimer labels(final String... labelValues) {
List<String> labelKeysAndValues = new ArrayList<>();
for (int i = 0; i < labelNames.length; i++) {
labelKeysAndValues.add(labelNames[i]);
labelKeysAndValues.add(labelValues[i]);
}
final Labels labels = Labels.of(labelKeysAndValues.toArray(new String[] {}));
return () -> {
final long startTime = System.nanoTime();
return () -> {
long elapsed = System.nanoTime() - startTime;
recorder.record(elapsed, labels);
return elapsed / 1e9;
};
};
}
}

@ -16,6 +16,7 @@ package org.hyperledger.besu.metrics.prometheus;
import static org.hyperledger.besu.metrics.BesuMetricCategory.DEFAULT_METRIC_CATEGORIES;
import org.hyperledger.besu.metrics.MetricsProtocol;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import java.util.Arrays;
@ -30,12 +31,13 @@ import com.google.common.base.MoreObjects;
public class MetricsConfiguration {
private static final String DEFAULT_METRICS_HOST = "127.0.0.1";
public static final int DEFAULT_METRICS_PORT = 9545;
private static final MetricsProtocol DEFAULT_METRICS_PROTOCOL = MetricsProtocol.PROMETHEUS;
private static final String DEFAULT_METRICS_PUSH_HOST = "127.0.0.1";
public static final int DEFAULT_METRICS_PUSH_PORT = 9001;
public static final Boolean DEFAULT_TIMERS_ENABLED = true;
private final boolean enabled;
private final MetricsProtocol protocol;
private final int port;
private int actualPort;
private final String host;
@ -55,6 +57,7 @@ public class MetricsConfiguration {
private MetricsConfiguration(
final boolean enabled,
final int port,
final MetricsProtocol protocol,
final String host,
final Set<MetricCategory> metricCategories,
final boolean pushEnabled,
@ -66,6 +69,7 @@ public class MetricsConfiguration {
final boolean timersEnabled) {
this.enabled = enabled;
this.port = port;
this.protocol = protocol;
this.host = host;
this.metricCategories = metricCategories;
this.pushEnabled = pushEnabled;
@ -81,6 +85,10 @@ public class MetricsConfiguration {
return enabled;
}
public MetricsProtocol getProtocol() {
return protocol;
}
public String getHost() {
return host;
}
@ -139,6 +147,7 @@ public class MetricsConfiguration {
public String toString() {
return MoreObjects.toStringHelper(this)
.add("enabled", enabled)
.add("protocol", protocol)
.add("port", port)
.add("host", host)
.add("metricCategories", metricCategories)
@ -161,6 +170,7 @@ public class MetricsConfiguration {
}
final MetricsConfiguration that = (MetricsConfiguration) o;
return enabled == that.enabled
&& Objects.equals(protocol, that.protocol)
&& port == that.port
&& pushEnabled == that.pushEnabled
&& pushPort == that.pushPort
@ -176,6 +186,7 @@ public class MetricsConfiguration {
public int hashCode() {
return Objects.hash(
enabled,
protocol,
port,
host,
metricCategories,
@ -189,6 +200,7 @@ public class MetricsConfiguration {
public static class Builder {
private boolean enabled = false;
private MetricsProtocol protocol = DEFAULT_METRICS_PROTOCOL;
private int port = DEFAULT_METRICS_PORT;
private String host = DEFAULT_METRICS_HOST;
private Set<MetricCategory> metricCategories = DEFAULT_METRIC_CATEGORIES;
@ -207,6 +219,11 @@ public class MetricsConfiguration {
return this;
}
public Builder protocol(final MetricsProtocol protocol) {
this.protocol = protocol;
return this;
}
public Builder port(final int port) {
this.port = port;
return this;
@ -268,6 +285,7 @@ public class MetricsConfiguration {
return new MetricsConfiguration(
enabled,
port,
protocol,
host,
metricCategories,
pushEnabled,

@ -17,6 +17,7 @@ package org.hyperledger.besu.metrics.prometheus;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Streams.stream;
import org.hyperledger.besu.metrics.MetricsService;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.io.ByteArrayOutputStream;
@ -45,7 +46,7 @@ import io.vertx.ext.web.RoutingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
class MetricsHttpService implements MetricsService {
public class MetricsHttpService implements MetricsService {
private static final Logger LOG = LogManager.getLogger();
private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0);
@ -56,7 +57,7 @@ class MetricsHttpService implements MetricsService {
private HttpServer httpServer;
MetricsHttpService(
public MetricsHttpService(
final Vertx vertx,
final MetricsConfiguration configuration,
final MetricsSystem metricsSystem) {

@ -16,6 +16,7 @@ package org.hyperledger.besu.metrics.prometheus;
import static com.google.common.base.Preconditions.checkArgument;
import org.hyperledger.besu.metrics.MetricsService;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.io.IOException;
@ -29,7 +30,7 @@ import io.prometheus.client.exporter.PushGateway;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
class MetricsPushGatewayService implements MetricsService {
public class MetricsPushGatewayService implements MetricsService {
private static final Logger LOG = LogManager.getLogger();
private PushGateway pushGateway;
@ -37,7 +38,7 @@ class MetricsPushGatewayService implements MetricsService {
private final MetricsConfiguration config;
private final MetricsSystem metricsSystem;
MetricsPushGatewayService(
public MetricsPushGatewayService(
final MetricsConfiguration configuration, final MetricsSystem metricsSystem) {
this.metricsSystem = metricsSystem;
validateConfig(configuration);

@ -14,9 +14,6 @@
*/
package org.hyperledger.besu.metrics.prometheus;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.Observation;
import org.hyperledger.besu.metrics.StandardMetricCategory;
@ -33,6 +30,7 @@ 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.collect.ImmutableSet;
@ -61,35 +59,24 @@ public class PrometheusMetricsSystem implements ObservableMetricsSystem {
private final Set<MetricCategory> enabledCategories;
private final boolean timersEnabled;
PrometheusMetricsSystem(
public PrometheusMetricsSystem(
final Set<MetricCategory> enabledCategories, final boolean timersEnabled) {
this.enabledCategories = ImmutableSet.copyOf(enabledCategories);
this.timersEnabled = timersEnabled;
}
public static ObservableMetricsSystem init(final MetricsConfiguration metricsConfiguration) {
if (!metricsConfiguration.isEnabled() && !metricsConfiguration.isPushEnabled()) {
return new NoOpMetricsSystem();
}
final PrometheusMetricsSystem metricsSystem =
new PrometheusMetricsSystem(
metricsConfiguration.getMetricCategories(), metricsConfiguration.isTimersEnabled());
if (metricsSystem.isCategoryEnabled(StandardMetricCategory.PROCESS)) {
metricsSystem.collectors.put(
StandardMetricCategory.PROCESS,
singleton(new StandardExports().register(metricsSystem.registry)));
}
if (metricsSystem.isCategoryEnabled(StandardMetricCategory.JVM)) {
metricsSystem.collectors.put(
StandardMetricCategory.JVM,
asList(
new MemoryPoolsExports().register(metricsSystem.registry),
new BufferPoolsExports().register(metricsSystem.registry),
new GarbageCollectorExports().register(metricsSystem.registry),
new ThreadExports().register(metricsSystem.registry),
new ClassLoadingExports().register(metricsSystem.registry)));
}
return metricsSystem;
public void init() {
addCollector(StandardMetricCategory.PROCESS, StandardExports::new);
addCollector(StandardMetricCategory.JVM, MemoryPoolsExports::new);
addCollector(StandardMetricCategory.JVM, BufferPoolsExports::new);
addCollector(StandardMetricCategory.JVM, GarbageCollectorExports::new);
addCollector(StandardMetricCategory.JVM, ThreadExports::new);
addCollector(StandardMetricCategory.JVM, ClassLoadingExports::new);
}
@Override
public Set<MetricCategory> getEnabledCategories() {
return enabledCategories;
}
@Override
@ -154,14 +141,10 @@ public class PrometheusMetricsSystem implements ObservableMetricsSystem {
}
}
private boolean isCategoryEnabled(final MetricCategory category) {
return enabledCategories.stream()
.anyMatch(metricCategory -> metricCategory.getName().equals(category.getName()));
}
public void addCollector(final MetricCategory category, final Collector metric) {
public void addCollector(
final MetricCategory category, final Supplier<Collector> metricSupplier) {
if (isCategoryEnabled(category)) {
addCollectorUnchecked(category, metric);
addCollectorUnchecked(category, metricSupplier.get());
}
}

@ -22,9 +22,11 @@ import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.MetricCategory;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.DoubleSupplier;
import java.util.stream.Stream;
@ -90,6 +92,11 @@ public class StubMetricsSystem implements ObservableMetricsSystem {
throw new UnsupportedOperationException("Observations aren't actually recorded");
}
@Override
public Set<MetricCategory> getEnabledCategories() {
return Collections.emptySet();
}
public static class StubLabelledCounter implements LabelledMetric<Counter> {
private final Map<List<String>, StubCounter> metrics = new HashMap<>();

@ -0,0 +1,253 @@
/*
* 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.opentelemetry;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
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;
import static org.hyperledger.besu.metrics.BesuMetricCategory.RPC;
import static org.hyperledger.besu.metrics.MetricsProtocol.OPENTELEMETRY;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.metrics.MetricsSystemFactory;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.Observation;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.google.common.collect.ImmutableSet;
import io.opentelemetry.sdk.metrics.data.MetricData;
import org.junit.Test;
public class OpenTelemetryMetricsSystemTest {
private static final Comparator<Observation> IGNORE_VALUES =
Comparator.<Observation, String>comparing(observation -> observation.getCategory().getName())
.thenComparing(Observation::getMetricName)
.thenComparing((o1, o2) -> o1.getLabels().equals(o2.getLabels()) ? 0 : 1);
private final ObservableMetricsSystem metricsSystem =
new OpenTelemetrySystem(DEFAULT_METRIC_CATEGORIES, true, "job");
@Test
public void shouldCreateObservationFromCounter() {
final Counter counter = metricsSystem.createCounter(PEERS, "connected", "Some help string");
counter.inc();
assertThat(metricsSystem.streamObservations())
.containsExactly(new Observation(PEERS, "connected", 1L, emptyList()));
counter.inc();
assertThat(metricsSystem.streamObservations())
.containsExactly(new Observation(PEERS, "connected", 2L, emptyList()));
}
@Test
public void shouldHandleDuplicateCounterCreation() {
final LabelledMetric<Counter> counter1 =
metricsSystem.createLabelledCounter(PEERS, "connected", "Some help string");
final LabelledMetric<Counter> counter2 =
metricsSystem.createLabelledCounter(PEERS, "connected", "Some help string");
assertThat(counter1).isEqualTo(counter2);
counter1.labels().inc();
assertThat(metricsSystem.streamObservations())
.containsExactly(new Observation(PEERS, "connected", 1L, emptyList()));
counter2.labels().inc();
assertThat(metricsSystem.streamObservations())
.containsExactly(new Observation(PEERS, "connected", 2L, emptyList()));
}
@Test
public void shouldCreateSeparateObservationsForEachCounterLabelValue() {
final LabelledMetric<Counter> counter =
metricsSystem.createLabelledCounter(PEERS, "connected", "Some help string", "labelName");
counter.labels("value1").inc();
counter.labels("value2").inc();
counter.labels("value1").inc();
assertThat(metricsSystem.streamObservations())
.containsExactlyInAnyOrder(
new Observation(PEERS, "connected", 2L, singletonList("value1")),
new Observation(PEERS, "connected", 1L, singletonList("value2")));
}
@Test
public void shouldIncrementCounterBySpecifiedAmount() {
final Counter counter = metricsSystem.createCounter(PEERS, "connected", "Some help string");
counter.inc(5);
assertThat(metricsSystem.streamObservations())
.containsExactly(new Observation(PEERS, "connected", 5L, emptyList()));
counter.inc(6);
assertThat(metricsSystem.streamObservations())
.containsExactly(new Observation(PEERS, "connected", 11L, emptyList()));
}
@Test
public void shouldCreateObservationsFromTimer() {
final OperationTimer timer = metricsSystem.createTimer(RPC, "request", "Some help");
final OperationTimer.TimingContext context = timer.startTimer();
context.stopTimer();
assertThat(metricsSystem.streamObservations())
.usingElementComparator(IGNORE_VALUES)
.containsExactlyInAnyOrder(new Observation(RPC, "request", null, Collections.emptyList()));
}
@Test
public void shouldHandleDuplicateTimerCreation() {
final LabelledMetric<OperationTimer> timer1 =
metricsSystem.createLabelledTimer(RPC, "request", "Some help");
final LabelledMetric<OperationTimer> timer2 =
metricsSystem.createLabelledTimer(RPC, "request", "Some help");
assertThat(timer1).isEqualTo(timer2);
}
@Test
public void shouldCreateObservationsFromTimerWithLabels() {
final LabelledMetric<OperationTimer> timer =
metricsSystem.createLabelledTimer(RPC, "request", "Some help", "methodName");
//noinspection EmptyTryBlock
try (final OperationTimer.TimingContext ignored = timer.labels("method").startTimer()) {}
//noinspection EmptyTryBlock
try (final OperationTimer.TimingContext ignored = timer.labels("method").startTimer()) {}
//noinspection EmptyTryBlock
try (final OperationTimer.TimingContext ignored = timer.labels("method").startTimer()) {}
//noinspection EmptyTryBlock
try (final OperationTimer.TimingContext ignored = timer.labels("method").startTimer()) {}
assertThat(metricsSystem.streamObservations())
.usingElementComparator(IGNORE_VALUES) // We don't know how long it will actually take.
.containsExactlyInAnyOrder(new Observation(RPC, "request", null, singletonList("method")));
}
@Test
public void shouldNotCreateObservationsFromTimerWhenTimersDisabled() {
final ObservableMetricsSystem metricsSystem =
new OpenTelemetrySystem(DEFAULT_METRIC_CATEGORIES, false, "job");
final LabelledMetric<OperationTimer> timer =
metricsSystem.createLabelledTimer(RPC, "request", "Some help", "methodName");
//noinspection EmptyTryBlock
try (final OperationTimer.TimingContext ignored = timer.labels("method").startTimer()) {}
assertThat(metricsSystem.streamObservations()).isEmpty();
}
@Test
public void shouldCreateObservationFromGauge() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder()
.metricCategories(ImmutableSet.of(BesuMetricCategory.RPC))
.enabled(true)
.protocol(OPENTELEMETRY)
.build();
final ObservableMetricsSystem localMetricSystem =
MetricsSystemFactory.create(metricsConfiguration);
localMetricSystem.createGauge(RPC, "myValue", "Help", () -> 7d);
List<MetricData.ValueAtPercentile> values = new ArrayList<>();
values.add(MetricData.ValueAtPercentile.create(0, 7d));
values.add(MetricData.ValueAtPercentile.create(100, 7d));
assertThat(localMetricSystem.streamObservations())
.containsExactlyInAnyOrder(new Observation(RPC, "myValue", values, emptyList()));
}
@Test
public void shouldOnlyObserveEnabledMetrics() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder()
.metricCategories(ImmutableSet.of(BesuMetricCategory.RPC))
.enabled(true)
.protocol(OPENTELEMETRY)
.build();
final ObservableMetricsSystem localMetricSystem =
MetricsSystemFactory.create(metricsConfiguration);
// do a category we are not watching
final LabelledMetric<Counter> counterN =
localMetricSystem.createLabelledCounter(NETWORK, "ABC", "Not that kind of network", "show");
assertThat(counterN).isSameAs(NoOpMetricsSystem.NO_OP_LABELLED_1_COUNTER);
counterN.labels("show").inc();
assertThat(localMetricSystem.streamObservations()).isEmpty();
// do a category we are watching
final LabelledMetric<Counter> counterR =
localMetricSystem.createLabelledCounter(RPC, "name", "Not useful", "method");
assertThat(counterR).isNotSameAs(NoOpMetricsSystem.NO_OP_LABELLED_1_COUNTER);
counterR.labels("op").inc();
assertThat(localMetricSystem.streamObservations())
.containsExactly(new Observation(RPC, "name", (long) 1, singletonList("op")));
}
@Test
public void returnsNoOpMetricsWhenAllDisabled() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder()
.enabled(false)
.pushEnabled(false)
.protocol(OPENTELEMETRY)
.build();
final MetricsSystem localMetricSystem = MetricsSystemFactory.create(metricsConfiguration);
assertThat(localMetricSystem).isInstanceOf(NoOpMetricsSystem.class);
}
@Test
public void returnsPrometheusMetricsWhenEnabled() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder()
.enabled(true)
.pushEnabled(false)
.protocol(OPENTELEMETRY)
.build();
final MetricsSystem localMetricSystem = MetricsSystemFactory.create(metricsConfiguration);
assertThat(localMetricSystem).isInstanceOf(OpenTelemetrySystem.class);
}
@Test
public void returnsNoOpMetricsWhenPushEnabled() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder()
.enabled(false)
.pushEnabled(true)
.protocol(OPENTELEMETRY)
.build();
final MetricsSystem localMetricSystem = MetricsSystemFactory.create(metricsConfiguration);
assertThat(localMetricSystem).isInstanceOf(OpenTelemetrySystem.class);
}
}

@ -18,6 +18,8 @@ import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.util.NetworkUtility.urlForSocketAddress;
import org.hyperledger.besu.metrics.MetricsSystemFactory;
import java.net.InetSocketAddress;
import java.util.Properties;
@ -53,13 +55,13 @@ public class MetricsHttpServiceTest {
}
private static MetricsHttpService createMetricsHttpService(final MetricsConfiguration config) {
return new MetricsHttpService(vertx, config, PrometheusMetricsSystem.init(config));
return new MetricsHttpService(vertx, config, MetricsSystemFactory.create(config));
}
private static MetricsHttpService createMetricsHttpService() {
final MetricsConfiguration metricsConfiguration = createMetricsConfig();
return new MetricsHttpService(
vertx, metricsConfiguration, PrometheusMetricsSystem.init(metricsConfiguration));
vertx, metricsConfiguration, MetricsSystemFactory.create(metricsConfiguration));
}
private static MetricsConfiguration createMetricsConfig() {

@ -26,6 +26,7 @@ import static org.hyperledger.besu.metrics.BesuMetricCategory.RPC;
import static org.hyperledger.besu.metrics.StandardMetricCategory.JVM;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.metrics.MetricsSystemFactory;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.Observation;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
@ -34,6 +35,7 @@ import org.hyperledger.besu.plugin.services.metrics.Counter;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
import org.hyperledger.besu.plugin.services.metrics.OperationTimer;
import java.util.Collections;
import java.util.Comparator;
import com.google.common.collect.ImmutableSet;
@ -160,7 +162,7 @@ public class PrometheusMetricsSystemTest {
@Test
public void shouldNotCreateObservationsFromTimerWhenTimersDisabled() {
final ObservableMetricsSystem metricsSystem =
new PrometheusMetricsSystem(DEFAULT_METRIC_CATEGORIES, false);
new PrometheusMetricsSystem(Collections.emptySet(), false);
final LabelledMetric<OperationTimer> timer =
metricsSystem.createLabelledTimer(RPC, "request", "Some help", "methodName");
@ -196,7 +198,7 @@ public class PrometheusMetricsSystemTest {
.enabled(true)
.build();
final ObservableMetricsSystem localMetricSystem =
PrometheusMetricsSystem.init(metricsConfiguration);
MetricsSystemFactory.create(metricsConfiguration);
// do a category we are not watching
final LabelledMetric<Counter> counterN =
@ -220,7 +222,7 @@ public class PrometheusMetricsSystemTest {
public void returnsNoOpMetricsWhenAllDisabled() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder().enabled(false).pushEnabled(false).build();
final MetricsSystem localMetricSystem = PrometheusMetricsSystem.init(metricsConfiguration);
final MetricsSystem localMetricSystem = MetricsSystemFactory.create(metricsConfiguration);
assertThat(localMetricSystem).isInstanceOf(NoOpMetricsSystem.class);
}
@ -229,7 +231,7 @@ public class PrometheusMetricsSystemTest {
public void returnsPrometheusMetricsWhenEnabled() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder().enabled(true).pushEnabled(false).build();
final MetricsSystem localMetricSystem = PrometheusMetricsSystem.init(metricsConfiguration);
final MetricsSystem localMetricSystem = MetricsSystemFactory.create(metricsConfiguration);
assertThat(localMetricSystem).isInstanceOf(PrometheusMetricsSystem.class);
}
@ -238,7 +240,7 @@ public class PrometheusMetricsSystemTest {
public void returnsNoOpMetricsWhenPushEnabled() {
final MetricsConfiguration metricsConfiguration =
MetricsConfiguration.builder().enabled(false).pushEnabled(true).build();
final MetricsSystem localMetricSystem = PrometheusMetricsSystem.init(metricsConfiguration);
final MetricsSystem localMetricSystem = MetricsSystemFactory.create(metricsConfiguration);
assertThat(localMetricSystem).isInstanceOf(PrometheusMetricsSystem.class);
}

@ -173,6 +173,9 @@ public class RocksDBStats {
final Statistics stats,
final PrometheusMetricsSystem metricsSystem,
final MetricCategory category) {
if (!metricsSystem.isCategoryEnabled(category)) {
return;
}
for (final TickerType ticker : TICKERS) {
final String promCounterName = ticker.name().toLowerCase();
metricsSystem.createLongGauge(
@ -183,7 +186,7 @@ public class RocksDBStats {
}
for (final HistogramType histogram : HISTOGRAMS) {
metricsSystem.addCollector(category, histogramToCollector(stats, histogram));
metricsSystem.addCollector(category, () -> histogramToCollector(stats, histogram));
}
}

Loading…
Cancel
Save