Expose metrics to prometheus (#506)

A service like the JSON-RPC service is opened up, only serving /metrics
requests in a file format for prometheus.

New CLI flags are --metrics-enabled and --metrics-listen, just like the
--rpc and --ws variants of the same.

--host-whitelist is respected the same as the JSON-RPC endpoint.
Danno Ferrin 6 years ago committed by GitHub
parent ae2e1eb520
commit 2c5f49dfef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java
  2. 1
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java
  3. 8
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfiguration.java
  4. 9
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java
  5. 1
      acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java
  6. 1
      gradle/versions.gradle
  7. 10
      metrics/build.gradle
  8. 104
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsConfiguration.java
  9. 231
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpService.java
  10. 24
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java
  11. 175
      metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpServiceTest.java
  12. 13
      pantheon/src/main/java/tech/pegasys/pantheon/Runner.java
  13. 26
      pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java
  14. 28
      pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java
  15. 12
      pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java
  16. 2
      pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java
  17. 58
      pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java
  18. 1
      pantheon/src/test/resources/complete_config.toml

@ -23,6 +23,7 @@ import tech.pegasys.pantheon.ethereum.core.Util;
import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition;
import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j;
import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction;
@ -69,6 +70,7 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto
private final MiningParameters miningParameters; private final MiningParameters miningParameters;
private final JsonRpcConfiguration jsonRpcConfiguration; private final JsonRpcConfiguration jsonRpcConfiguration;
private final WebSocketConfiguration webSocketConfiguration; private final WebSocketConfiguration webSocketConfiguration;
private final MetricsConfiguration metricsConfiguration;
private final PermissioningConfiguration permissioningConfiguration; private final PermissioningConfiguration permissioningConfiguration;
private final GenesisConfigProvider genesisConfigProvider; private final GenesisConfigProvider genesisConfigProvider;
private final boolean devMode; private final boolean devMode;
@ -82,6 +84,7 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto
final MiningParameters miningParameters, final MiningParameters miningParameters,
final JsonRpcConfiguration jsonRpcConfiguration, final JsonRpcConfiguration jsonRpcConfiguration,
final WebSocketConfiguration webSocketConfiguration, final WebSocketConfiguration webSocketConfiguration,
final MetricsConfiguration metricsConfiguration,
final PermissioningConfiguration permissioningConfiguration, final PermissioningConfiguration permissioningConfiguration,
final boolean devMode, final boolean devMode,
final GenesisConfigProvider genesisConfigProvider, final GenesisConfigProvider genesisConfigProvider,
@ -94,6 +97,7 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto
this.miningParameters = miningParameters; this.miningParameters = miningParameters;
this.jsonRpcConfiguration = jsonRpcConfiguration; this.jsonRpcConfiguration = jsonRpcConfiguration;
this.webSocketConfiguration = webSocketConfiguration; this.webSocketConfiguration = webSocketConfiguration;
this.metricsConfiguration = metricsConfiguration;
this.permissioningConfiguration = permissioningConfiguration; this.permissioningConfiguration = permissioningConfiguration;
this.genesisConfigProvider = genesisConfigProvider; this.genesisConfigProvider = genesisConfigProvider;
this.devMode = devMode; this.devMode = devMode;
@ -289,6 +293,10 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto
webSocketConfiguration().getHost() + ":" + webSocketConfiguration().getPort()); webSocketConfiguration().getHost() + ":" + webSocketConfiguration().getPort());
} }
MetricsConfiguration metricsConfiguration() {
return metricsConfiguration;
}
int p2pPort() { int p2pPort() {
return p2pPort; return p2pPort;
} }

@ -86,6 +86,7 @@ public class ThreadPantheonNodeRunner implements PantheonNodeRunner {
.dataDir(node.homeDirectory()) .dataDir(node.homeDirectory())
.bannedNodeIds(Collections.emptySet()) .bannedNodeIds(Collections.emptySet())
.metricsSystem(noOpMetricsSystem) .metricsSystem(noOpMetricsSystem)
.metricsConfiguration(node.metricsConfiguration())
.permissioningConfiguration(node.getPermissioningConfiguration()) .permissioningConfiguration(node.getPermissioningConfiguration())
.build(); .build();

@ -16,6 +16,7 @@ import tech.pegasys.pantheon.ethereum.core.MiningParameters;
import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.tests.acceptance.dsl.node.GenesisConfigProvider; import tech.pegasys.pantheon.tests.acceptance.dsl.node.GenesisConfigProvider;
class PantheonFactoryConfiguration { class PantheonFactoryConfiguration {
@ -24,6 +25,7 @@ class PantheonFactoryConfiguration {
private final MiningParameters miningParameters; private final MiningParameters miningParameters;
private final JsonRpcConfiguration jsonRpcConfiguration; private final JsonRpcConfiguration jsonRpcConfiguration;
private final WebSocketConfiguration webSocketConfiguration; private final WebSocketConfiguration webSocketConfiguration;
private final MetricsConfiguration metricsConfiguration;
private final PermissioningConfiguration permissioningConfiguration; private final PermissioningConfiguration permissioningConfiguration;
private final boolean devMode; private final boolean devMode;
private final GenesisConfigProvider genesisConfigProvider; private final GenesisConfigProvider genesisConfigProvider;
@ -33,6 +35,7 @@ class PantheonFactoryConfiguration {
final MiningParameters miningParameters, final MiningParameters miningParameters,
final JsonRpcConfiguration jsonRpcConfiguration, final JsonRpcConfiguration jsonRpcConfiguration,
final WebSocketConfiguration webSocketConfiguration, final WebSocketConfiguration webSocketConfiguration,
final MetricsConfiguration metricsConfiguration,
final PermissioningConfiguration permissioningConfiguration, final PermissioningConfiguration permissioningConfiguration,
final boolean devMode, final boolean devMode,
final GenesisConfigProvider genesisConfigProvider) { final GenesisConfigProvider genesisConfigProvider) {
@ -40,6 +43,7 @@ class PantheonFactoryConfiguration {
this.miningParameters = miningParameters; this.miningParameters = miningParameters;
this.jsonRpcConfiguration = jsonRpcConfiguration; this.jsonRpcConfiguration = jsonRpcConfiguration;
this.webSocketConfiguration = webSocketConfiguration; this.webSocketConfiguration = webSocketConfiguration;
this.metricsConfiguration = metricsConfiguration;
this.permissioningConfiguration = permissioningConfiguration; this.permissioningConfiguration = permissioningConfiguration;
this.devMode = devMode; this.devMode = devMode;
this.genesisConfigProvider = genesisConfigProvider; this.genesisConfigProvider = genesisConfigProvider;
@ -61,6 +65,10 @@ class PantheonFactoryConfiguration {
return webSocketConfiguration; return webSocketConfiguration;
} }
public MetricsConfiguration getMetricsConfiguration() {
return metricsConfiguration;
}
public PermissioningConfiguration getPermissioningConfiguration() { public PermissioningConfiguration getPermissioningConfiguration() {
return permissioningConfiguration; return permissioningConfiguration;
} }

@ -19,6 +19,7 @@ import tech.pegasys.pantheon.ethereum.core.MiningParametersTestBuilder;
import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.tests.acceptance.dsl.node.GenesisConfigProvider; import tech.pegasys.pantheon.tests.acceptance.dsl.node.GenesisConfigProvider;
import java.util.Optional; import java.util.Optional;
@ -30,6 +31,7 @@ public class PantheonFactoryConfigurationBuilder {
new MiningParametersTestBuilder().enabled(false).build(); new MiningParametersTestBuilder().enabled(false).build();
private JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); private JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault();
private WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); private WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
private MetricsConfiguration metricsConfiguration = MetricsConfiguration.createDefault();
private PermissioningConfiguration permissioningConfiguration = private PermissioningConfiguration permissioningConfiguration =
PermissioningConfiguration.createDefault(); PermissioningConfiguration.createDefault();
private boolean devMode = true; private boolean devMode = true;
@ -73,6 +75,12 @@ public class PantheonFactoryConfigurationBuilder {
return this; return this;
} }
public PantheonFactoryConfigurationBuilder setMetricsConfiguration(
final MetricsConfiguration metricsConfiguration) {
this.metricsConfiguration = metricsConfiguration;
return this;
}
public PantheonFactoryConfigurationBuilder webSocketEnabled() { public PantheonFactoryConfigurationBuilder webSocketEnabled() {
final WebSocketConfiguration config = WebSocketConfiguration.createDefault(); final WebSocketConfiguration config = WebSocketConfiguration.createDefault();
config.setEnabled(true); config.setEnabled(true);
@ -105,6 +113,7 @@ public class PantheonFactoryConfigurationBuilder {
miningParameters, miningParameters,
jsonRpcConfiguration, jsonRpcConfiguration,
webSocketConfiguration, webSocketConfiguration,
metricsConfiguration,
permissioningConfiguration, permissioningConfiguration,
devMode, devMode,
genesisConfigProvider); genesisConfigProvider);

@ -48,6 +48,7 @@ public class PantheonNodeFactory {
config.getMiningParameters(), config.getMiningParameters(),
config.getJsonRpcConfiguration(), config.getJsonRpcConfiguration(),
config.getWebSocketConfiguration(), config.getWebSocketConfiguration(),
config.getMetricsConfiguration(),
config.getPermissioningConfiguration(), config.getPermissioningConfiguration(),
config.isDevMode(), config.isDevMode(),
config.getGenesisConfigProvider(), config.getGenesisConfigProvider(),

@ -36,6 +36,7 @@ dependencyManagement {
dependency 'io.pkts:pkts-core:3.0.3' dependency 'io.pkts:pkts-core:3.0.3'
dependency "io.prometheus:simpleclient:0.5.0" dependency "io.prometheus:simpleclient:0.5.0"
dependency "io.prometheus:simpleclient_common:0.5.0"
dependency "io.prometheus:simpleclient_hotspot:0.5.0" dependency "io.prometheus:simpleclient_hotspot:0.5.0"
dependency 'io.reactivex.rxjava2:rxjava:2.2.2' dependency 'io.reactivex.rxjava2:rxjava:2.2.2'

@ -26,15 +26,21 @@ jar {
} }
dependencies { dependencies {
implementation project(':util')
implementation 'com.google.guava:guava' implementation 'com.google.guava:guava'
implementation 'org.apache.logging.log4j:log4j-api'
implementation 'io.prometheus:simpleclient' implementation 'io.prometheus:simpleclient'
implementation 'io.prometheus:simpleclient_common'
implementation 'io.prometheus:simpleclient_hotspot' implementation 'io.prometheus:simpleclient_hotspot'
implementation 'io.vertx:vertx-core'
implementation 'io.vertx:vertx-web'
implementation 'org.apache.logging.log4j:log4j-api'
runtime 'org.apache.logging.log4j:log4j-core' runtime 'org.apache.logging.log4j:log4j-core'
// test dependencies. // test dependencies.
testImplementation 'junit:junit' testImplementation 'junit:junit'
testImplementation "org.mockito:mockito-core"
testImplementation 'org.assertj:assertj-core' testImplementation 'org.assertj:assertj-core'
testImplementation 'org.mockito:mockito-core'
testImplementation 'com.squareup.okhttp3:okhttp'
} }

@ -0,0 +1,104 @@
/*
* Copyright 2019 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 java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import com.google.common.collect.Lists;
public class MetricsConfiguration {
private static final String DEFAULT_METRICS_HOST = "127.0.0.1";
public static final int DEFAULT_METRICS_PORT = 9545;
private boolean enabled;
private int port;
private String host;
private Collection<String> hostsWhitelist = Collections.singletonList("localhost");
public static MetricsConfiguration createDefault() {
final MetricsConfiguration metricsConfiguration = new MetricsConfiguration();
metricsConfiguration.setEnabled(false);
metricsConfiguration.setPort(DEFAULT_METRICS_PORT);
metricsConfiguration.setHost(DEFAULT_METRICS_HOST);
return metricsConfiguration;
}
private MetricsConfiguration() {}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(final boolean enabled) {
this.enabled = enabled;
}
public int getPort() {
return port;
}
public void setPort(final int port) {
this.port = port;
}
public String getHost() {
return host;
}
public void setHost(final String host) {
this.host = host;
}
Collection<String> getHostsWhitelist() {
return Collections.unmodifiableCollection(this.hostsWhitelist);
}
public void setHostsWhitelist(final Collection<String> hostsWhitelist) {
this.hostsWhitelist = hostsWhitelist;
}
@Override
public String toString() {
return "MetricsConfiguration{"
+ "enabled="
+ enabled
+ ", port="
+ port
+ ", host='"
+ host
+ '\''
+ ", hostsWhitelist="
+ hostsWhitelist
+ '}';
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final MetricsConfiguration that = (MetricsConfiguration) o;
return enabled == that.enabled
&& port == that.port
&& Objects.equals(host, that.host)
&& com.google.common.base.Objects.equal(
Lists.newArrayList(hostsWhitelist), Lists.newArrayList(that.hostsWhitelist));
}
@Override
public int hashCode() {
return Objects.hash(enabled, port, host, hostsWhitelist);
}
}

@ -0,0 +1,231 @@
/*
* Copyright 2019 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 com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Streams.stream;
import static tech.pegasys.pantheon.util.NetworkUtility.urlForSocketAddress;
import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.util.NetworkUtility;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.prometheus.client.exporter.common.TextFormat;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class MetricsHttpService {
private static final Logger LOG = LogManager.getLogger();
private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0);
private final Vertx vertx;
private final MetricsConfiguration config;
private final MetricsSystem metricsSystem;
private HttpServer httpServer;
public MetricsHttpService(
final Vertx vertx,
final MetricsConfiguration configuration,
final MetricsSystem metricsSystem) {
validateConfig(configuration);
this.vertx = vertx;
this.config = configuration;
this.metricsSystem = metricsSystem;
}
private void validateConfig(final MetricsConfiguration config) {
checkArgument(
config.getPort() == 0 || NetworkUtility.isValidPort(config.getPort()),
"Invalid port configuration.");
checkArgument(config.getHost() != null, "Required host is not configured.");
}
public CompletableFuture<?> start() {
LOG.info("Starting JsonRPC service on {}:{}", config.getHost(), config.getPort());
// Create the HTTP server and a router object.
httpServer =
vertx.createHttpServer(
new HttpServerOptions().setHost(config.getHost()).setPort(config.getPort()));
// Handle json rpc requests
final Router router = Router.router(vertx);
// Verify Host header to avoid rebind attack.
router.route().handler(checkWhitelistHostHeader());
router.route("/").method(HttpMethod.GET).handler(this::handleEmptyRequest);
router
.route("/metrics")
.method(HttpMethod.GET)
.produces(TextFormat.CONTENT_TYPE_004)
.handler(this::metricsRequest);
final CompletableFuture<?> resultFuture = new CompletableFuture<>();
httpServer
.requestHandler(router::accept)
.listen(
res -> {
if (!res.failed()) {
resultFuture.complete(null);
LOG.info(
"Metrics service started and listening on {}:{}",
config.getHost(),
httpServer.actualPort());
return;
}
httpServer = null;
final Throwable cause = res.cause();
if (cause instanceof SocketException) {
resultFuture.completeExceptionally(
new RuntimeException(
String.format(
"Failed to bind metrics listener to %s:%s: %s",
config.getHost(), config.getPort(), cause.getMessage())));
return;
}
resultFuture.completeExceptionally(cause);
});
return resultFuture;
}
private Handler<RoutingContext> checkWhitelistHostHeader() {
return event -> {
final Optional<String> hostHeader = getAndValidateHostHeader(event);
if (config.getHostsWhitelist().contains("*")
|| (hostHeader.isPresent() && hostIsInWhitelist(hostHeader.get()))) {
event.next();
} else {
event
.response()
.setStatusCode(403)
.putHeader("Content-Type", "text/plain; charset=utf-8")
.end("Host not authorized.");
}
};
}
private Optional<String> getAndValidateHostHeader(final RoutingContext event) {
final Iterable<String> splitHostHeader = Splitter.on(':').split(event.request().host());
final long hostPieces = stream(splitHostHeader).count();
if (hostPieces > 1) {
// If the host contains a colon, verify the host is correctly formed - host [ ":" port ]
if (hostPieces > 2 || !Iterables.get(splitHostHeader, 1).matches("\\d{1,5}+")) {
return Optional.empty();
}
}
return Optional.ofNullable(Iterables.get(splitHostHeader, 0));
}
private boolean hostIsInWhitelist(final String hostHeader) {
return config
.getHostsWhitelist()
.stream()
.anyMatch(whitelistEntry -> whitelistEntry.toLowerCase().equals(hostHeader.toLowerCase()));
}
public CompletableFuture<?> stop() {
if (httpServer == null) {
return CompletableFuture.completedFuture(null);
}
final CompletableFuture<?> resultFuture = new CompletableFuture<>();
httpServer.close(
res -> {
if (res.failed()) {
resultFuture.completeExceptionally(res.cause());
} else {
httpServer = null;
resultFuture.complete(null);
}
});
return resultFuture;
}
private void metricsRequest(final RoutingContext routingContext) {
final Set<String> names = new TreeSet<>(routingContext.queryParam("name[]"));
final HttpServerResponse response = routingContext.response();
vertx.<String>executeBlocking(
future -> {
try {
final ByteArrayOutputStream metrics = new ByteArrayOutputStream(16 * 1024);
final OutputStreamWriter osw = new OutputStreamWriter(metrics, StandardCharsets.UTF_8);
TextFormat.write004(
osw,
((PrometheusMetricsSystem) (metricsSystem))
.getRegistry()
.filteredMetricFamilySamples(names));
osw.flush();
osw.close();
metrics.flush();
metrics.close();
future.complete(metrics.toString(StandardCharsets.UTF_8.name()));
} catch (final IOException ioe) {
future.fail(ioe);
}
},
false,
(res) -> {
if (res.failed()) {
response.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end();
} else {
response.setStatusCode(HttpResponseStatus.OK.code());
response.putHeader("Content-Type", TextFormat.CONTENT_TYPE_004);
response.end(res.result());
}
});
}
public InetSocketAddress socketAddress() {
if (httpServer == null) {
return EMPTY_SOCKET_ADDRESS;
}
return new InetSocketAddress(config.getHost(), httpServer.actualPort());
}
@VisibleForTesting
public String url() {
if (httpServer == null) {
return "";
}
return urlForSocketAddress("http", socketAddress());
}
// Facilitate remote health-checks in AWS, inter alia.
private void handleEmptyRequest(final RoutingContext routingContext) {
routingContext.response().setStatusCode(201).end();
}
}

@ -34,6 +34,7 @@ import io.prometheus.client.Collector;
import io.prometheus.client.Collector.MetricFamilySamples; import io.prometheus.client.Collector.MetricFamilySamples;
import io.prometheus.client.Collector.MetricFamilySamples.Sample; import io.prometheus.client.Collector.MetricFamilySamples.Sample;
import io.prometheus.client.Collector.Type; import io.prometheus.client.Collector.Type;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Counter; import io.prometheus.client.Counter;
import io.prometheus.client.Summary; import io.prometheus.client.Summary;
import io.prometheus.client.hotspot.BufferPoolsExports; import io.prometheus.client.hotspot.BufferPoolsExports;
@ -47,20 +48,22 @@ public class PrometheusMetricsSystem implements MetricsSystem {
private static final String PANTHEON_PREFIX = "pantheon_"; private static final String PANTHEON_PREFIX = "pantheon_";
private final Map<MetricCategory, Collection<Collector>> collectors = new ConcurrentHashMap<>(); private final Map<MetricCategory, Collection<Collector>> collectors = new ConcurrentHashMap<>();
private final CollectorRegistry registry = new CollectorRegistry(true);
PrometheusMetricsSystem() {} PrometheusMetricsSystem() {}
public static MetricsSystem init() { public static MetricsSystem init() {
final PrometheusMetricsSystem metricsSystem = new PrometheusMetricsSystem(); final PrometheusMetricsSystem metricsSystem = new PrometheusMetricsSystem();
metricsSystem.collectors.put(MetricCategory.PROCESS, singleton(new StandardExports())); metricsSystem.collectors.put(
MetricCategory.PROCESS, singleton(new StandardExports().register(metricsSystem.registry)));
metricsSystem.collectors.put( metricsSystem.collectors.put(
MetricCategory.JVM, MetricCategory.JVM,
asList( asList(
new MemoryPoolsExports(), new MemoryPoolsExports().register(metricsSystem.registry),
new BufferPoolsExports(), new BufferPoolsExports().register(metricsSystem.registry),
new GarbageCollectorExports(), new GarbageCollectorExports().register(metricsSystem.registry),
new ThreadExports(), new ThreadExports().register(metricsSystem.registry),
new ClassLoadingExports())); new ClassLoadingExports().register(metricsSystem.registry)));
return metricsSystem; return metricsSystem;
} }
@ -108,10 +111,11 @@ public class PrometheusMetricsSystem implements MetricsSystem {
addCollector(category, new CurrentValueCollector(metricName, help, valueSupplier)); addCollector(category, new CurrentValueCollector(metricName, help, valueSupplier));
} }
private void addCollector(final MetricCategory category, final Collector counter) { private void addCollector(final MetricCategory category, final Collector metric) {
metric.register(registry);
collectors collectors
.computeIfAbsent(category, key -> Collections.newSetFromMap(new ConcurrentHashMap<>())) .computeIfAbsent(category, key -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
.add(counter); .add(metric);
} }
@Override @Override
@ -192,4 +196,8 @@ public class PrometheusMetricsSystem implements MetricsSystem {
? PANTHEON_PREFIX + category.getName() + "_" ? PANTHEON_PREFIX + category.getName() + "_"
: category.getName() + "_"; : category.getName() + "_";
} }
CollectorRegistry getRegistry() {
return registry;
}
} }

@ -0,0 +1,175 @@
/*
* 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 org.assertj.core.api.Assertions.assertThat;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.Properties;
import io.vertx.core.Vertx;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class MetricsHttpServiceTest {
@ClassRule public static final TemporaryFolder folder = new TemporaryFolder();
private static final Vertx vertx = Vertx.vertx();
private static MetricsHttpService service;
private static OkHttpClient client;
private static String baseUrl;
@BeforeClass
public static void initServerAndClient() {
service = createMetricsHttpService();
service.start().join();
// Build an OkHttp client.
client = new OkHttpClient();
baseUrl = service.url();
}
private static MetricsHttpService createMetricsHttpService(final MetricsConfiguration config) {
return new MetricsHttpService(vertx, config, PrometheusMetricsSystem.init());
}
private static MetricsHttpService createMetricsHttpService() {
return new MetricsHttpService(vertx, createMetricsConfig(), PrometheusMetricsSystem.init());
}
private static MetricsConfiguration createMetricsConfig() {
final MetricsConfiguration config = MetricsConfiguration.createDefault();
config.setPort(0);
config.setHostsWhitelist(Collections.singletonList("*"));
return config;
}
/** Tears down the HTTP server. */
@AfterClass
public static void shutdownServer() {
service.stop().join();
}
@Test
public void invalidCallToStart() {
service
.start()
.whenComplete(
(unused, exception) -> assertThat(exception).isInstanceOf(IllegalStateException.class));
}
@Test
public void http404() throws Exception {
try (final Response resp = client.newCall(buildGetRequest("/foo")).execute()) {
assertThat(resp.code()).isEqualTo(404);
}
}
@Test
public void handleEmptyRequest() throws Exception {
try (final Response resp = client.newCall(buildGetRequest("")).execute()) {
assertThat(resp.code()).isEqualTo(201);
}
}
@Test
public void getSocketAddressWhenActive() {
final InetSocketAddress socketAddress = service.socketAddress();
assertThat("127.0.0.1").isEqualTo(socketAddress.getAddress().getHostAddress());
assertThat(socketAddress.getPort() > 0).isTrue();
}
@Test
public void getSocketAddressWhenStoppedIsEmpty() {
final MetricsHttpService service = createMetricsHttpService();
final InetSocketAddress socketAddress = service.socketAddress();
assertThat("0.0.0.0").isEqualTo(socketAddress.getAddress().getHostAddress());
assertThat(0).isEqualTo(socketAddress.getPort());
assertThat("").isEqualTo(service.url());
}
@Test
public void getSocketAddressWhenBindingToAllInterfaces() {
final MetricsConfiguration config = createMetricsConfig();
config.setHost("0.0.0.0");
final MetricsHttpService service = createMetricsHttpService(config);
service.start().join();
try {
final InetSocketAddress socketAddress = service.socketAddress();
assertThat("0.0.0.0").isEqualTo(socketAddress.getAddress().getHostAddress());
assertThat(socketAddress.getPort() > 0).isTrue();
assertThat(!service.url().contains("0.0.0.0")).isTrue();
} finally {
service.stop().join();
}
}
@Test
public void metricsArePresent() throws Exception {
final Request metricsRequest = new Request.Builder().url(baseUrl + "/metrics").build();
try (final Response resp = client.newCall(metricsRequest).execute()) {
assertThat(resp.code()).isEqualTo(200);
// Check general format of result, it maps to java.util.Properties
final Properties props = new Properties();
props.load(resp.body().byteStream());
// We should have JVM metrics already loaded, verify a simple key.
assertThat(props).containsKey("jvm_threads_deadlocked");
}
}
@Test
public void metricsArePresentWhenFiltered() throws Exception {
final Request metricsRequest =
new Request.Builder().url(baseUrl + "/metrics?name[]=jvm_threads_deadlocked").build();
try (final Response resp = client.newCall(metricsRequest).execute()) {
assertThat(resp.code()).isEqualTo(200);
// Check general format of result, it maps to java.util.Properties
final Properties props = new Properties();
props.load(resp.body().byteStream());
// We should have JVM metrics already loaded, verify a simple key.
assertThat(props).containsKey("jvm_threads_deadlocked");
}
}
@Test
public void metricsAreAbsentWhenFiltered() throws Exception {
final Request metricsRequest =
new Request.Builder().url(baseUrl + "/metrics?name[]=does_not_exist").build();
try (final Response resp = client.newCall(metricsRequest).execute()) {
assertThat(resp.code()).isEqualTo(200);
// Check general format of result, it maps to java.util.Properties
final Properties props = new Properties();
props.load(resp.body().byteStream());
// We should have JVM metrics already loaded, verify a simple key.
assertThat(props).isEmpty();
}
}
private Request buildGetRequest(final String path) {
return new Request.Builder().get().url(baseUrl + path).build();
}
}

@ -16,6 +16,7 @@ import tech.pegasys.pantheon.controller.PantheonController;
import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcHttpService; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcHttpService;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketService; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketService;
import tech.pegasys.pantheon.ethereum.p2p.NetworkRunner; import tech.pegasys.pantheon.ethereum.p2p.NetworkRunner;
import tech.pegasys.pantheon.metrics.prometheus.MetricsHttpService;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -42,6 +43,7 @@ public class Runner implements AutoCloseable {
private final Optional<JsonRpcHttpService> jsonRpc; private final Optional<JsonRpcHttpService> jsonRpc;
private final Optional<WebSocketService> websocketRpc; private final Optional<WebSocketService> websocketRpc;
private final Optional<MetricsHttpService> metrics;
private final PantheonController<?> pantheonController; private final PantheonController<?> pantheonController;
private final Path dataDir; private final Path dataDir;
@ -51,12 +53,14 @@ public class Runner implements AutoCloseable {
final NetworkRunner networkRunner, final NetworkRunner networkRunner,
final Optional<JsonRpcHttpService> jsonRpc, final Optional<JsonRpcHttpService> jsonRpc,
final Optional<WebSocketService> websocketRpc, final Optional<WebSocketService> websocketRpc,
final Optional<MetricsHttpService> metrics,
final PantheonController<?> pantheonController, final PantheonController<?> pantheonController,
final Path dataDir) { final Path dataDir) {
this.vertx = vertx; this.vertx = vertx;
this.networkRunner = networkRunner; this.networkRunner = networkRunner;
this.jsonRpc = jsonRpc; this.jsonRpc = jsonRpc;
this.websocketRpc = websocketRpc; this.websocketRpc = websocketRpc;
this.metrics = metrics;
this.pantheonController = pantheonController; this.pantheonController = pantheonController;
this.dataDir = dataDir; this.dataDir = dataDir;
} }
@ -68,6 +72,7 @@ public class Runner implements AutoCloseable {
pantheonController.getSynchronizer().start(); pantheonController.getSynchronizer().start();
jsonRpc.ifPresent(service -> service.start().join()); jsonRpc.ifPresent(service -> service.start().join());
websocketRpc.ifPresent(service -> service.start().join()); websocketRpc.ifPresent(service -> service.start().join());
metrics.ifPresent(service -> service.start().join());
LOG.info("Ethereum main loop is up."); LOG.info("Ethereum main loop is up.");
writePantheonPortsToFile(); writePantheonPortsToFile();
networkRunner.awaitStop(); networkRunner.awaitStop();
@ -88,6 +93,7 @@ public class Runner implements AutoCloseable {
try { try {
jsonRpc.ifPresent(service -> service.stop().join()); jsonRpc.ifPresent(service -> service.stop().join());
websocketRpc.ifPresent(service -> service.stop().join()); websocketRpc.ifPresent(service -> service.stop().join());
metrics.ifPresent(service -> service.stop().join());
} finally { } finally {
try { try {
exec.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); exec.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
@ -113,6 +119,9 @@ public class Runner implements AutoCloseable {
if (getWebsocketPort().isPresent()) { if (getWebsocketPort().isPresent()) {
properties.setProperty("ws-rpc", String.valueOf(getWebsocketPort().get())); properties.setProperty("ws-rpc", String.valueOf(getWebsocketPort().get()));
} }
if (getMetricsPort().isPresent()) {
properties.setProperty("metrics", String.valueOf(getMetricsPort().get()));
}
final File portsFile = new File(dataDir.toFile(), "pantheon.ports"); final File portsFile = new File(dataDir.toFile(), "pantheon.ports");
portsFile.deleteOnExit(); portsFile.deleteOnExit();
@ -134,6 +143,10 @@ public class Runner implements AutoCloseable {
return websocketRpc.map(service -> service.socketAddress().getPort()); return websocketRpc.map(service -> service.socketAddress().getPort());
} }
public Optional<Integer> getMetricsPort() {
return metrics.map(service -> service.socketAddress().getPort());
}
public int getP2pUdpPort() { public int getP2pUdpPort() {
return networkRunner.getNetwork().getDiscoverySocketAddress().getPort(); return networkRunner.getNetwork().getDiscoverySocketAddress().getPort();
} }

@ -54,6 +54,8 @@ import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol;
import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsHttpService;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.nio.file.Path; import java.nio.file.Path;
@ -80,6 +82,7 @@ public class RunnerBuilder {
private WebSocketConfiguration webSocketConfiguration; private WebSocketConfiguration webSocketConfiguration;
private Path dataDir; private Path dataDir;
private Collection<String> bannedNodeIds; private Collection<String> bannedNodeIds;
private MetricsConfiguration metricsConfiguration;
private MetricsSystem metricsSystem; private MetricsSystem metricsSystem;
private PermissioningConfiguration permissioningConfiguration; private PermissioningConfiguration permissioningConfiguration;
@ -144,6 +147,11 @@ public class RunnerBuilder {
return this; return this;
} }
public RunnerBuilder metricsConfiguration(final MetricsConfiguration metricsConfiguration) {
this.metricsConfiguration = metricsConfiguration;
return this;
}
public RunnerBuilder metricsSystem(final MetricsSystem metricsSystem) { public RunnerBuilder metricsSystem(final MetricsSystem metricsSystem) {
this.metricsSystem = metricsSystem; this.metricsSystem = metricsSystem;
return this; return this;
@ -285,8 +293,19 @@ public class RunnerBuilder {
vertx, webSocketConfiguration, subscriptionManager, webSocketsJsonRpcMethods)); vertx, webSocketConfiguration, subscriptionManager, webSocketsJsonRpcMethods));
} }
Optional<MetricsHttpService> metricsService = Optional.empty();
if (metricsConfiguration.isEnabled()) {
metricsService = Optional.of(createMetricsService(vertx, metricsConfiguration));
}
return new Runner( return new Runner(
vertx, networkRunner, jsonRpcHttpService, webSocketService, pantheonController, dataDir); vertx,
networkRunner,
jsonRpcHttpService,
webSocketService,
metricsService,
pantheonController,
dataDir);
} }
private FilterManager createFilterManager( private FilterManager createFilterManager(
@ -382,4 +401,9 @@ public class RunnerBuilder {
return new WebSocketService(vertx, configuration, websocketRequestHandler); return new WebSocketService(vertx, configuration, websocketRequestHandler);
} }
private MetricsHttpService createMetricsService(
final Vertx vertx, final MetricsConfiguration configuration) {
return new MetricsHttpService(vertx, configuration, metricsSystem);
}
} }

@ -40,6 +40,7 @@ import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.ethereum.util.InvalidConfigurationException; import tech.pegasys.pantheon.ethereum.util.InvalidConfigurationException;
import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem; import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem;
import tech.pegasys.pantheon.util.BlockImporter; import tech.pegasys.pantheon.util.BlockImporter;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
@ -342,6 +343,21 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
this.refreshDelay = refreshDelay; this.refreshDelay = refreshDelay;
} }
@Option(
names = {"--metrics-enabled"},
description = "Set if the metrics exporter should be started (default: ${DEFAULT-VALUE})"
)
private final Boolean isMetricsEnabled = false;
@Option(
names = {"--metrics-listen"},
paramLabel = MANDATORY_HOST_AND_PORT_FORMAT_HELP,
description = "Host and port for the metrics exporter to listen on (default: ${DEFAULT-VALUE})",
arity = "1"
)
private final HostAndPort metricsHostAndPort =
getDefaultHostAndPort(MetricsConfiguration.DEFAULT_METRICS_PORT);
@Option( @Option(
names = {"--host-whitelist"}, names = {"--host-whitelist"},
paramLabel = "<hostname>", paramLabel = "<hostname>",
@ -501,6 +517,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
p2pHostAndPort, p2pHostAndPort,
jsonRpcConfiguration(), jsonRpcConfiguration(),
webSocketConfiguration(), webSocketConfiguration(),
metricsConfiguration(),
permissioningConfiguration); permissioningConfiguration);
} }
@ -578,6 +595,15 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
return webSocketConfiguration; return webSocketConfiguration;
} }
private MetricsConfiguration metricsConfiguration() {
final MetricsConfiguration metricsConfiguration = MetricsConfiguration.createDefault();
metricsConfiguration.setEnabled(isMetricsEnabled);
metricsConfiguration.setHost(metricsHostAndPort.getHost());
metricsConfiguration.setPort(metricsHostAndPort.getPort());
metricsConfiguration.setHostsWhitelist(hostsWhitelist.hostnamesWhitelist());
return metricsConfiguration;
}
private PermissioningConfiguration permissioningConfiguration() { private PermissioningConfiguration permissioningConfiguration() {
final PermissioningConfiguration permissioningConfiguration = final PermissioningConfiguration permissioningConfiguration =
PermissioningConfiguration.createDefault(); PermissioningConfiguration.createDefault();
@ -602,6 +628,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
final HostAndPort discoveryHostAndPort, final HostAndPort discoveryHostAndPort,
final JsonRpcConfiguration jsonRpcConfiguration, final JsonRpcConfiguration jsonRpcConfiguration,
final WebSocketConfiguration webSocketConfiguration, final WebSocketConfiguration webSocketConfiguration,
final MetricsConfiguration metricsConfiguration,
final PermissioningConfiguration permissioningConfiguration) { final PermissioningConfiguration permissioningConfiguration) {
checkNotNull(runnerBuilder); checkNotNull(runnerBuilder);
@ -621,6 +648,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
.dataDir(dataDir()) .dataDir(dataDir())
.bannedNodeIds(bannedNodeIds) .bannedNodeIds(bannedNodeIds)
.metricsSystem(metricsSystem) .metricsSystem(metricsSystem)
.metricsConfiguration(metricsConfiguration)
.permissioningConfiguration(permissioningConfiguration) .permissioningConfiguration(permissioningConfiguration)
.build(); .build();

@ -39,6 +39,7 @@ import tech.pegasys.pantheon.ethereum.storage.StorageProvider;
import tech.pegasys.pantheon.ethereum.storage.keyvalue.RocksDbStorageProvider; import tech.pegasys.pantheon.ethereum.storage.keyvalue.RocksDbStorageProvider;
import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.util.uint.UInt256; import tech.pegasys.pantheon.util.uint.UInt256;
import java.io.IOException; import java.io.IOException;
@ -124,6 +125,7 @@ public final class RunnerTest {
final ExecutorService executorService = Executors.newFixedThreadPool(2); final ExecutorService executorService = Executors.newFixedThreadPool(2);
final JsonRpcConfiguration aheadJsonRpcConfiguration = jsonRpcConfiguration(); final JsonRpcConfiguration aheadJsonRpcConfiguration = jsonRpcConfiguration();
final WebSocketConfiguration aheadWebSocketConfiguration = wsRpcConfiguration(); final WebSocketConfiguration aheadWebSocketConfiguration = wsRpcConfiguration();
final MetricsConfiguration aheadMetricsConfiguration = metricsConfiguration();
final PermissioningConfiguration aheadPermissioningConfiguration = permissioningConfiguration(); final PermissioningConfiguration aheadPermissioningConfiguration = permissioningConfiguration();
final RunnerBuilder runnerBuilder = final RunnerBuilder runnerBuilder =
new RunnerBuilder() new RunnerBuilder()
@ -141,6 +143,7 @@ public final class RunnerTest {
.bootstrapPeers(Collections.emptyList()) .bootstrapPeers(Collections.emptyList())
.jsonRpcConfiguration(aheadJsonRpcConfiguration) .jsonRpcConfiguration(aheadJsonRpcConfiguration)
.webSocketConfiguration(aheadWebSocketConfiguration) .webSocketConfiguration(aheadWebSocketConfiguration)
.metricsConfiguration(aheadMetricsConfiguration)
.dataDir(dbAhead) .dataDir(dbAhead)
.permissioningConfiguration(aheadPermissioningConfiguration) .permissioningConfiguration(aheadPermissioningConfiguration)
.build(); .build();
@ -149,6 +152,7 @@ public final class RunnerTest {
executorService.submit(runnerAhead::execute); executorService.submit(runnerAhead::execute);
final JsonRpcConfiguration behindJsonRpcConfiguration = jsonRpcConfiguration(); final JsonRpcConfiguration behindJsonRpcConfiguration = jsonRpcConfiguration();
final WebSocketConfiguration behindWebSocketConfiguration = wsRpcConfiguration(); final WebSocketConfiguration behindWebSocketConfiguration = wsRpcConfiguration();
final MetricsConfiguration behindMetricsConfiguration = metricsConfiguration();
// Setup runner with no block data // Setup runner with no block data
final PantheonController<Void> controllerBehind = final PantheonController<Void> controllerBehind =
@ -172,6 +176,7 @@ public final class RunnerTest {
runnerAhead.getP2pTcpPort()))) runnerAhead.getP2pTcpPort())))
.jsonRpcConfiguration(behindJsonRpcConfiguration) .jsonRpcConfiguration(behindJsonRpcConfiguration)
.webSocketConfiguration(behindWebSocketConfiguration) .webSocketConfiguration(behindWebSocketConfiguration)
.metricsConfiguration(behindMetricsConfiguration)
.dataDir(temp.newFolder().toPath()) .dataDir(temp.newFolder().toPath())
.metricsSystem(noOpMetricsSystem) .metricsSystem(noOpMetricsSystem)
.build(); .build();
@ -263,6 +268,13 @@ public final class RunnerTest {
return configuration; return configuration;
} }
private MetricsConfiguration metricsConfiguration() {
final MetricsConfiguration configuration = MetricsConfiguration.createDefault();
configuration.setPort(0);
configuration.setEnabled(false);
return configuration;
}
private PermissioningConfiguration permissioningConfiguration() { private PermissioningConfiguration permissioningConfiguration() {
return PermissioningConfiguration.createDefault(); return PermissioningConfiguration.createDefault();
} }

@ -23,6 +23,7 @@ import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration;
import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.util.BlockImporter; import tech.pegasys.pantheon.util.BlockImporter;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -73,6 +74,7 @@ public abstract class CommandTestAbstract {
@Captor ArgumentCaptor<Integer> intArgumentCaptor; @Captor ArgumentCaptor<Integer> intArgumentCaptor;
@Captor ArgumentCaptor<JsonRpcConfiguration> jsonRpcConfigArgumentCaptor; @Captor ArgumentCaptor<JsonRpcConfiguration> jsonRpcConfigArgumentCaptor;
@Captor ArgumentCaptor<WebSocketConfiguration> wsRpcConfigArgumentCaptor; @Captor ArgumentCaptor<WebSocketConfiguration> wsRpcConfigArgumentCaptor;
@Captor ArgumentCaptor<MetricsConfiguration> metricsConfigArgumentCaptor;
@Captor ArgumentCaptor<PermissioningConfiguration> permissioningConfigurationArgumentCaptor; @Captor ArgumentCaptor<PermissioningConfiguration> permissioningConfigurationArgumentCaptor;
@Captor ArgumentCaptor<Collection<URI>> uriListArgumentCaptor; @Captor ArgumentCaptor<Collection<URI>> uriListArgumentCaptor;

@ -37,6 +37,7 @@ import tech.pegasys.pantheon.ethereum.eth.sync.SyncMode;
import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration;
import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis; import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.io.File; import java.io.File;
@ -72,6 +73,7 @@ public class PantheonCommandTest extends CommandTestAbstract {
@Rule public final TemporaryFolder temp = new TemporaryFolder(); @Rule public final TemporaryFolder temp = new TemporaryFolder();
private static final JsonRpcConfiguration defaultJsonRpcConfiguration; private static final JsonRpcConfiguration defaultJsonRpcConfiguration;
private static final WebSocketConfiguration defaultWebSocketConfiguration; private static final WebSocketConfiguration defaultWebSocketConfiguration;
private static final MetricsConfiguration defaultMetricsConfiguration;
private static final String GENESIS_CONFIG_TESTDATA = "genesis_config"; private static final String GENESIS_CONFIG_TESTDATA = "genesis_config";
final String[] validENodeStrings = { final String[] validENodeStrings = {
@ -95,6 +97,9 @@ public class PantheonCommandTest extends CommandTestAbstract {
websocketConf.addRpcApi(CliqueRpcApis.CLIQUE); websocketConf.addRpcApi(CliqueRpcApis.CLIQUE);
websocketConf.addRpcApi(IbftRpcApis.IBFT); websocketConf.addRpcApi(IbftRpcApis.IBFT);
defaultWebSocketConfiguration = websocketConf; defaultWebSocketConfiguration = websocketConf;
final MetricsConfiguration metricsConf = MetricsConfiguration.createDefault();
defaultMetricsConfiguration = metricsConf;
} }
@Before @Before
@ -120,6 +125,7 @@ public class PantheonCommandTest extends CommandTestAbstract {
when(mockRunnerBuilder.dataDir(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.dataDir(any())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.metricsSystem(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.metricsSystem(any())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.metricsConfiguration(any())).thenReturn(mockRunnerBuilder);
when(mockRunnerBuilder.build()).thenReturn(mockRunner); when(mockRunnerBuilder.build()).thenReturn(mockRunner);
} }
@ -157,6 +163,7 @@ public class PantheonCommandTest extends CommandTestAbstract {
verify(mockRunnerBuilder).maxPeers(eq(25)); verify(mockRunnerBuilder).maxPeers(eq(25));
verify(mockRunnerBuilder).jsonRpcConfiguration(eq(defaultJsonRpcConfiguration)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(defaultJsonRpcConfiguration));
verify(mockRunnerBuilder).webSocketConfiguration(eq(defaultWebSocketConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(defaultWebSocketConfiguration));
verify(mockRunnerBuilder).metricsConfiguration(eq(defaultMetricsConfiguration));
verify(mockRunnerBuilder).build(); verify(mockRunnerBuilder).build();
final ArgumentCaptor<MiningParameters> miningArg = final ArgumentCaptor<MiningParameters> miningArg =
@ -290,6 +297,11 @@ public class PantheonCommandTest extends CommandTestAbstract {
webSocketConfiguration.addRpcApi(CliqueRpcApis.CLIQUE); webSocketConfiguration.addRpcApi(CliqueRpcApis.CLIQUE);
webSocketConfiguration.addRpcApi(IbftRpcApis.IBFT); webSocketConfiguration.addRpcApi(IbftRpcApis.IBFT);
final MetricsConfiguration metricsConfiguration = MetricsConfiguration.createDefault();
metricsConfiguration.setEnabled(false);
metricsConfiguration.setHost("8.6.7.5");
metricsConfiguration.setPort(309);
parseCommand("--config", toml.toString()); parseCommand("--config", toml.toString());
verify(mockRunnerBuilder).discovery(eq(false)); verify(mockRunnerBuilder).discovery(eq(false));
@ -299,6 +311,7 @@ public class PantheonCommandTest extends CommandTestAbstract {
verify(mockRunnerBuilder).maxPeers(eq(42)); verify(mockRunnerBuilder).maxPeers(eq(42));
verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration));
verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration));
verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration));
verify(mockRunnerBuilder).build(); verify(mockRunnerBuilder).build();
final Collection<URI> nodes = final Collection<URI> nodes =
@ -341,6 +354,8 @@ public class PantheonCommandTest extends CommandTestAbstract {
webSocketConfiguration.addRpcApi(CliqueRpcApis.CLIQUE); webSocketConfiguration.addRpcApi(CliqueRpcApis.CLIQUE);
webSocketConfiguration.addRpcApi(IbftRpcApis.IBFT); webSocketConfiguration.addRpcApi(IbftRpcApis.IBFT);
final MetricsConfiguration metricsConfiguration = MetricsConfiguration.createDefault();
verify(mockRunnerBuilder).discovery(eq(true)); verify(mockRunnerBuilder).discovery(eq(true));
verify(mockRunnerBuilder).bootstrapPeers(MAINNET_BOOTSTRAP_NODES); verify(mockRunnerBuilder).bootstrapPeers(MAINNET_BOOTSTRAP_NODES);
verify(mockRunnerBuilder).discoveryHost(eq("127.0.0.1")); verify(mockRunnerBuilder).discoveryHost(eq("127.0.0.1"));
@ -348,6 +363,7 @@ public class PantheonCommandTest extends CommandTestAbstract {
verify(mockRunnerBuilder).maxPeers(eq(25)); verify(mockRunnerBuilder).maxPeers(eq(25));
verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration));
verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration));
verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration));
verify(mockRunnerBuilder).build(); verify(mockRunnerBuilder).build();
verify(mockControllerBuilder).syncWithOttoman(eq(false)); verify(mockControllerBuilder).syncWithOttoman(eq(false));
@ -1011,6 +1027,48 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty();
} }
@Test
public void metricsEnabledPropertyDefaultIsFalse() {
parseCommand();
verify(mockRunnerBuilder).metricsConfiguration(metricsConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(metricsConfigArgumentCaptor.getValue().isEnabled()).isFalse();
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void metricsEnabledPropertyMustBeUsed() {
parseCommand("--metrics-enabled");
verify(mockRunnerBuilder).metricsConfiguration(metricsConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(metricsConfigArgumentCaptor.getValue().isEnabled()).isTrue();
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void metricsHostAndPortOptionMustBeUsed() {
final String host = "1.2.3.4";
final int port = 1234;
parseCommand("--metrics-listen", String.format("%1$s:%2$s", host, port));
verify(mockRunnerBuilder).metricsConfiguration(metricsConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(metricsConfigArgumentCaptor.getValue().getHost()).isEqualTo(host);
assertThat(metricsConfigArgumentCaptor.getValue().getPort()).isEqualTo(port);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test @Test
public void pantheonDoesNotStartInMiningModeIfCoinbaseNotSet() { public void pantheonDoesNotStartInMiningModeIfCoinbaseNotSet() {
parseCommand("--miner-enabled"); parseCommand("--miner-enabled");

@ -15,6 +15,7 @@ p2p-listen="1.2.3.4:1234" # IP:port
max-peers=42 max-peers=42
rpc-listen="5.6.7.8:5678" # IP:port rpc-listen="5.6.7.8:5678" # IP:port
ws-listen="9.10.11.12:9101" # IP:port ws-listen="9.10.11.12:9101" # IP:port
metrics-listen="8.6.7.5:309" # IP:port
# chain # chain
genesis="~/genesis.json" # Path genesis="~/genesis.json" # Path

Loading…
Cancel
Save