From 081c2f92a6fd48695121fba5d86af2af7f2b5b39 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Fri, 30 Nov 2018 09:56:47 +1000 Subject: [PATCH] Begin capturing metrics to better understand Pantheon's behaviour (#326) Metrics being captured initially: Total number of peers ever connected to Total number of peers disconnected, by disconnect reason and whether the disconnect was initiated locally or remotely. Current number of peers Timing for processing JSON-RPC requests, broken down by method name. Generic JVM and process metrics (memory used, heap size, thread count, time spent in GC, file descriptors opened, CPU time etc). --- ethereum/eth/build.gradle | 1 + .../ethereum/eth/transactions/TestNode.java | 4 +- ethereum/jsonrpc/build.gradle | 1 + .../jsonrpc/JsonRpcTestMethodsFactory.java | 4 + .../ethereum/jsonrpc/JsonRpcHttpService.java | 15 +- .../jsonrpc/JsonRpcMethodsFactory.java | 8 +- .../internal/methods/DebugMetrics.java | 72 ++++++++ .../AbstractEthJsonRpcHttpServiceTest.java | 6 +- .../jsonrpc/JsonRpcHttpServiceCorsTest.java | 5 +- .../JsonRpcHttpServiceRpcApisTest.java | 5 +- .../jsonrpc/JsonRpcHttpServiceTest.java | 11 +- .../internal/methods/DebugMetricsTest.java | 88 ++++++++++ ethereum/p2p/build.gradle | 3 +- .../ethereum/p2p/netty/NettyP2PNetwork.java | 10 +- .../p2p/netty/PeerConnectionRegistry.java | 27 +++ .../ethereum/p2p/NettyP2PNetworkTest.java | 49 ++++-- .../p2p/NetworkingServiceLifecycleTest.java | 62 +++++-- .../p2p/netty/PeerConnectionRegistryTest.java | 4 +- gradle/versions.gradle | 3 + metrics/build.gradle | 40 +++++ .../pegasys/pantheon/metrics/Counter.java | 19 ++ .../pantheon/metrics/LabelledMetric.java | 18 ++ .../pantheon/metrics/MetricCategory.java | 40 +++++ .../pantheon/metrics/MetricsSystem.java | 44 +++++ .../pegasys/pantheon/metrics/Observation.java | 82 +++++++++ .../pantheon/metrics/OperationTimer.java | 29 +++ .../pantheon/metrics/noop/NoOpCounter.java | 24 +++ .../metrics/noop/NoOpMetricsSystem.java | 66 +++++++ .../prometheus/CurrentValueCollector.java | 43 +++++ .../metrics/prometheus/PrometheusCounter.java | 48 +++++ .../prometheus/PrometheusMetricsSystem.java | 166 ++++++++++++++++++ .../metrics/prometheus/PrometheusTimer.java | 34 ++++ .../PrometheusMetricsSystemTest.java | 149 ++++++++++++++++ pantheon/build.gradle | 1 + .../tech/pegasys/pantheon/RunnerBuilder.java | 14 +- settings.gradle | 1 + 36 files changed, 1152 insertions(+), 44 deletions(-) create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetrics.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetricsTest.java create mode 100644 metrics/build.gradle create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/Counter.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/LabelledMetric.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricCategory.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricsSystem.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/Observation.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/OperationTimer.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpCounter.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpMetricsSystem.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/CurrentValueCollector.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusCounter.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java create mode 100644 metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusTimer.java create mode 100644 metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystemTest.java diff --git a/ethereum/eth/build.gradle b/ethereum/eth/build.gradle index 4bd9049281..1035578843 100644 --- a/ethereum/eth/build.gradle +++ b/ethereum/eth/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation project(':ethereum:core') implementation project(':ethereum:p2p') implementation project(':ethereum:rlp') + implementation project(':metrics') implementation project(':services:kvstore') implementation 'io.vertx:vertx-core' diff --git a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/transactions/TestNode.java b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/transactions/TestNode.java index fb478c3352..6b77b98ca9 100644 --- a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/transactions/TestNode.java +++ b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/transactions/TestNode.java @@ -44,6 +44,7 @@ import tech.pegasys.pantheon.ethereum.p2p.peers.Endpoint; import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.Closeable; @@ -112,7 +113,8 @@ public class TestNode implements Closeable { networkingConfiguration, capabilities, ethProtocolManager, - new PeerBlacklist())) + new PeerBlacklist(), + new NoOpMetricsSystem())) .build(); network = networkRunner.getNetwork(); this.port = network.getSelf().getPort(); diff --git a/ethereum/jsonrpc/build.gradle b/ethereum/jsonrpc/build.gradle index d81f8a04dd..3c423c8ac4 100644 --- a/ethereum/jsonrpc/build.gradle +++ b/ethereum/jsonrpc/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation project(':ethereum:eth') implementation project(':ethereum:p2p') implementation project(':ethereum:rlp') + implementation project(':metrics') implementation 'com.google.guava:guava' implementation 'io.vertx:vertx-core' diff --git a/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java b/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java index fa281775ef..7b707bbf4e 100644 --- a/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java +++ b/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java @@ -35,6 +35,8 @@ import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSpec; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import java.util.HashSet; import java.util.Map; @@ -75,6 +77,7 @@ public class JsonRpcTestMethodsFactory { new FilterManager( blockchainQueries, transactionPool, new FilterIdGenerator(), new FilterRepository()); final EthHashMiningCoordinator miningCoordinator = mock(EthHashMiningCoordinator.class); + final MetricsSystem metricsSystem = new NoOpMetricsSystem(); return new JsonRpcMethodsFactory() .methods( @@ -86,6 +89,7 @@ public class JsonRpcTestMethodsFactory { filterManager, transactionPool, miningCoordinator, + metricsSystem, new HashSet<>(), RpcApis.DEFAULT_JSON_RPC_APIS); } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java index 1204f51a2b..9ff5c36aac 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java @@ -25,6 +25,11 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResp import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcNoResponse; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponseType; +import tech.pegasys.pantheon.metrics.LabelledMetric; +import tech.pegasys.pantheon.metrics.MetricCategory; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.OperationTimer; +import tech.pegasys.pantheon.metrics.OperationTimer.TimingContext; import tech.pegasys.pantheon.util.NetworkUtility; import java.net.BindException; @@ -69,6 +74,7 @@ public class JsonRpcHttpService { private final JsonRpcConfiguration config; private final Map jsonRpcMethods; private final Path dataDir; + private final LabelledMetric requestTimer; private HttpServer httpServer; @@ -76,8 +82,15 @@ public class JsonRpcHttpService { final Vertx vertx, final Path dataDir, final JsonRpcConfiguration config, + final MetricsSystem metricsSystem, final Map methods) { this.dataDir = dataDir; + requestTimer = + metricsSystem.createLabelledTimer( + MetricCategory.RPC, + "request_time", + "Time taken to process a JSON-RPC request", + "methodName"); validateConfig(config); this.config = config; this.vertx = vertx; @@ -327,7 +340,7 @@ public class JsonRpcHttpService { } // Generate response - try { + try (final TimingContext context = requestTimer.labels(request.getMethod()).startTimer()) { return method.response(request); } catch (final InvalidJsonRpcParameters e) { LOG.debug(e); diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java index c38ef427fe..4ea8939dce 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java @@ -19,6 +19,7 @@ import tech.pegasys.pantheon.ethereum.core.TransactionPool; import tech.pegasys.pantheon.ethereum.db.WorldStateArchive; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.AdminPeers; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.DebugMetrics; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.DebugStorageRangeAt; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.DebugTraceTransaction; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthAccounts; @@ -75,6 +76,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactor import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.metrics.MetricsSystem; import java.util.Collection; import java.util.HashMap; @@ -95,6 +97,7 @@ public class JsonRpcMethodsFactory { final TransactionPool transactionPool, final ProtocolSchedule protocolSchedule, final MiningCoordinator miningCoordinator, + final MetricsSystem metricsSystem, final Set supportedCapabilities, final Collection rpcApis, final FilterManager filterManager) { @@ -109,6 +112,7 @@ public class JsonRpcMethodsFactory { filterManager, transactionPool, miningCoordinator, + metricsSystem, supportedCapabilities, rpcApis); } @@ -122,6 +126,7 @@ public class JsonRpcMethodsFactory { final FilterManager filterManager, final TransactionPool transactionPool, final MiningCoordinator miningCoordinator, + final MetricsSystem metricsSystem, final Set supportedCapabilities, final Collection rpcApis) { final Map enabledMethods = new HashMap<>(); @@ -189,7 +194,8 @@ public class JsonRpcMethodsFactory { enabledMethods, new DebugTraceTransaction( blockchainQueries, new TransactionTracer(blockReplay), parameter), - new DebugStorageRangeAt(parameter, blockchainQueries, blockReplay)); + new DebugStorageRangeAt(parameter, blockchainQueries, blockReplay), + new DebugMetrics(metricsSystem)); } if (rpcApis.contains(RpcApis.NET)) { addMethods( diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetrics.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetrics.java new file mode 100644 index 0000000000..47467eae89 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetrics.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.Observation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DebugMetrics implements JsonRpcMethod { + + private final MetricsSystem metricsSystem; + + public DebugMetrics(final MetricsSystem metricsSystem) { + this.metricsSystem = metricsSystem; + } + + @Override + public String getName() { + return "debug_metrics"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Map observations = new HashMap<>(); + metricsSystem.getMetrics().forEach(observation -> addObservation(observations, observation)); + return new JsonRpcSuccessResponse(request.getId(), observations); + } + + 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()); + } else { + addLabelledObservation(categoryObservations, observation); + } + } + + private void addLabelledObservation( + final Map categoryObservations, final Observation observation) { + final List labels = observation.getLabels(); + Map values = getNextMapLevel(categoryObservations, observation.getMetricName()); + for (int i = 0; i < labels.size() - 1; i++) { + values = getNextMapLevel(values, labels.get(i)); + } + values.put(labels.get(labels.size() - 1), observation.getValue()); + } + + @SuppressWarnings("unchecked") + private Map getNextMapLevel( + final Map current, final String name) { + return (Map) + current.computeIfAbsent(name, key -> new HashMap()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java index 0f2b396024..7006ca07df 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java @@ -47,6 +47,7 @@ import tech.pegasys.pantheon.ethereum.mainnet.ValidationResult; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.util.RawBlockIterator; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import java.net.URL; import java.nio.file.Paths; @@ -175,11 +176,14 @@ public abstract class AbstractEthJsonRpcHttpServiceTest { filterManager, transactionPoolMock, miningCoordinatorMock, + new NoOpMetricsSystem(), supportedCapabilities, JSON_RPC_APIS); final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); config.setPort(0); - service = new JsonRpcHttpService(vertx, folder.newFolder().toPath(), config, methods); + service = + new JsonRpcHttpService( + vertx, folder.newFolder().toPath(), config, new NoOpMetricsSystem(), methods); service.start().join(); client = new OkHttpClient(); diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java index 507381f1e7..5f9074672e 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java @@ -14,6 +14,8 @@ package tech.pegasys.pantheon.ethereum.jsonrpc; import static org.assertj.core.api.Assertions.assertThat; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; + import java.util.HashMap; import com.google.common.collect.Lists; @@ -168,7 +170,8 @@ public class JsonRpcHttpServiceCorsTest { } final JsonRpcHttpService jsonRpcHttpService = - new JsonRpcHttpService(vertx, folder.newFolder().toPath(), config, new HashMap<>()); + new JsonRpcHttpService( + vertx, folder.newFolder().toPath(), config, new NoOpMetricsSystem(), new HashMap<>()); jsonRpcHttpService.start().join(); return jsonRpcHttpService; diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java index a38d0ceac4..2cdbbc0653 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java @@ -28,6 +28,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import java.util.HashSet; import java.util.Map; @@ -176,10 +177,12 @@ public class JsonRpcHttpServiceRpcApisTest { mock(FilterManager.class), mock(TransactionPool.class), mock(EthHashMiningCoordinator.class), + new NoOpMetricsSystem(), supportedCapabilities, config.getRpcApis())); final JsonRpcHttpService jsonRpcHttpService = - new JsonRpcHttpService(vertx, folder.newFolder().toPath(), config, rpcMethods); + new JsonRpcHttpService( + vertx, folder.newFolder().toPath(), config, new NoOpMetricsSystem(), rpcMethods); jsonRpcHttpService.start().join(); baseUrl = jsonRpcHttpService.url(); diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java index 0cccb441fe..3150587602 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java @@ -42,6 +42,7 @@ import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.testutil.BlockDataGenerator; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValues; import tech.pegasys.pantheon.util.uint.UInt256; @@ -119,6 +120,7 @@ public class JsonRpcHttpServiceTest { mock(FilterManager.class), mock(TransactionPool.class), mock(EthHashMiningCoordinator.class), + new NoOpMetricsSystem(), supportedCapabilities, JSON_RPC_APIS)); service = createJsonRpcHttpService(); @@ -131,12 +133,17 @@ public class JsonRpcHttpServiceTest { protected static JsonRpcHttpService createJsonRpcHttpService(final JsonRpcConfiguration config) throws Exception { - return new JsonRpcHttpService(vertx, folder.newFolder().toPath(), config, rpcMethods); + return new JsonRpcHttpService( + vertx, folder.newFolder().toPath(), config, new NoOpMetricsSystem(), rpcMethods); } protected static JsonRpcHttpService createJsonRpcHttpService() throws Exception { return new JsonRpcHttpService( - vertx, folder.newFolder().toPath(), createJsonRpcConfig(), rpcMethods); + vertx, + folder.newFolder().toPath(), + createJsonRpcConfig(), + new NoOpMetricsSystem(), + rpcMethods); } protected static JsonRpcConfiguration createJsonRpcConfig() { diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetricsTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetricsTest.java new file mode 100644 index 0000000000..e89a03827e --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetricsTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static tech.pegasys.pantheon.metrics.MetricCategory.PEERS; +import static tech.pegasys.pantheon.metrics.MetricCategory.RPC; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.Observation; + +import java.util.Collections; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableMap; +import org.junit.Test; + +public class DebugMetricsTest { + + private static final JsonRpcRequest REQUEST = + new JsonRpcRequest("2.0", "debug_metrics", new Object[0]); + private final MetricsSystem metricsSystem = mock(MetricsSystem.class); + + private final DebugMetrics method = new DebugMetrics(metricsSystem); + + @Test + public void shouldHaveCorrectName() { + assertThat(method.getName()).isEqualTo("debug_metrics"); + } + + @Test + public void shouldReportUnlabelledObservationsByCategory() { + when(metricsSystem.getMetrics()) + .thenReturn( + Stream.of( + new Observation(PEERS, "peer1", "peer1Value", Collections.emptyList()), + new Observation(PEERS, "peer2", "peer2Value", Collections.emptyList()), + new Observation(RPC, "rpc1", "rpc1Value", Collections.emptyList()))); + + assertResponse( + ImmutableMap.of( + PEERS.getName(), + ImmutableMap.of("peer1", "peer1Value", "peer2", "peer2Value"), + RPC.getName(), + ImmutableMap.of("rpc1", "rpc1Value"))); + } + + @Test + public void shouldNestObservationsByLabel() { + when(metricsSystem.getMetrics()) + .thenReturn( + Stream.of( + new Observation(PEERS, "peer1", "value1", asList("label1A", "label2A")), + new Observation(PEERS, "peer1", "value2", asList("label1A", "label2B")), + new Observation(PEERS, "peer1", "value3", asList("label1B", "label2B")))); + + assertResponse( + ImmutableMap.of( + PEERS.getName(), + ImmutableMap.of( + "peer1", + ImmutableMap.of( + "label1A", + ImmutableMap.of("label2A", "value1", "label2B", "value2"), + "label1B", + ImmutableMap.of("label2B", "value3"))))); + } + + private void assertResponse(final ImmutableMap expectedResponse) { + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(REQUEST); + assertThat(response.getResult()).isEqualTo(expectedResponse); + } +} diff --git a/ethereum/p2p/build.gradle b/ethereum/p2p/build.gradle index e23707dad7..d321d5c463 100644 --- a/ethereum/p2p/build.gradle +++ b/ethereum/p2p/build.gradle @@ -29,8 +29,10 @@ dependencies { implementation project(':crypto') implementation project(':ethereum:core') implementation project(':ethereum:rlp') + implementation project(':metrics') implementation 'com.google.guava:guava' + implementation 'io.prometheus:simpleclient' implementation 'io.vertx:vertx-core' implementation 'org.apache.logging.log4j:log4j-api' implementation 'org.xerial.snappy:snappy-java' @@ -51,5 +53,4 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'org.awaitility:awaitility' testImplementation 'org.mockito:mockito-core' - } diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java index 52dfd6f7eb..55b8c1d094 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java @@ -30,6 +30,7 @@ import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo; import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol; import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.util.Subscribers; import java.net.InetSocketAddress; @@ -117,7 +118,7 @@ public final class NettyP2PNetwork implements P2PNetwork { private final PeerBlacklist peerBlacklist; private OptionalLong peerBondedObserverId = OptionalLong.empty(); - private final PeerConnectionRegistry connections = new PeerConnectionRegistry(); + private final PeerConnectionRegistry connections; private final AtomicInteger pendingConnections = new AtomicInteger(0); @@ -148,6 +149,7 @@ public final class NettyP2PNetwork implements P2PNetwork { * @param supportedCapabilities The wire protocol capabilities to advertise to connected peers. * @param peerBlacklist The peers with which this node will not connect * @param peerRequirement Queried to determine if enough peers are currently connected. + * @param metricsSystem The metrics system to capture metrics with. */ public NettyP2PNetwork( final Vertx vertx, @@ -155,8 +157,10 @@ public final class NettyP2PNetwork implements P2PNetwork { final NetworkingConfiguration config, final List supportedCapabilities, final PeerRequirement peerRequirement, - final PeerBlacklist peerBlacklist) { + final PeerBlacklist peerBlacklist, + final MetricsSystem metricsSystem) { + connections = new PeerConnectionRegistry(metricsSystem); this.peerBlacklist = peerBlacklist; peerDiscoveryAgent = new PeerDiscoveryAgent( @@ -180,7 +184,7 @@ public final class NettyP2PNetwork implements P2PNetwork { future -> { final InetSocketAddress socketAddress = (InetSocketAddress) server.channel().localAddress(); - String message = + final String message = String.format( "Unable start up P2P network on %s:%s. Check for port conflicts.", config.getRlpx().getBindHost(), config.getRlpx().getBindPort()); diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java index 9c8ae686ab..5be6b6cdac 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java @@ -17,6 +17,10 @@ import static java.util.Collections.unmodifiableCollection; import tech.pegasys.pantheon.ethereum.p2p.api.DisconnectCallback; import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import tech.pegasys.pantheon.metrics.Counter; +import tech.pegasys.pantheon.metrics.LabelledMetric; +import tech.pegasys.pantheon.metrics.MetricCategory; +import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.util.Collection; @@ -27,8 +31,30 @@ public class PeerConnectionRegistry implements DisconnectCallback { private final ConcurrentMap connections = new ConcurrentHashMap<>(); + private final LabelledMetric disconnectCounter; + private final Counter connectedPeersCounter; + + public PeerConnectionRegistry(final MetricsSystem metricsSystem) { + disconnectCounter = + metricsSystem.createLabelledCounter( + MetricCategory.PEERS, + "disconnected_total", + "Total number of peers disconnected", + "initiator", + "disconnectReason"); + connectedPeersCounter = + metricsSystem.createCounter( + MetricCategory.PEERS, "connected_total", "Total number of peers connected"); + metricsSystem.createGauge( + MetricCategory.PEERS, + "peer_count_current", + "Number of peers currently connected", + () -> (double) connections.size()); + } + public void registerConnection(final PeerConnection connection) { connections.put(connection.getPeer().getNodeId(), connection); + connectedPeersCounter.inc(); } public Collection getPeerConnections() { @@ -49,5 +75,6 @@ public class PeerConnectionRegistry implements DisconnectCallback { final DisconnectReason reason, final boolean initiatedByPeer) { connections.remove(connection.getPeer().getNodeId()); + disconnectCounter.labels(initiatedByPeer ? "remote" : "local", reason.name()).inc(); } } diff --git a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java index b21c8303cf..e3b12bf13b 100644 --- a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java +++ b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java @@ -36,6 +36,7 @@ import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo; import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol; import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.net.InetAddress; @@ -73,7 +74,8 @@ public final class NettyP2PNetworkTest { .setRlpx(RlpxConfiguration.create().setBindPort(0)), singletonList(cap), () -> false, - new PeerBlacklist()); + new PeerBlacklist(), + new NoOpMetricsSystem()); final P2PNetwork connector = new NettyP2PNetwork( vertx, @@ -84,7 +86,8 @@ public final class NettyP2PNetworkTest { .setDiscovery(noDiscovery), singletonList(cap), () -> false, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { final int listenPort = listener.getSelf().getPort(); listener.run(); @@ -123,7 +126,8 @@ public final class NettyP2PNetworkTest { .setRlpx(RlpxConfiguration.create().setBindPort(0)), capabilities, () -> true, - new PeerBlacklist()); + new PeerBlacklist(), + new NoOpMetricsSystem()); final P2PNetwork connector = new NettyP2PNetwork( vertx, @@ -134,7 +138,8 @@ public final class NettyP2PNetworkTest { .setDiscovery(noDiscovery), capabilities, () -> true, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { final int listenPort = listener.getSelf().getPort(); listener.run(); connector.run(); @@ -188,7 +193,8 @@ public final class NettyP2PNetworkTest { .setSupportedProtocols(subProtocol), cap, () -> true, - new PeerBlacklist()); + new PeerBlacklist(), + new NoOpMetricsSystem()); final P2PNetwork connector1 = new NettyP2PNetwork( vertx, @@ -199,7 +205,8 @@ public final class NettyP2PNetworkTest { .setSupportedProtocols(subProtocol), cap, () -> true, - new PeerBlacklist()); + new PeerBlacklist(), + new NoOpMetricsSystem()); final P2PNetwork connector2 = new NettyP2PNetwork( vertx, @@ -210,7 +217,8 @@ public final class NettyP2PNetworkTest { .setSupportedProtocols(subProtocol), cap, () -> true, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { final int listenPort = listener.getSelf().getPort(); // Setup listener and first connection @@ -263,7 +271,8 @@ public final class NettyP2PNetworkTest { .setRlpx(RlpxConfiguration.create().setBindPort(0)), singletonList(cap1), () -> false, - new PeerBlacklist()); + new PeerBlacklist(), + new NoOpMetricsSystem()); final P2PNetwork connector = new NettyP2PNetwork( vertx, @@ -274,7 +283,8 @@ public final class NettyP2PNetworkTest { .setDiscovery(noDiscovery), singletonList(cap2), () -> false, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { final int listenPort = listener.getSelf().getPort(); listener.run(); connector.run(); @@ -314,7 +324,8 @@ public final class NettyP2PNetworkTest { .setRlpx(RlpxConfiguration.create().setBindPort(0)), singletonList(cap), () -> false, - localBlacklist); + localBlacklist, + new NoOpMetricsSystem()); final P2PNetwork remoteNetwork = new NettyP2PNetwork( vertx, @@ -325,7 +336,8 @@ public final class NettyP2PNetworkTest { .setDiscovery(noDiscovery), singletonList(cap), () -> false, - remoteBlacklist)) { + remoteBlacklist, + new NoOpMetricsSystem())) { final int localListenPort = localNetwork.getSelf().getPort(); final int remoteListenPort = remoteNetwork.getSelf().getPort(); final Peer localPeer = @@ -410,9 +422,9 @@ public final class NettyP2PNetworkTest { @Test public void shouldSendClientQuittingWhenNetworkStops() { - NettyP2PNetwork nettyP2PNetwork = mockNettyP2PNetwork(); - Peer peer = mockPeer(); - PeerConnection peerConnection = mockPeerConnection(); + final NettyP2PNetwork nettyP2PNetwork = mockNettyP2PNetwork(); + final Peer peer = mockPeer(); + final PeerConnection peerConnection = mockPeerConnection(); nettyP2PNetwork.connect(peer).complete(peerConnection); nettyP2PNetwork.stop(); @@ -421,9 +433,9 @@ public final class NettyP2PNetworkTest { } private PeerConnection mockPeerConnection() { - PeerInfo peerInfo = mock(PeerInfo.class); + final PeerInfo peerInfo = mock(PeerInfo.class); when(peerInfo.getNodeId()).thenReturn(BytesValue.fromHexString("0x00")); - PeerConnection peerConnection = mock(PeerConnection.class); + final PeerConnection peerConnection = mock(PeerConnection.class); when(peerConnection.getPeer()).thenReturn(peerInfo); return peerConnection; } @@ -444,11 +456,12 @@ public final class NettyP2PNetworkTest { networkingConfiguration, singletonList(cap), () -> false, - new PeerBlacklist()); + new PeerBlacklist(), + new NoOpMetricsSystem()); } private Peer mockPeer() { - Peer peer = mock(Peer.class); + final Peer peer = mock(Peer.class); when(peer.getId()) .thenReturn( BytesValue.fromHexString( diff --git a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java index a2a49168b2..6cda77f8cb 100644 --- a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java +++ b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java @@ -25,6 +25,7 @@ import tech.pegasys.pantheon.ethereum.p2p.config.NetworkingConfiguration; import tech.pegasys.pantheon.ethereum.p2p.discovery.PeerDiscoveryServiceException; import tech.pegasys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.util.NetworkUtility; import java.io.IOException; @@ -53,7 +54,8 @@ public class NetworkingServiceLifecycleTest { configWithRandomPorts(), emptyList(), () -> true, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { service.run(); final int port = service.getDiscoverySocketAddress().getPort(); @@ -71,7 +73,14 @@ public class NetworkingServiceLifecycleTest { NetworkingConfiguration.create() .setDiscovery(DiscoveryConfiguration.create().setBindHost(null)); try (final P2PNetwork broken = - new NettyP2PNetwork(vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + new NettyP2PNetwork( + vertx, + keyPair, + config, + emptyList(), + () -> true, + new PeerBlacklist(), + new NoOpMetricsSystem())) { Assertions.fail("Expected Exception"); } } @@ -83,7 +92,14 @@ public class NetworkingServiceLifecycleTest { NetworkingConfiguration.create() .setDiscovery(DiscoveryConfiguration.create().setBindHost("fake.fake.fake")); try (final P2PNetwork broken = - new NettyP2PNetwork(vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + new NettyP2PNetwork( + vertx, + keyPair, + config, + emptyList(), + () -> true, + new PeerBlacklist(), + new NoOpMetricsSystem())) { Assertions.fail("Expected Exception"); } } @@ -95,7 +111,14 @@ public class NetworkingServiceLifecycleTest { NetworkingConfiguration.create() .setDiscovery(DiscoveryConfiguration.create().setBindPort(-1)); try (final P2PNetwork broken = - new NettyP2PNetwork(vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + new NettyP2PNetwork( + vertx, + keyPair, + config, + emptyList(), + () -> true, + new PeerBlacklist(), + new NoOpMetricsSystem())) { Assertions.fail("Expected Exception"); } } @@ -104,7 +127,13 @@ public class NetworkingServiceLifecycleTest { public void createPeerDiscoveryAgent_NullKeyPair() throws IOException { try (final P2PNetwork broken = new NettyP2PNetwork( - vertx, null, configWithRandomPorts(), emptyList(), () -> true, new PeerBlacklist())) { + vertx, + null, + configWithRandomPorts(), + emptyList(), + () -> true, + new PeerBlacklist(), + new NoOpMetricsSystem())) { Assertions.fail("Expected Exception"); } } @@ -119,7 +148,8 @@ public class NetworkingServiceLifecycleTest { configWithRandomPorts(), emptyList(), () -> true, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { service.run(); service.stop(); service.run(); @@ -136,7 +166,8 @@ public class NetworkingServiceLifecycleTest { configWithRandomPorts(), emptyList(), () -> true, - new PeerBlacklist()); + new PeerBlacklist(), + new NoOpMetricsSystem()); final NettyP2PNetwork service2 = new NettyP2PNetwork( vertx, @@ -144,7 +175,8 @@ public class NetworkingServiceLifecycleTest { configWithRandomPorts(), emptyList(), () -> true, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { service1.run(); service1.stop(); service2.run(); @@ -162,13 +194,20 @@ public class NetworkingServiceLifecycleTest { configWithRandomPorts(), emptyList(), () -> true, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { service1.run(); final NetworkingConfiguration config = configWithRandomPorts(); config.getDiscovery().setBindPort(service1.getDiscoverySocketAddress().getPort()); try (final NettyP2PNetwork service2 = new NettyP2PNetwork( - vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + vertx, + keyPair, + config, + emptyList(), + () -> true, + new PeerBlacklist(), + new NoOpMetricsSystem())) { try { service2.run(); } catch (final Exception e) { @@ -196,7 +235,8 @@ public class NetworkingServiceLifecycleTest { configWithRandomPorts(), emptyList(), () -> true, - new PeerBlacklist())) { + new PeerBlacklist(), + new NoOpMetricsSystem())) { assertTrue(agent.getDiscoveryPeers().isEmpty()); assertEquals(0, agent.getPeers().size()); } diff --git a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java index 763de2d808..13076d7b92 100644 --- a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java +++ b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.when; import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo; import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import org.junit.Before; @@ -32,7 +33,8 @@ public class PeerConnectionRegistryTest { private final PeerConnection connection1 = mock(PeerConnection.class); private final PeerConnection connection2 = mock(PeerConnection.class); - private final PeerConnectionRegistry registry = new PeerConnectionRegistry(); + private final PeerConnectionRegistry registry = + new PeerConnectionRegistry(new NoOpMetricsSystem()); @Before public void setUp() { diff --git a/gradle/versions.gradle b/gradle/versions.gradle index a9266d3710..2b482053b0 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -35,6 +35,9 @@ dependencyManagement { dependency 'io.pkts:pkts-core:3.0.3' + dependency "io.prometheus:simpleclient:0.5.0" + dependency "io.prometheus:simpleclient_hotspot:0.5.0" + dependency 'io.vertx:vertx-codegen:3.5.4' dependency 'io.vertx:vertx-core:3.5.4' dependency 'io.vertx:vertx-unit:3.5.4' diff --git a/metrics/build.gradle b/metrics/build.gradle new file mode 100644 index 0000000000..c137953b03 --- /dev/null +++ b/metrics/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright 2018 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. + */ + +apply plugin: 'java-library' + +jar { + baseName 'pantheon-metrics' + manifest { + attributes( + 'Specification-Title': baseName, + 'Specification-Version': project.version, + 'Implementation-Title': baseName, + 'Implementation-Version': calculateVersion() + ) + } +} + +dependencies { + implementation 'com.google.guava:guava' + implementation 'org.apache.logging.log4j:log4j-api' + implementation 'io.prometheus:simpleclient' + implementation 'io.prometheus:simpleclient_hotspot' + + runtime 'org.apache.logging.log4j:log4j-core' + + // test dependencies. + testImplementation 'junit:junit' + testImplementation "org.mockito:mockito-core" + testImplementation 'org.assertj:assertj-core' +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/Counter.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/Counter.java new file mode 100644 index 0000000000..5fa2fd2176 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/Counter.java @@ -0,0 +1,19 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics; + +public interface Counter { + void inc(); + + void inc(long amount); +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/LabelledMetric.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/LabelledMetric.java new file mode 100644 index 0000000000..974ead24e0 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/LabelledMetric.java @@ -0,0 +1,18 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics; + +public interface LabelledMetric { + + T labels(String... labels); +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricCategory.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricCategory.java new file mode 100644 index 0000000000..8798501b25 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricCategory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics; + +public enum MetricCategory { + PEERS("peers"), + RPC("rpc"), + JVM("jvm", false), + PROCESS("process", false); + + private final String name; + private final boolean pantheonSpecific; + + MetricCategory(final String name) { + this(name, true); + } + + MetricCategory(final String name, final boolean pantheonSpecific) { + this.name = name; + this.pantheonSpecific = pantheonSpecific; + } + + public String getName() { + return name; + } + + public boolean isPantheonSpecific() { + return pantheonSpecific; + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricsSystem.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricsSystem.java new file mode 100644 index 0000000000..2e95b93416 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricsSystem.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +public interface MetricsSystem { + + default Counter createCounter( + final MetricCategory category, final String name, final String help) { + return createLabelledCounter(category, name, help, new String[0]).labels(); + } + + LabelledMetric createLabelledCounter( + MetricCategory category, String name, String help, String... labelNames); + + default OperationTimer createTimer( + final MetricCategory category, final String name, final String help) { + return createLabelledTimer(category, name, help, new String[0]).labels(); + } + + LabelledMetric createLabelledTimer( + MetricCategory category, String name, String help, String... labelNames); + + void createGauge( + MetricCategory category, String name, String help, Supplier valueSupplier); + + Stream getMetrics(MetricCategory category); + + default Stream getMetrics() { + return Stream.of(MetricCategory.values()).flatMap(this::getMetrics); + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/Observation.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/Observation.java new file mode 100644 index 0000000000..6fe1efd9c1 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/Observation.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics; + +import java.util.List; +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +public class Observation { + private final MetricCategory category; + private final String metricName; + private final List labels; + private final Object value; + + 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; + } + + public MetricCategory getCategory() { + return category; + } + + public String getMetricName() { + return metricName; + } + + public List getLabels() { + return labels; + } + + 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 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(); + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/OperationTimer.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/OperationTimer.java new file mode 100644 index 0000000000..e16bc15dca --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/OperationTimer.java @@ -0,0 +1,29 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics; + +import java.io.Closeable; + +public interface OperationTimer { + + TimingContext startTimer(); + + interface TimingContext extends Closeable { + void stopTimer(); + + @Override + default void close() { + stopTimer(); + } + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpCounter.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpCounter.java new file mode 100644 index 0000000000..42db9ecb28 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpCounter.java @@ -0,0 +1,24 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics.noop; + +import tech.pegasys.pantheon.metrics.Counter; + +class NoOpCounter implements Counter { + + @Override + public void inc() {} + + @Override + public void inc(final long amount) {} +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpMetricsSystem.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpMetricsSystem.java new file mode 100644 index 0000000000..090c98058c --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpMetricsSystem.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics.noop; + +import tech.pegasys.pantheon.metrics.Counter; +import tech.pegasys.pantheon.metrics.LabelledMetric; +import tech.pegasys.pantheon.metrics.MetricCategory; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.Observation; +import tech.pegasys.pantheon.metrics.OperationTimer; +import tech.pegasys.pantheon.metrics.OperationTimer.TimingContext; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class NoOpMetricsSystem implements MetricsSystem { + + private static final Counter NO_OP_COUNTER = new NoOpCounter(); + private static final TimingContext NO_OP_TIMING_CONTEXT = () -> {}; + private static final OperationTimer NO_OP_TIMER = () -> NO_OP_TIMING_CONTEXT; + + @Override + public LabelledMetric createLabelledCounter( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + return labels -> NO_OP_COUNTER; + } + + @Override + public LabelledMetric createLabelledTimer( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + return labels -> NO_OP_TIMER; + } + + @Override + public void createGauge( + final MetricCategory category, + final String name, + final String help, + final Supplier valueSupplier) {} + + @Override + public Stream getMetrics(final MetricCategory category) { + return Stream.empty(); + } + + @Override + public Stream getMetrics() { + return Stream.empty(); + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/CurrentValueCollector.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/CurrentValueCollector.java new file mode 100644 index 0000000000..e1666be7ca --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/CurrentValueCollector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics.prometheus; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +import java.util.List; +import java.util.function.Supplier; + +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 Supplier valueSupplier; + + public CurrentValueCollector( + final String metricName, final String help, final Supplier valueSupplier) { + this.metricName = metricName; + this.help = help; + this.valueSupplier = valueSupplier; + } + + @Override + public List collect() { + final Sample sample = new Sample(metricName, emptyList(), emptyList(), valueSupplier.get()); + return singletonList( + new MetricFamilySamples(metricName, Type.GAUGE, help, singletonList(sample))); + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusCounter.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusCounter.java new file mode 100644 index 0000000000..09c303c3b3 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusCounter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics.prometheus; + +import tech.pegasys.pantheon.metrics.Counter; +import tech.pegasys.pantheon.metrics.LabelledMetric; + +class PrometheusCounter implements LabelledMetric { + + private final io.prometheus.client.Counter counter; + + public PrometheusCounter(final io.prometheus.client.Counter counter) { + this.counter = counter; + } + + @Override + public Counter labels(final String... labels) { + return new UnlabelledCounter(counter.labels(labels)); + } + + private static class UnlabelledCounter implements Counter { + private final io.prometheus.client.Counter.Child counter; + + private UnlabelledCounter(final io.prometheus.client.Counter.Child counter) { + this.counter = counter; + } + + @Override + public void inc() { + counter.inc(); + } + + @Override + public void inc(final long amount) { + counter.inc(amount); + } + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java new file mode 100644 index 0000000000..f97fd24a53 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java @@ -0,0 +1,166 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics.prometheus; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; + +import tech.pegasys.pantheon.metrics.LabelledMetric; +import tech.pegasys.pantheon.metrics.MetricCategory; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.Observation; +import tech.pegasys.pantheon.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.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import io.prometheus.client.Collector; +import io.prometheus.client.Collector.MetricFamilySamples; +import io.prometheus.client.Collector.MetricFamilySamples.Sample; +import io.prometheus.client.Collector.Type; +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; +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; + +public class PrometheusMetricsSystem implements MetricsSystem { + + private static final String PANTHEON_PREFIX = "pantheon_"; + private final Map> collectors = new ConcurrentHashMap<>(); + + PrometheusMetricsSystem() {} + + public static MetricsSystem init() { + final PrometheusMetricsSystem metricsSystem = new PrometheusMetricsSystem(); + metricsSystem.collectors.put(MetricCategory.PROCESS, singleton(new StandardExports())); + metricsSystem.collectors.put( + MetricCategory.JVM, + asList( + new MemoryPoolsExports(), + new BufferPoolsExports(), + new GarbageCollectorExports(), + new ThreadExports(), + new ClassLoadingExports())); + return metricsSystem; + } + + @Override + public LabelledMetric createLabelledCounter( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + final Counter counter = + Counter.build(convertToPrometheusName(category, name), help) + .labelNames(labelNames) + .create(); + addCollector(category, counter); + return new PrometheusCounter(counter); + } + + @Override + public LabelledMetric createLabelledTimer( + final MetricCategory category, + final String name, + final String help, + final String... labelNames) { + final Histogram histogram = Histogram.build(name, help).labelNames(labelNames).create(); + addCollector(category, histogram); + return new PrometheusTimer(histogram); + } + + @Override + public void createGauge( + final MetricCategory category, + final String name, + final String help, + final Supplier valueSupplier) { + final String metricName = convertToPrometheusName(category, name); + addCollector(category, new CurrentValueCollector(metricName, help, valueSupplier)); + } + + private void addCollector(final MetricCategory category, final Collector counter) { + collectors + .computeIfAbsent(category, key -> Collections.newSetFromMap(new ConcurrentHashMap<>())) + .add(counter); + } + + @Override + public Stream getMetrics(final MetricCategory category) { + return collectors + .getOrDefault(category, Collections.emptySet()) + .stream() + .flatMap(collector -> collector.collect().stream()) + .flatMap(familySamples -> convertSamplesToObservations(category, familySamples)); + } + + private Stream convertSamplesToObservations( + final MetricCategory category, final MetricFamilySamples familySamples) { + return familySamples + .samples + .stream() + .map(sample -> createObservationFromSample(category, sample, familySamples)); + } + + private Observation createObservationFromSample( + final MetricCategory category, final Sample sample, final MetricFamilySamples familySamples) { + if (familySamples.type == Type.HISTOGRAM) { + return convertHistogramSampleNamesToLabels(category, sample, familySamples); + } + return new Observation( + category, + convertFromPrometheusName(category, sample.name), + sample.value, + sample.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 String convertToPrometheusName(final MetricCategory category, final String name) { + return prometheusPrefix(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 prometheusPrefix(final MetricCategory category) { + return category.isPantheonSpecific() + ? PANTHEON_PREFIX + category.getName() + "_" + : category.getName() + "_"; + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusTimer.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusTimer.java new file mode 100644 index 0000000000..55e75a4ff5 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusTimer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics.prometheus; + +import tech.pegasys.pantheon.metrics.LabelledMetric; +import tech.pegasys.pantheon.metrics.OperationTimer; + +import io.prometheus.client.Histogram; +import io.prometheus.client.Histogram.Child; + +class PrometheusTimer implements LabelledMetric { + + private final Histogram histogram; + + public PrometheusTimer(final Histogram histogram) { + this.histogram = histogram; + } + + @Override + public OperationTimer labels(final String... labels) { + final Child metric = histogram.labels(labels); + return () -> metric.startTimer()::observeDuration; + } +} diff --git a/metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystemTest.java b/metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystemTest.java new file mode 100644 index 0000000000..243695355a --- /dev/null +++ b/metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystemTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2018 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. + */ +package tech.pegasys.pantheon.metrics.prometheus; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.metrics.MetricCategory.JVM; +import static tech.pegasys.pantheon.metrics.MetricCategory.PEERS; +import static tech.pegasys.pantheon.metrics.MetricCategory.RPC; + +import tech.pegasys.pantheon.metrics.Counter; +import tech.pegasys.pantheon.metrics.LabelledMetric; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.Observation; +import tech.pegasys.pantheon.metrics.OperationTimer; +import tech.pegasys.pantheon.metrics.OperationTimer.TimingContext; + +import java.util.Comparator; + +import org.junit.Test; + +public class PrometheusMetricsSystemTest { + + private static final Comparator IGNORE_VALUES = + Comparator.comparing(Observation::getCategory) + .thenComparing(Observation::getMetricName) + .thenComparing((o1, o2) -> o1.getLabels().equals(o2.getLabels()) ? 0 : 1); + + private final MetricsSystem metricsSystem = new PrometheusMetricsSystem(); + + @Test + public void shouldCreateObservationFromCounter() { + final Counter counter = metricsSystem.createCounter(PEERS, "connected", "Some help string"); + + counter.inc(); + assertThat(metricsSystem.getMetrics()) + .containsExactly(new Observation(PEERS, "connected", 1d, emptyList())); + + counter.inc(); + assertThat(metricsSystem.getMetrics()) + .containsExactly(new Observation(PEERS, "connected", 2d, emptyList())); + } + + @Test + public void shouldCreateSeparateObservationsForEachCounterLabelValue() { + final LabelledMetric counter = + metricsSystem.createLabelledCounter(PEERS, "connected", "Some help string", "labelName"); + + counter.labels("value1").inc(); + counter.labels("value2").inc(); + counter.labels("value1").inc(); + + assertThat(metricsSystem.getMetrics()) + .containsExactlyInAnyOrder( + new Observation(PEERS, "connected", 2d, singletonList("value1")), + new Observation(PEERS, "connected", 1d, singletonList("value2"))); + } + + @Test + public void shouldIncrementCounterBySpecifiedAmount() { + final Counter counter = metricsSystem.createCounter(PEERS, "connected", "Some help string"); + + counter.inc(5); + assertThat(metricsSystem.getMetrics()) + .containsExactly(new Observation(PEERS, "connected", 5d, emptyList())); + + counter.inc(6); + assertThat(metricsSystem.getMetrics()) + .containsExactly(new Observation(PEERS, "connected", 11d, emptyList())); + } + + @Test + public void shouldCreateObservationsFromTimer() { + final OperationTimer timer = metricsSystem.createTimer(RPC, "request", "Some help"); + + final TimingContext context = timer.startTimer(); + context.stopTimer(); + + assertThat(metricsSystem.getMetrics()) + .usingElementComparator(IGNORE_VALUES) + .containsExactlyInAnyOrder( + new Observation(RPC, "request", null, asList("bucket", "0.005")), + new Observation(RPC, "request", null, asList("bucket", "0.01")), + new Observation(RPC, "request", null, asList("bucket", "0.025")), + new Observation(RPC, "request", null, asList("bucket", "0.05")), + new Observation(RPC, "request", null, asList("bucket", "0.075")), + new Observation(RPC, "request", null, asList("bucket", "0.1")), + new Observation(RPC, "request", null, asList("bucket", "0.25")), + new Observation(RPC, "request", null, asList("bucket", "0.5")), + new Observation(RPC, "request", null, asList("bucket", "0.75")), + new Observation(RPC, "request", null, asList("bucket", "1.0")), + new Observation(RPC, "request", null, asList("bucket", "2.5")), + new Observation(RPC, "request", null, asList("bucket", "5.0")), + new Observation(RPC, "request", null, asList("bucket", "7.5")), + new Observation(RPC, "request", null, asList("bucket", "10.0")), + new Observation(RPC, "request", null, asList("bucket", "+Inf")), + new Observation(RPC, "request", null, singletonList("sum")), + new Observation(RPC, "request", 1d, singletonList("count"))); + } + + @Test + public void shouldCreateObservationsFromTimerWithLabels() { + final LabelledMetric timer = + metricsSystem.createLabelledTimer(RPC, "request", "Some help", "methodName"); + + try (final TimingContext context = timer.labels("method").startTimer()) {} + + assertThat(metricsSystem.getMetrics()) + .usingElementComparator(IGNORE_VALUES) // We don't know how long it will actually take. + .containsExactlyInAnyOrder( + new Observation(RPC, "request", null, asList("method", "bucket", "0.005")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.01")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.025")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.05")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.075")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.1")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.25")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.5")), + new Observation(RPC, "request", null, asList("method", "bucket", "0.75")), + new Observation(RPC, "request", null, asList("method", "bucket", "1.0")), + new Observation(RPC, "request", null, asList("method", "bucket", "2.5")), + new Observation(RPC, "request", null, asList("method", "bucket", "5.0")), + new Observation(RPC, "request", null, asList("method", "bucket", "7.5")), + new Observation(RPC, "request", null, asList("method", "bucket", "10.0")), + new Observation(RPC, "request", null, asList("method", "bucket", "+Inf")), + new Observation(RPC, "request", null, asList("method", "sum")), + new Observation(RPC, "request", null, asList("method", "count"))); + } + + @Test + public void shouldCreateObservationFromGauge() { + metricsSystem.createGauge(JVM, "myValue", "Help", () -> 7d); + + assertThat(metricsSystem.getMetrics()) + .containsExactlyInAnyOrder(new Observation(JVM, "myValue", 7d, emptyList())); + } +} diff --git a/pantheon/build.gradle b/pantheon/build.gradle index 089d2f875f..f63b7bfa35 100644 --- a/pantheon/build.gradle +++ b/pantheon/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation project(':ethereum:jsonrpc') implementation project(':ethereum:p2p') implementation project(':ethereum:rlp') + implementation project(':metrics') implementation project(':services:kvstore') implementation 'com.google.guava:guava' diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 2b3a5d65fe..a29bbd4588 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -50,6 +50,8 @@ import tech.pegasys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.nio.file.Path; @@ -80,6 +82,7 @@ public class RunnerBuilder { Preconditions.checkNotNull(pantheonController); + final MetricsSystem metricsSystem = PrometheusMetricsSystem.init(); final DiscoveryConfiguration discoveryConfiguration; if (discovery) { final Collection bootstrap; @@ -136,7 +139,8 @@ public class RunnerBuilder { networkConfig, caps, PeerRequirement.aggregateOf(protocolManagers), - peerBlacklist)) + peerBlacklist, + metricsSystem)) .build(); final Synchronizer synchronizer = pantheonController.getSynchronizer(); @@ -156,11 +160,14 @@ public class RunnerBuilder { synchronizer, transactionPool, miningCoordinator, + metricsSystem, supportedCapabilities, jsonRpcConfiguration.getRpcApis(), filterManager); jsonRpcHttpService = - Optional.of(new JsonRpcHttpService(vertx, dataDir, jsonRpcConfiguration, jsonRpcMethods)); + Optional.of( + new JsonRpcHttpService( + vertx, dataDir, jsonRpcConfiguration, metricsSystem, jsonRpcMethods)); } Optional webSocketService = Optional.empty(); @@ -174,6 +181,7 @@ public class RunnerBuilder { synchronizer, transactionPool, miningCoordinator, + metricsSystem, supportedCapabilities, webSocketConfiguration.getRpcApis(), filterManager); @@ -219,6 +227,7 @@ public class RunnerBuilder { final Synchronizer synchronizer, final TransactionPool transactionPool, final MiningCoordinator miningCoordinator, + final MetricsSystem metricsSystem, final Set supportedCapabilities, final Collection jsonRpcApis, final FilterManager filterManager) { @@ -233,6 +242,7 @@ public class RunnerBuilder { transactionPool, protocolSchedule, miningCoordinator, + metricsSystem, supportedCapabilities, jsonRpcApis, filterManager); diff --git a/settings.gradle b/settings.gradle index 60bac3c720..f6bfe1a300 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,6 +31,7 @@ include 'ethereum:blockcreation' include 'ethereum:rlp' include 'ethereum:eth' include 'ethereum:trie' +include 'metrics' include 'pantheon' include 'services:kvstore' include 'testutil'