diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java index ead225ad97..adcb466f4d 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java @@ -23,6 +23,7 @@ import tech.pegasys.pantheon.ethereum.core.Util; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; 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.transaction.PantheonWeb3j; 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 JsonRpcConfiguration jsonRpcConfiguration; private final WebSocketConfiguration webSocketConfiguration; + private final MetricsConfiguration metricsConfiguration; private final PermissioningConfiguration permissioningConfiguration; private final GenesisConfigProvider genesisConfigProvider; private final boolean devMode; @@ -82,6 +84,7 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto final MiningParameters miningParameters, final JsonRpcConfiguration jsonRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, + final MetricsConfiguration metricsConfiguration, final PermissioningConfiguration permissioningConfiguration, final boolean devMode, final GenesisConfigProvider genesisConfigProvider, @@ -94,6 +97,7 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto this.miningParameters = miningParameters; this.jsonRpcConfiguration = jsonRpcConfiguration; this.webSocketConfiguration = webSocketConfiguration; + this.metricsConfiguration = metricsConfiguration; this.permissioningConfiguration = permissioningConfiguration; this.genesisConfigProvider = genesisConfigProvider; this.devMode = devMode; @@ -289,6 +293,10 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto webSocketConfiguration().getHost() + ":" + webSocketConfiguration().getPort()); } + MetricsConfiguration metricsConfiguration() { + return metricsConfiguration; + } + int p2pPort() { return p2pPort; } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java index e164c6264b..a0fa9348a1 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java @@ -86,6 +86,7 @@ public class ThreadPantheonNodeRunner implements PantheonNodeRunner { .dataDir(node.homeDirectory()) .bannedNodeIds(Collections.emptySet()) .metricsSystem(noOpMetricsSystem) + .metricsConfiguration(node.metricsConfiguration()) .permissioningConfiguration(node.getPermissioningConfiguration()) .build(); diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfiguration.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfiguration.java index 9eae43e3fe..f9ce4d2af2 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfiguration.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfiguration.java @@ -16,6 +16,7 @@ import tech.pegasys.pantheon.ethereum.core.MiningParameters; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; +import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.tests.acceptance.dsl.node.GenesisConfigProvider; class PantheonFactoryConfiguration { @@ -24,6 +25,7 @@ class PantheonFactoryConfiguration { private final MiningParameters miningParameters; private final JsonRpcConfiguration jsonRpcConfiguration; private final WebSocketConfiguration webSocketConfiguration; + private final MetricsConfiguration metricsConfiguration; private final PermissioningConfiguration permissioningConfiguration; private final boolean devMode; private final GenesisConfigProvider genesisConfigProvider; @@ -33,6 +35,7 @@ class PantheonFactoryConfiguration { final MiningParameters miningParameters, final JsonRpcConfiguration jsonRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, + final MetricsConfiguration metricsConfiguration, final PermissioningConfiguration permissioningConfiguration, final boolean devMode, final GenesisConfigProvider genesisConfigProvider) { @@ -40,6 +43,7 @@ class PantheonFactoryConfiguration { this.miningParameters = miningParameters; this.jsonRpcConfiguration = jsonRpcConfiguration; this.webSocketConfiguration = webSocketConfiguration; + this.metricsConfiguration = metricsConfiguration; this.permissioningConfiguration = permissioningConfiguration; this.devMode = devMode; this.genesisConfigProvider = genesisConfigProvider; @@ -61,6 +65,10 @@ class PantheonFactoryConfiguration { return webSocketConfiguration; } + public MetricsConfiguration getMetricsConfiguration() { + return metricsConfiguration; + } + public PermissioningConfiguration getPermissioningConfiguration() { return permissioningConfiguration; } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java index 642c07f098..470598c6bc 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java @@ -19,6 +19,7 @@ import tech.pegasys.pantheon.ethereum.core.MiningParametersTestBuilder; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; +import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.tests.acceptance.dsl.node.GenesisConfigProvider; import java.util.Optional; @@ -30,6 +31,7 @@ public class PantheonFactoryConfigurationBuilder { new MiningParametersTestBuilder().enabled(false).build(); private JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); private WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); + private MetricsConfiguration metricsConfiguration = MetricsConfiguration.createDefault(); private PermissioningConfiguration permissioningConfiguration = PermissioningConfiguration.createDefault(); private boolean devMode = true; @@ -73,6 +75,12 @@ public class PantheonFactoryConfigurationBuilder { return this; } + public PantheonFactoryConfigurationBuilder setMetricsConfiguration( + final MetricsConfiguration metricsConfiguration) { + this.metricsConfiguration = metricsConfiguration; + return this; + } + public PantheonFactoryConfigurationBuilder webSocketEnabled() { final WebSocketConfiguration config = WebSocketConfiguration.createDefault(); config.setEnabled(true); @@ -105,6 +113,7 @@ public class PantheonFactoryConfigurationBuilder { miningParameters, jsonRpcConfiguration, webSocketConfiguration, + metricsConfiguration, permissioningConfiguration, devMode, genesisConfigProvider); diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java index f1eacd4a5d..a5cef58fad 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java @@ -48,6 +48,7 @@ public class PantheonNodeFactory { config.getMiningParameters(), config.getJsonRpcConfiguration(), config.getWebSocketConfiguration(), + config.getMetricsConfiguration(), config.getPermissioningConfiguration(), config.isDevMode(), config.getGenesisConfigProvider(), diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 8867aad7da..9f86667f5a 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -36,6 +36,7 @@ dependencyManagement { dependency 'io.pkts:pkts-core:3.0.3' 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.reactivex.rxjava2:rxjava:2.2.2' diff --git a/metrics/build.gradle b/metrics/build.gradle index c137953b03..32c640a069 100644 --- a/metrics/build.gradle +++ b/metrics/build.gradle @@ -26,15 +26,21 @@ jar { } dependencies { + implementation project(':util') + implementation 'com.google.guava:guava' - implementation 'org.apache.logging.log4j:log4j-api' implementation 'io.prometheus:simpleclient' + implementation 'io.prometheus:simpleclient_common' 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' // test dependencies. testImplementation 'junit:junit' - testImplementation "org.mockito:mockito-core" testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' + testImplementation 'com.squareup.okhttp3:okhttp' } diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsConfiguration.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsConfiguration.java new file mode 100644 index 0000000000..9bb8d242af --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsConfiguration.java @@ -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 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 getHostsWhitelist() { + return Collections.unmodifiableCollection(this.hostsWhitelist); + } + + public void setHostsWhitelist(final Collection 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); + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpService.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpService.java new file mode 100644 index 0000000000..ec5f339031 --- /dev/null +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpService.java @@ -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 checkWhitelistHostHeader() { + return event -> { + final Optional 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 getAndValidateHostHeader(final RoutingContext event) { + final Iterable 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 names = new TreeSet<>(routingContext.queryParam("name[]")); + final HttpServerResponse response = routingContext.response(); + vertx.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(); + } +} diff --git a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java index 00ed5fd6f1..fef0ee052c 100644 --- a/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java +++ b/metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/PrometheusMetricsSystem.java @@ -34,6 +34,7 @@ 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.CollectorRegistry; import io.prometheus.client.Counter; import io.prometheus.client.Summary; import io.prometheus.client.hotspot.BufferPoolsExports; @@ -47,20 +48,22 @@ public class PrometheusMetricsSystem implements MetricsSystem { private static final String PANTHEON_PREFIX = "pantheon_"; private final Map> collectors = new ConcurrentHashMap<>(); + private final CollectorRegistry registry = new CollectorRegistry(true); PrometheusMetricsSystem() {} public static MetricsSystem init() { 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( MetricCategory.JVM, asList( - new MemoryPoolsExports(), - new BufferPoolsExports(), - new GarbageCollectorExports(), - new ThreadExports(), - new ClassLoadingExports())); + new MemoryPoolsExports().register(metricsSystem.registry), + new BufferPoolsExports().register(metricsSystem.registry), + new GarbageCollectorExports().register(metricsSystem.registry), + new ThreadExports().register(metricsSystem.registry), + new ClassLoadingExports().register(metricsSystem.registry))); return metricsSystem; } @@ -108,10 +111,11 @@ public class PrometheusMetricsSystem implements MetricsSystem { 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 .computeIfAbsent(category, key -> Collections.newSetFromMap(new ConcurrentHashMap<>())) - .add(counter); + .add(metric); } @Override @@ -192,4 +196,8 @@ public class PrometheusMetricsSystem implements MetricsSystem { ? PANTHEON_PREFIX + category.getName() + "_" : category.getName() + "_"; } + + CollectorRegistry getRegistry() { + return registry; + } } diff --git a/metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpServiceTest.java b/metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpServiceTest.java new file mode 100644 index 0000000000..e2ce7586d5 --- /dev/null +++ b/metrics/src/test/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpServiceTest.java @@ -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(); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java index 662aee5868..246ebca64a 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java @@ -16,6 +16,7 @@ import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcHttpService; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketService; import tech.pegasys.pantheon.ethereum.p2p.NetworkRunner; +import tech.pegasys.pantheon.metrics.prometheus.MetricsHttpService; import java.io.File; import java.io.FileOutputStream; @@ -42,6 +43,7 @@ public class Runner implements AutoCloseable { private final Optional jsonRpc; private final Optional websocketRpc; + private final Optional metrics; private final PantheonController pantheonController; private final Path dataDir; @@ -51,12 +53,14 @@ public class Runner implements AutoCloseable { final NetworkRunner networkRunner, final Optional jsonRpc, final Optional websocketRpc, + final Optional metrics, final PantheonController pantheonController, final Path dataDir) { this.vertx = vertx; this.networkRunner = networkRunner; this.jsonRpc = jsonRpc; this.websocketRpc = websocketRpc; + this.metrics = metrics; this.pantheonController = pantheonController; this.dataDir = dataDir; } @@ -68,6 +72,7 @@ public class Runner implements AutoCloseable { pantheonController.getSynchronizer().start(); jsonRpc.ifPresent(service -> service.start().join()); websocketRpc.ifPresent(service -> service.start().join()); + metrics.ifPresent(service -> service.start().join()); LOG.info("Ethereum main loop is up."); writePantheonPortsToFile(); networkRunner.awaitStop(); @@ -88,6 +93,7 @@ public class Runner implements AutoCloseable { try { jsonRpc.ifPresent(service -> service.stop().join()); websocketRpc.ifPresent(service -> service.stop().join()); + metrics.ifPresent(service -> service.stop().join()); } finally { try { exec.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); @@ -113,6 +119,9 @@ public class Runner implements AutoCloseable { if (getWebsocketPort().isPresent()) { 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"); portsFile.deleteOnExit(); @@ -134,6 +143,10 @@ public class Runner implements AutoCloseable { return websocketRpc.map(service -> service.socketAddress().getPort()); } + public Optional getMetricsPort() { + return metrics.map(service -> service.socketAddress().getPort()); + } + public int getP2pUdpPort() { return networkRunner.getNetwork().getDiscoverySocketAddress().getPort(); } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 709fcc77c1..7c9a15a9ae 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -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.PermissioningConfiguration; 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 java.nio.file.Path; @@ -80,6 +82,7 @@ public class RunnerBuilder { private WebSocketConfiguration webSocketConfiguration; private Path dataDir; private Collection bannedNodeIds; + private MetricsConfiguration metricsConfiguration; private MetricsSystem metricsSystem; private PermissioningConfiguration permissioningConfiguration; @@ -144,6 +147,11 @@ public class RunnerBuilder { return this; } + public RunnerBuilder metricsConfiguration(final MetricsConfiguration metricsConfiguration) { + this.metricsConfiguration = metricsConfiguration; + return this; + } + public RunnerBuilder metricsSystem(final MetricsSystem metricsSystem) { this.metricsSystem = metricsSystem; return this; @@ -285,8 +293,19 @@ public class RunnerBuilder { vertx, webSocketConfiguration, subscriptionManager, webSocketsJsonRpcMethods)); } + Optional metricsService = Optional.empty(); + if (metricsConfiguration.isEnabled()) { + metricsService = Optional.of(createMetricsService(vertx, metricsConfiguration)); + } + return new Runner( - vertx, networkRunner, jsonRpcHttpService, webSocketService, pantheonController, dataDir); + vertx, + networkRunner, + jsonRpcHttpService, + webSocketService, + metricsService, + pantheonController, + dataDir); } private FilterManager createFilterManager( @@ -382,4 +401,9 @@ public class RunnerBuilder { return new WebSocketService(vertx, configuration, websocketRequestHandler); } + + private MetricsHttpService createMetricsService( + final Vertx vertx, final MetricsConfiguration configuration) { + return new MetricsHttpService(vertx, configuration, metricsSystem); + } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index 2b9ad9a99f..5540eb2cfc 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -40,6 +40,7 @@ import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.util.InvalidConfigurationException; import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem; import tech.pegasys.pantheon.util.BlockImporter; import tech.pegasys.pantheon.util.bytes.BytesValue; @@ -342,6 +343,21 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { 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( names = {"--host-whitelist"}, paramLabel = "", @@ -501,6 +517,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { p2pHostAndPort, jsonRpcConfiguration(), webSocketConfiguration(), + metricsConfiguration(), permissioningConfiguration); } @@ -578,6 +595,15 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { 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() { final PermissioningConfiguration permissioningConfiguration = PermissioningConfiguration.createDefault(); @@ -602,6 +628,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { final HostAndPort discoveryHostAndPort, final JsonRpcConfiguration jsonRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, + final MetricsConfiguration metricsConfiguration, final PermissioningConfiguration permissioningConfiguration) { checkNotNull(runnerBuilder); @@ -621,6 +648,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { .dataDir(dataDir()) .bannedNodeIds(bannedNodeIds) .metricsSystem(metricsSystem) + .metricsConfiguration(metricsConfiguration) .permissioningConfiguration(permissioningConfiguration) .build(); diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java index 3ec242522b..56129232e7 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java @@ -39,6 +39,7 @@ import tech.pegasys.pantheon.ethereum.storage.StorageProvider; import tech.pegasys.pantheon.ethereum.storage.keyvalue.RocksDbStorageProvider; import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; +import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.util.uint.UInt256; import java.io.IOException; @@ -124,6 +125,7 @@ public final class RunnerTest { final ExecutorService executorService = Executors.newFixedThreadPool(2); final JsonRpcConfiguration aheadJsonRpcConfiguration = jsonRpcConfiguration(); final WebSocketConfiguration aheadWebSocketConfiguration = wsRpcConfiguration(); + final MetricsConfiguration aheadMetricsConfiguration = metricsConfiguration(); final PermissioningConfiguration aheadPermissioningConfiguration = permissioningConfiguration(); final RunnerBuilder runnerBuilder = new RunnerBuilder() @@ -141,6 +143,7 @@ public final class RunnerTest { .bootstrapPeers(Collections.emptyList()) .jsonRpcConfiguration(aheadJsonRpcConfiguration) .webSocketConfiguration(aheadWebSocketConfiguration) + .metricsConfiguration(aheadMetricsConfiguration) .dataDir(dbAhead) .permissioningConfiguration(aheadPermissioningConfiguration) .build(); @@ -149,6 +152,7 @@ public final class RunnerTest { executorService.submit(runnerAhead::execute); final JsonRpcConfiguration behindJsonRpcConfiguration = jsonRpcConfiguration(); final WebSocketConfiguration behindWebSocketConfiguration = wsRpcConfiguration(); + final MetricsConfiguration behindMetricsConfiguration = metricsConfiguration(); // Setup runner with no block data final PantheonController controllerBehind = @@ -172,6 +176,7 @@ public final class RunnerTest { runnerAhead.getP2pTcpPort()))) .jsonRpcConfiguration(behindJsonRpcConfiguration) .webSocketConfiguration(behindWebSocketConfiguration) + .metricsConfiguration(behindMetricsConfiguration) .dataDir(temp.newFolder().toPath()) .metricsSystem(noOpMetricsSystem) .build(); @@ -263,6 +268,13 @@ public final class RunnerTest { return configuration; } + private MetricsConfiguration metricsConfiguration() { + final MetricsConfiguration configuration = MetricsConfiguration.createDefault(); + configuration.setPort(0); + configuration.setEnabled(false); + return configuration; + } + private PermissioningConfiguration permissioningConfiguration() { return PermissioningConfiguration.createDefault(); } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index bda77602b3..9389118f6a 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -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.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; +import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.util.BlockImporter; import java.io.ByteArrayOutputStream; @@ -73,6 +74,7 @@ public abstract class CommandTestAbstract { @Captor ArgumentCaptor intArgumentCaptor; @Captor ArgumentCaptor jsonRpcConfigArgumentCaptor; @Captor ArgumentCaptor wsRpcConfigArgumentCaptor; + @Captor ArgumentCaptor metricsConfigArgumentCaptor; @Captor ArgumentCaptor permissioningConfigurationArgumentCaptor; @Captor ArgumentCaptor> uriListArgumentCaptor; diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index f6bb3f20df..276e38aae5 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -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.RpcApis; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.File; @@ -72,6 +73,7 @@ public class PantheonCommandTest extends CommandTestAbstract { @Rule public final TemporaryFolder temp = new TemporaryFolder(); private static final JsonRpcConfiguration defaultJsonRpcConfiguration; private static final WebSocketConfiguration defaultWebSocketConfiguration; + private static final MetricsConfiguration defaultMetricsConfiguration; private static final String GENESIS_CONFIG_TESTDATA = "genesis_config"; final String[] validENodeStrings = { @@ -95,6 +97,9 @@ public class PantheonCommandTest extends CommandTestAbstract { websocketConf.addRpcApi(CliqueRpcApis.CLIQUE); websocketConf.addRpcApi(IbftRpcApis.IBFT); defaultWebSocketConfiguration = websocketConf; + + final MetricsConfiguration metricsConf = MetricsConfiguration.createDefault(); + defaultMetricsConfiguration = metricsConf; } @Before @@ -120,6 +125,7 @@ public class PantheonCommandTest extends CommandTestAbstract { when(mockRunnerBuilder.dataDir(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.metricsSystem(any())).thenReturn(mockRunnerBuilder); + when(mockRunnerBuilder.metricsConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.build()).thenReturn(mockRunner); } @@ -157,6 +163,7 @@ public class PantheonCommandTest extends CommandTestAbstract { verify(mockRunnerBuilder).maxPeers(eq(25)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(defaultJsonRpcConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(defaultWebSocketConfiguration)); + verify(mockRunnerBuilder).metricsConfiguration(eq(defaultMetricsConfiguration)); verify(mockRunnerBuilder).build(); final ArgumentCaptor miningArg = @@ -290,6 +297,11 @@ public class PantheonCommandTest extends CommandTestAbstract { webSocketConfiguration.addRpcApi(CliqueRpcApis.CLIQUE); 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()); verify(mockRunnerBuilder).discovery(eq(false)); @@ -299,6 +311,7 @@ public class PantheonCommandTest extends CommandTestAbstract { verify(mockRunnerBuilder).maxPeers(eq(42)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); + verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration)); verify(mockRunnerBuilder).build(); final Collection nodes = @@ -341,6 +354,8 @@ public class PantheonCommandTest extends CommandTestAbstract { webSocketConfiguration.addRpcApi(CliqueRpcApis.CLIQUE); webSocketConfiguration.addRpcApi(IbftRpcApis.IBFT); + final MetricsConfiguration metricsConfiguration = MetricsConfiguration.createDefault(); + verify(mockRunnerBuilder).discovery(eq(true)); verify(mockRunnerBuilder).bootstrapPeers(MAINNET_BOOTSTRAP_NODES); verify(mockRunnerBuilder).discoveryHost(eq("127.0.0.1")); @@ -348,6 +363,7 @@ public class PantheonCommandTest extends CommandTestAbstract { verify(mockRunnerBuilder).maxPeers(eq(25)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); + verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration)); verify(mockRunnerBuilder).build(); verify(mockControllerBuilder).syncWithOttoman(eq(false)); @@ -1011,6 +1027,48 @@ public class PantheonCommandTest extends CommandTestAbstract { 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 public void pantheonDoesNotStartInMiningModeIfCoinbaseNotSet() { parseCommand("--miner-enabled"); diff --git a/pantheon/src/test/resources/complete_config.toml b/pantheon/src/test/resources/complete_config.toml index 83ff8818d9..52b483f848 100644 --- a/pantheon/src/test/resources/complete_config.toml +++ b/pantheon/src/test/resources/complete_config.toml @@ -15,6 +15,7 @@ p2p-listen="1.2.3.4:1234" # IP:port max-peers=42 rpc-listen="5.6.7.8:5678" # IP:port ws-listen="9.10.11.12:9101" # IP:port +metrics-listen="8.6.7.5:309" # IP:port # chain genesis="~/genesis.json" # Path