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'