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

@ -86,6 +86,7 @@ public class ThreadPantheonNodeRunner implements PantheonNodeRunner {
.dataDir(node.homeDirectory())
.bannedNodeIds(Collections.emptySet())
.metricsSystem(noOpMetricsSystem)
.metricsConfiguration(node.metricsConfiguration())
.permissioningConfiguration(node.getPermissioningConfiguration())
.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.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;
}

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

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

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

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

@ -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.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<MetricCategory, Collection<Collector>> 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;
}
}

@ -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.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<JsonRpcHttpService> jsonRpc;
private final Optional<WebSocketService> websocketRpc;
private final Optional<MetricsHttpService> metrics;
private final PantheonController<?> pantheonController;
private final Path dataDir;
@ -51,12 +53,14 @@ public class Runner implements AutoCloseable {
final NetworkRunner networkRunner,
final Optional<JsonRpcHttpService> jsonRpc,
final Optional<WebSocketService> websocketRpc,
final Optional<MetricsHttpService> 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<Integer> getMetricsPort() {
return metrics.map(service -> service.socketAddress().getPort());
}
public int getP2pUdpPort() {
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.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<String> 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<MetricsHttpService> 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);
}
}

@ -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 = "<hostname>",
@ -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();

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

@ -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<Integer> intArgumentCaptor;
@Captor ArgumentCaptor<JsonRpcConfiguration> jsonRpcConfigArgumentCaptor;
@Captor ArgumentCaptor<WebSocketConfiguration> wsRpcConfigArgumentCaptor;
@Captor ArgumentCaptor<MetricsConfiguration> metricsConfigArgumentCaptor;
@Captor ArgumentCaptor<PermissioningConfiguration> permissioningConfigurationArgumentCaptor;
@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.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<MiningParameters> 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<URI> 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");

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

Loading…
Cancel
Save