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).
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Adrian Sutton 6 years ago committed by GitHub
parent 3ccd622a52
commit b2ad123443
  1. 1
      ethereum/eth/build.gradle
  2. 4
      ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/transactions/TestNode.java
  3. 1
      ethereum/jsonrpc/build.gradle
  4. 4
      ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java
  5. 15
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java
  6. 8
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java
  7. 72
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetrics.java
  8. 6
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java
  9. 5
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java
  10. 5
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java
  11. 11
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java
  12. 88
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/DebugMetricsTest.java
  13. 3
      ethereum/p2p/build.gradle
  14. 10
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java
  15. 27
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java
  16. 49
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java
  17. 62
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java
  18. 4
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java
  19. 3
      gradle/versions.gradle
  20. 40
      metrics/build.gradle
  21. 19
      metrics/src/main/java/tech/pegasys/pantheon/metrics/Counter.java
  22. 18
      metrics/src/main/java/tech/pegasys/pantheon/metrics/LabelledMetric.java
  23. 40
      metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricCategory.java
  24. 44
      metrics/src/main/java/tech/pegasys/pantheon/metrics/MetricsSystem.java
  25. 82
      metrics/src/main/java/tech/pegasys/pantheon/metrics/Observation.java
  26. 29
      metrics/src/main/java/tech/pegasys/pantheon/metrics/OperationTimer.java
  27. 24
      metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpCounter.java
  28. 66
      metrics/src/main/java/tech/pegasys/pantheon/metrics/noop/NoOpMetricsSystem.java
  29. 43
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/CurrentValueCollector.java
  30. 48
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusCounter.java
  31. 166
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java
  32. 34
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusTimer.java
  33. 149
      metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystemTest.java
  34. 1
      pantheon/build.gradle
  35. 14
      pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java
  36. 1
      settings.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'

@ -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();

@ -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'

@ -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);
}

@ -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<String, JsonRpcMethod> jsonRpcMethods;
private final Path dataDir;
private final LabelledMetric<OperationTimer> 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<String, JsonRpcMethod> 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);

@ -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<Capability> supportedCapabilities,
final Collection<RpcApi> 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<Capability> supportedCapabilities,
final Collection<RpcApi> rpcApis) {
final Map<String, JsonRpcMethod> 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(

@ -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<String, Object> observations = new HashMap<>();
metricsSystem.getMetrics().forEach(observation -> addObservation(observations, observation));
return new JsonRpcSuccessResponse(request.getId(), observations);
}
private void addObservation(
final Map<String, Object> observations, final Observation observation) {
final Map<String, Object> 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<String, Object> categoryObservations, final Observation observation) {
final List<String> labels = observation.getLabels();
Map<String, Object> 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<String, Object> getNextMapLevel(
final Map<String, Object> current, final String name) {
return (Map<String, Object>)
current.computeIfAbsent(name, key -> new HashMap<String, Object>());
}
}

@ -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();

@ -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;

@ -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();

@ -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() {

@ -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.<String, Object>of("peer1", "peer1Value", "peer2", "peer2Value"),
RPC.getName(),
ImmutableMap.<String, Object>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.<String, Object>of(
"peer1",
ImmutableMap.of(
"label1A",
ImmutableMap.of("label2A", "value1", "label2B", "value2"),
"label1B",
ImmutableMap.of("label2B", "value3")))));
}
private void assertResponse(final ImmutableMap<String, Object> expectedResponse) {
final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(REQUEST);
assertThat(response.getResult()).isEqualTo(expectedResponse);
}
}

@ -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'
}

@ -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<Capability> 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());

@ -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<BytesValue, PeerConnection> connections = new ConcurrentHashMap<>();
private final LabelledMetric<Counter> 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<PeerConnection> 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();
}
}

@ -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(

@ -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());
}

@ -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() {

@ -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'

@ -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'
}

@ -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);
}

@ -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> {
T labels(String... labels);
}

@ -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;
}
}

@ -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<Counter> 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<OperationTimer> createLabelledTimer(
MetricCategory category, String name, String help, String... labelNames);
void createGauge(
MetricCategory category, String name, String help, Supplier<Double> valueSupplier);
Stream<Observation> getMetrics(MetricCategory category);
default Stream<Observation> getMetrics() {
return Stream.of(MetricCategory.values()).flatMap(this::getMetrics);
}
}

@ -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<String> labels;
private final Object value;
public Observation(
final MetricCategory category,
final String metricName,
final Object value,
final List<String> 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<String> 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();
}
}

@ -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();
}
}
}

@ -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) {}
}

@ -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<Counter> createLabelledCounter(
final MetricCategory category,
final String name,
final String help,
final String... labelNames) {
return labels -> NO_OP_COUNTER;
}
@Override
public LabelledMetric<OperationTimer> 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<Double> valueSupplier) {}
@Override
public Stream<Observation> getMetrics(final MetricCategory category) {
return Stream.empty();
}
@Override
public Stream<Observation> getMetrics() {
return Stream.empty();
}
}

@ -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<Double> valueSupplier;
public CurrentValueCollector(
final String metricName, final String help, final Supplier<Double> valueSupplier) {
this.metricName = metricName;
this.help = help;
this.valueSupplier = valueSupplier;
}
@Override
public List<MetricFamilySamples> collect() {
final Sample sample = new Sample(metricName, emptyList(), emptyList(), valueSupplier.get());
return singletonList(
new MetricFamilySamples(metricName, Type.GAUGE, help, singletonList(sample)));
}
}

@ -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<Counter> {
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);
}
}
}

@ -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<MetricCategory, Collection<Collector>> 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<tech.pegasys.pantheon.metrics.Counter> 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<OperationTimer> 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<Double> 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<Observation> getMetrics(final MetricCategory category) {
return collectors
.getOrDefault(category, Collections.emptySet())
.stream()
.flatMap(collector -> collector.collect().stream())
.flatMap(familySamples -> convertSamplesToObservations(category, familySamples));
}
private Stream<Observation> 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<String> 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() + "_";
}
}

@ -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<OperationTimer> {
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;
}
}

@ -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<Observation> 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> 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<OperationTimer> 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()));
}
}

@ -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'

@ -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> 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<Capability> supportedCapabilities,
final Collection<RpcApi> jsonRpcApis,
final FilterManager filterManager) {
@ -233,6 +242,7 @@ public class RunnerBuilder {
transactionPool,
protocolSchedule,
miningCoordinator,
metricsSystem,
supportedCapabilities,
jsonRpcApis,
filterManager);

@ -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'

Loading…
Cancel
Save