Metrics Prometheus Push Gateway Support (#638)

Sometimes metrics are hard to poll (docker containers with varying ip
addresses). Because of that the push gateway exists. This extends the
metrics system to support push or pull mode for metrics (but not both
at the same time).

Three new flags
`--metrics-mode=`<`push`|`pull`> - Whether we are in pull mode (the default) where
prometheus is expected to poll or push mode where pantheon pushes to
a push gateway.

`--metrics-push-interval=`<_integer_> the frequency, in seconds, between pushes to
the push gateway. Only relevant in push mode

`--metrics-prometheus-job=`<_string_> The name of the job to report in the push gateway

Also, `--metrics-host=` and `--metrics-port=` gain new meaning in push mode. Instead of the
server they are opening up it is the host and the port of the push gateway it should push to.

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Danno Ferrin 6 years ago committed by GitHub
parent c7cb0264f5
commit 319ee2095c
  1. 1
      gradle/versions.gradle
  2. 1
      metrics/build.gradle
  3. 33
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsConfiguration.java
  4. 23
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsHttpService.java
  5. 106
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsPushGatewayService.java
  6. 58
      metrics/src/main/java/tech/pegasys/pantheon/metrics/prometheus/MetricsService.java
  7. 12
      pantheon/src/main/java/tech/pegasys/pantheon/Runner.java
  8. 8
      pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java
  9. 14
      pantheon/src/main/java/tech/pegasys/pantheon/cli/BlocksSubCommand.java
  10. 26
      pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java
  11. 40
      pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java
  12. 3
      pantheon/src/test/resources/everything_config.toml

@ -38,6 +38,7 @@ dependencyManagement {
dependency "io.prometheus:simpleclient:0.6.0"
dependency "io.prometheus:simpleclient_common:0.6.0"
dependency "io.prometheus:simpleclient_hotspot:0.6.0"
dependency "io.prometheus:simpleclient_pushgateway:0.6.0"
dependency 'io.reactivex.rxjava2:rxjava:2.2.5'

@ -32,6 +32,7 @@ dependencies {
implementation 'io.prometheus:simpleclient'
implementation 'io.prometheus:simpleclient_common'
implementation 'io.prometheus:simpleclient_hotspot'
implementation 'io.prometheus:simpleclient_pushgateway'
implementation 'io.vertx:vertx-core'
implementation 'io.vertx:vertx-web'
implementation 'org.apache.logging.log4j:log4j-api'

@ -22,9 +22,15 @@ public class MetricsConfiguration {
private static final String DEFAULT_METRICS_HOST = "127.0.0.1";
public static final int DEFAULT_METRICS_PORT = 9545;
public static final String MODE_PUSH_GATEWAY = "push";
public static final String MODE_SERVER_PULL = "pull";
private boolean enabled;
private int port;
private String host;
private String mode;
private int pushInterval;
private String prometheusJob;
private Collection<String> hostsWhitelist = Collections.singletonList("localhost");
public static MetricsConfiguration createDefault() {
@ -32,6 +38,9 @@ public class MetricsConfiguration {
metricsConfiguration.setEnabled(false);
metricsConfiguration.setPort(DEFAULT_METRICS_PORT);
metricsConfiguration.setHost(DEFAULT_METRICS_HOST);
metricsConfiguration.setMode(MODE_SERVER_PULL);
metricsConfiguration.setPushInterval(15);
metricsConfiguration.setPrometheusJob("pantheon-client");
return metricsConfiguration;
}
@ -62,6 +71,30 @@ public class MetricsConfiguration {
this.host = host;
}
public String getMode() {
return mode;
}
public void setMode(final String mode) {
this.mode = mode;
}
public int getPushInterval() {
return pushInterval;
}
public void setPushInterval(final int pushInterval) {
this.pushInterval = pushInterval;
}
public String getPrometheusJob() {
return prometheusJob;
}
public void setPrometheusJob(final String prometheusJob) {
this.prometheusJob = prometheusJob;
}
Collection<String> getHostsWhitelist() {
return Collections.unmodifiableCollection(this.hostsWhitelist);
}

@ -46,7 +46,7 @@ import io.vertx.ext.web.RoutingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class MetricsHttpService {
class MetricsHttpService implements MetricsService {
private static final Logger LOG = LogManager.getLogger();
private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0);
@ -57,7 +57,7 @@ public class MetricsHttpService {
private HttpServer httpServer;
public MetricsHttpService(
MetricsHttpService(
final Vertx vertx,
final MetricsConfiguration configuration,
final MetricsSystem metricsSystem) {
@ -72,8 +72,16 @@ public class MetricsHttpService {
config.getPort() == 0 || NetworkUtility.isValidPort(config.getPort()),
"Invalid port configuration.");
checkArgument(config.getHost() != null, "Required host is not configured.");
checkArgument(
MetricsConfiguration.MODE_SERVER_PULL.equals(config.getMode()),
"Metrics Http Service cannot start up outside of '"
+ MetricsConfiguration.MODE_SERVER_PULL
+ "' mode, requested mode is '"
+ config.getMode()
+ "'.");
}
@Override
public CompletableFuture<?> start() {
LOG.info("Starting Metrics service on {}:{}", config.getHost(), config.getPort());
// Create the HTTP server and a router object.
@ -159,6 +167,7 @@ public class MetricsHttpService {
.anyMatch(whitelistEntry -> whitelistEntry.toLowerCase().equals(hostHeader.toLowerCase()));
}
@Override
public CompletableFuture<?> stop() {
if (httpServer == null) {
return CompletableFuture.completedFuture(null);
@ -211,13 +220,21 @@ public class MetricsHttpService {
});
}
public InetSocketAddress socketAddress() {
InetSocketAddress socketAddress() {
if (httpServer == null) {
return EMPTY_SOCKET_ADDRESS;
}
return new InetSocketAddress(config.getHost(), httpServer.actualPort());
}
@Override
public Optional<Integer> getPort() {
if (httpServer == null) {
return Optional.empty();
}
return Optional.of(httpServer.actualPort());
}
@VisibleForTesting
public String url() {
if (httpServer == null) {

@ -0,0 +1,106 @@
/*
* 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 tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.util.NetworkUtility;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import io.prometheus.client.exporter.PushGateway;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
class MetricsPushGatewayService implements MetricsService {
private static final Logger LOG = LogManager.getLogger();
private PushGateway pushGateway;
private ScheduledExecutorService scheduledExecutorService;
private final MetricsConfiguration config;
private final MetricsSystem metricsSystem;
MetricsPushGatewayService(
final MetricsConfiguration configuration, final MetricsSystem metricsSystem) {
this.metricsSystem = metricsSystem;
validateConfig(configuration);
config = configuration;
}
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.");
checkArgument(
MetricsConfiguration.MODE_PUSH_GATEWAY.equals(config.getMode()),
"Metrics Push Gateway Service cannot start up outside of '"
+ MetricsConfiguration.MODE_PUSH_GATEWAY
+ "' mode.");
checkArgument(
metricsSystem instanceof PrometheusMetricsSystem,
"Push Gateway requires a Prometheus Metrics System.");
}
@Override
public CompletableFuture<?> start() {
LOG.info("Starting Metrics service on {}:{}", config.getHost(), config.getPort());
pushGateway = new PushGateway(config.getHost() + ":" + config.getPort());
// Create the executor
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleAtFixedRate(
this::pushMetrics,
config.getPushInterval() / 2,
config.getPushInterval(),
TimeUnit.SECONDS);
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<?> stop() {
final CompletableFuture<?> resultFuture = new CompletableFuture<>();
try {
// Calling shutdown now cancels the pending push, which is desirable.
scheduledExecutorService.shutdownNow();
scheduledExecutorService.awaitTermination(30, TimeUnit.SECONDS);
pushGateway.delete(config.getPrometheusJob());
resultFuture.complete(null);
} catch (final IOException | InterruptedException e) {
LOG.error(e);
resultFuture.completeExceptionally(e);
}
return resultFuture;
}
@Override
public Optional<Integer> getPort() {
return Optional.empty();
}
private void pushMetrics() {
try {
pushGateway.pushAdd(
((PrometheusMetricsSystem) metricsSystem).getRegistry(), config.getPrometheusJob());
} catch (final IOException e) {
LOG.warn("Cound not push metrics", e);
}
}
}

@ -0,0 +1,58 @@
/*
* 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 tech.pegasys.pantheon.metrics.MetricsSystem;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import io.vertx.core.Vertx;
public interface MetricsService {
static MetricsService create(
final Vertx vertx,
final MetricsConfiguration configuration,
final MetricsSystem metricsSystem) {
switch (configuration.getMode()) {
case MetricsConfiguration.MODE_PUSH_GATEWAY:
return new MetricsPushGatewayService(configuration, metricsSystem);
case MetricsConfiguration.MODE_SERVER_PULL:
return new MetricsHttpService(vertx, configuration, metricsSystem);
default:
throw new RuntimeException("No metrics service for mode '" + configuration.getMode() + "'");
}
}
/**
* Starts the Metrics Service and all needed backend systems.
*
* @return completion state
*/
CompletableFuture<?> start();
/**
* Stops the Metrics Service and performs needed cleanup.
*
* @return completion state
*/
CompletableFuture<?> stop();
/**
* If serving to a port on the client, what the port number is.
*
* @return Port number optional, serving if present.
*/
Optional<Integer> getPort();
}

@ -16,7 +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 tech.pegasys.pantheon.metrics.prometheus.MetricsService;
import java.io.File;
import java.io.FileOutputStream;
@ -42,7 +42,7 @@ public class Runner implements AutoCloseable {
private final NetworkRunner networkRunner;
private final Optional<JsonRpcHttpService> jsonRpc;
private final Optional<WebSocketService> websocketRpc;
private final Optional<MetricsHttpService> metrics;
private final Optional<MetricsService> metrics;
private final PantheonController<?> pantheonController;
private final Path dataDir;
@ -52,7 +52,7 @@ public class Runner implements AutoCloseable {
final NetworkRunner networkRunner,
final Optional<JsonRpcHttpService> jsonRpc,
final Optional<WebSocketService> websocketRpc,
final Optional<MetricsHttpService> metrics,
final Optional<MetricsService> metrics,
final PantheonController<?> pantheonController,
final Path dataDir) {
this.vertx = vertx;
@ -148,7 +148,11 @@ public class Runner implements AutoCloseable {
}
public Optional<Integer> getMetricsPort() {
return metrics.map(service -> service.socketAddress().getPort());
if (metrics.isPresent()) {
return metrics.get().getPort();
} else {
return Optional.empty();
}
}
public int getP2pUdpPort() {

@ -57,7 +57,7 @@ import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive;
import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsHttpService;
import tech.pegasys.pantheon.metrics.prometheus.MetricsService;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.nio.file.Path;
@ -305,7 +305,7 @@ public class RunnerBuilder {
vertx, webSocketConfiguration, subscriptionManager, webSocketsJsonRpcMethods));
}
Optional<MetricsHttpService> metricsService = Optional.empty();
Optional<MetricsService> metricsService = Optional.empty();
if (metricsConfiguration.isEnabled()) {
metricsService = Optional.of(createMetricsService(vertx, metricsConfiguration));
}
@ -416,8 +416,8 @@ public class RunnerBuilder {
return new WebSocketService(vertx, configuration, websocketRequestHandler);
}
private MetricsHttpService createMetricsService(
private MetricsService createMetricsService(
final Vertx vertx, final MetricsConfiguration configuration) {
return new MetricsHttpService(vertx, configuration, metricsSystem);
return MetricsService.create(vertx, configuration, metricsSystem);
}
}

@ -18,7 +18,7 @@ import static tech.pegasys.pantheon.cli.DefaultCommandValues.MANDATORY_FILE_FORM
import tech.pegasys.pantheon.cli.BlocksSubCommand.ImportSubCommand;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsHttpService;
import tech.pegasys.pantheon.metrics.prometheus.MetricsService;
import tech.pegasys.pantheon.util.BlockImporter;
import java.io.File;
@ -104,25 +104,25 @@ class BlocksSubCommand implements Runnable {
checkNotNull(parentCommand.parentCommand);
checkNotNull(parentCommand.blockImporter);
Optional<MetricsHttpService> metricsHttpService = Optional.empty();
Optional<MetricsService> metricsService = Optional.empty();
try {
final MetricsConfiguration metricsConfiguration =
parentCommand.parentCommand.metricsConfiguration();
if (metricsConfiguration.isEnabled()) {
metricsHttpService =
metricsService =
Optional.of(
new MetricsHttpService(
MetricsService.create(
Vertx.vertx(),
metricsConfiguration,
parentCommand.parentCommand.getMetricsSystem()));
metricsHttpService.ifPresent(MetricsHttpService::start);
metricsService.ifPresent(MetricsService::start);
}
// As blocksImportFile even if initialized as null is injected by PicoCLI and param is
// mandatory
// So we are sure it's always not null, we can remove the warning
//noinspection ConstantConditions
Path path = blocksImportFile.toPath();
final Path path = blocksImportFile.toPath();
parentCommand.blockImporter.importBlockchain(
path, parentCommand.parentCommand.buildController());
@ -133,7 +133,7 @@ class BlocksSubCommand implements Runnable {
throw new ExecutionException(
new CommandLine(this), "Unable to import blocks from " + blocksImportFile, e);
} finally {
metricsHttpService.ifPresent(MetricsHttpService::stop);
metricsService.ifPresent(MetricsService::stop);
}
}
}

@ -406,6 +406,13 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
)
private final Boolean isMetricsEnabled = false;
@Option(
names = {"--metrics-mode"},
description =
"Mode for the metrics service to run in, 'push' or 'pull' (default: ${DEFAULT-VALUE})"
)
private String metricsMode = MetricsConfiguration.MODE_SERVER_PULL;
@Option(
names = {"--metrics-host"},
paramLabel = MANDATORY_HOST_FORMAT_HELP,
@ -423,6 +430,22 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
)
private final Integer metricsPort = DEFAULT_METRICS_PORT;
@Option(
names = {"--metrics-push-interval"},
paramLabel = MANDATORY_INTEGER_FORMAT_HELP,
description =
"Interval in seconds to push metrics when in push mode (default: ${DEFAULT-VALUE})",
arity = "1"
)
private final Integer metricsPushInterval = 15;
@Option(
names = {"--metrics-prometheus-job"},
description = "Job name to use when in push mode (default: ${DEFAULT-VALUE})",
arity = "1"
)
private String metricsPrometheusJob = "pantheon-client";
@Option(
names = {"--host-whitelist"},
paramLabel = "<hostname>[,<hostname>...]... or * or all",
@ -673,8 +696,11 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
MetricsConfiguration metricsConfiguration() {
final MetricsConfiguration metricsConfiguration = createDefault();
metricsConfiguration.setEnabled(isMetricsEnabled);
metricsConfiguration.setMode(metricsMode);
metricsConfiguration.setHost(metricsHost.toString());
metricsConfiguration.setPort(metricsPort);
metricsConfiguration.setPushInterval(metricsPushInterval);
metricsConfiguration.setPrometheusJob(metricsPrometheusJob);
metricsConfiguration.setHostsWhitelist(hostsWhitelist);
return metricsConfiguration;
}

@ -1340,6 +1340,46 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void metricsModeOptionMustBeUsed() {
parseCommand("--metrics-mode", "pull");
verify(mockRunnerBuilder).metricsConfiguration(metricsConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(metricsConfigArgumentCaptor.getValue().getMode()).isEqualTo("pull");
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void metricsPushIntervalMustBeUsed() {
parseCommand("--metrics-push-interval", "42");
verify(mockRunnerBuilder).metricsConfiguration(metricsConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(metricsConfigArgumentCaptor.getValue().getPushInterval()).isEqualTo(42);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void metricsPrometheusJobMustBeUsed() {
parseCommand("--metrics-prometheus-job", "pantheon-command-test");
verify(mockRunnerBuilder).metricsConfiguration(metricsConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(metricsConfigArgumentCaptor.getValue().getPrometheusJob())
.isEqualTo("pantheon-command-test");
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void pantheonDoesNotStartInMiningModeIfCoinbaseNotSet() {
parseCommand("--miner-enabled");

@ -57,8 +57,11 @@ rpc-ws-refresh-delay=500
# Prometheus Metrics Endpoint
metrics-enabled=false
metrics-mode="pull"
metrics-host="8.6.7.5"
metrics-port=309
metrics-push-interval=42
metrics-prometheus-job="pantheon-everything"
# Mining
miner-enabled=false

Loading…
Cancel
Save