Merge pull request #2181 from freimair/monitor

Bisq Network Monitor: Babysteps
This commit is contained in:
Manfred Karrer 2018-12-30 11:05:57 +01:00 committed by GitHub
commit d013e7d8a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1453 additions and 1803 deletions

2
.gitignore vendored
View File

@ -29,3 +29,5 @@ desktop.ini
*.class
deploy
*/releases/*
/monitor/TorHiddenServiceStartupTimeTests/*
/monitor/monitor-tor/*

View File

@ -34,10 +34,12 @@ configure(subprojects) {
joptVersion = '5.0.3'
langVersion = '3.4'
libdohjVersion = '7be803fa'
logbackVersion = '1.1.10'
lombokVersion = '1.18.2'
mockitoVersion = '2.21.0'
powermockVersion = '2.0.0-beta.5'
protobufVersion = '3.5.1'
slf4jVersion = '1.7.22'
sparkVersion = '2.5.2'
springVersion = '4.3.6.RELEASE'
@ -159,9 +161,9 @@ configure(project(':common')) {
exclude(module: 'junit')
}
compile "org.springframework:spring-core:$springVersion"
compile 'org.slf4j:slf4j-api:1.7.22'
compile 'ch.qos.logback:logback-core:1.1.10'
compile 'ch.qos.logback:logback-classic:1.1.10'
compile "org.slf4j:slf4j-api:$slf4jVersion"
compile "ch.qos.logback:logback-core:$logbackVersion"
compile "ch.qos.logback:logback-classic:$logbackVersion"
compile 'com.google.code.findbugs:jsr305:3.0.2'
compile 'com.google.guava:guava:20.0'
compile('com.google.inject:guice:4.1.0') {
@ -310,14 +312,33 @@ configure(project(':desktop')) {
configure(project(':monitor')) {
mainClassName = 'bisq.monitor.MonitorMain'
mainClassName = 'bisq.monitor.Monitor'
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
dependencies {
compile project(':core')
compile "com.sparkjava:spark-core:$sparkVersion"
compile 'net.gpedro.integrations.slack:slack-webhook:1.1.1'
compile "org.slf4j:slf4j-api:$slf4jVersion"
compile "ch.qos.logback:logback-core:$logbackVersion"
compile "ch.qos.logback:logback-classic:$logbackVersion"
compile 'com.google.guava:guava:20.0'
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compile('com.github.JesusMcCloud.netlayer:tor.native:0.6.2') {
exclude(module: 'slf4j-api')
}
testCompile 'org.junit.jupiter:junit-jupiter-api:5.3.2'
testCompile 'org.junit.jupiter:junit-jupiter-params:5.3.2'
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testRuntime('org.junit.jupiter:junit-jupiter-engine:5.3.2')
}
}

View File

@ -10,6 +10,7 @@
// 4. Commit the changes
//
// See https://github.com/signalapp/gradle-witness#using-witness for further details.
dependencyVerification {
verify = [
'org.controlsfx:controlsfx:b98f1c9507c05600f80323674b33d15674926c71b0116f70085b62bdacf1e573',

57
monitor/README.md Normal file
View File

@ -0,0 +1,57 @@
# Bisq Network Monitor Node
The Bisq monitor node collects a set of metrics which are of interest to developers and users alike. These metrics are then made available through reporters.
The *Babysteps* release features these metrics:
- Tor Startup Time: The time it takes to start Tor starting at a clean system, unpacking the shipped Tor binaries, firing up Tor until Tor is connected to the Tor network and ready to use.
- Tor Roundtrip Time: Given a bootstrapped Tor, the roundtrip time of connecting to a hidden service is measured.
- Tor Hidden Service Startup Time: Given a bootstrapped Tor, the time it takes to create and announce a freshly created hidden service.
The *Babysteps* release features these reporters:
- A reporter that simply writes the findings to `System.err`
- A reporter that reports the findings to a Graphite/Carbon instance using the [plaintext protocol](https://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol)
## Configuration
The *Bisq Network Monitor Node* is to be configured via a Java properties file. The location of the file is to be passed as command line parameter:
```
./bisq-monitor /path/to/your/config.properties
```
A sample configuration file looks like follows:
```
## Each Metric is configured via a set of properties.
##
## The minimal set of properties required to run a Metric is:
##
## YourMetricName.enabled=true|false
## YourMetricName.run.interval=10 [seconds]
#Edit and uncomment the lines below for your liking
#TorStartupTime Metric
TorStartupTime.enabled=true
TorStartupTime.run.interval=100
TorStartupTime.run.socksPort=90500 # so that there is no interference with a system Tor
#TorRoundTripTime Metric
TorRoundTripTime.enabled=true
TorRoundTripTime.run.interval=100
TorRoundTripTime.run.sampleSize=5
TorRoundTripTime.run.hosts=http://expyuzz4wqqyqhjn.onion:80 # torproject.org hidden service
#TorHiddenServiceStartupTime Metric
TorHiddenServiceStartupTime.enabled=true
TorHiddenServiceStartupTime.run.interval=100
TorHiddenServiceStartupTime.run.localPort=90501 # so that there is no interference with a system Tor
TorHiddenServiceStartupTime.run.servicePort=90511 # so that there is no interference with a system Tor
## Reporters are configured via a set of properties as well.
##
## In contrast to Metrics, Reporters do not have a minimal set of properties.
#GraphiteReporter
GraphiteReporter.serviceUrl=http://yourHiddenService.onion:2003
```

View File

@ -0,0 +1,74 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import java.util.Properties;
/**
* Does some pre-computation for a configurable class.
*
* @author Florian Reimair
*/
public abstract class Configurable {
protected Properties configuration = new Properties();
private String name;
/**
* Filters all java properties starting with {@link Configurable#getName()} of
* the class and makes them available. Does <em>NOT</em> parse the content of
* the properties!
* <p>
* For example, if the implementing class sets its name (using
* {@link Configurable#setName(String)}) to <code>MyName</code>, the list of
* properties is scanned for properties starting with <code>MyName</code>.
* Matching lines are made available to the class without the prefix. For
* example, a property <code>MyName.answer=42</code> is made available as
* <code>configuration.getProperty("answer")</code> resulting in
* <code>42</code>.
*
* @param properties a set of configuration properties
*/
public void configure(final Properties properties) {
// only configure the Properties which belong to us
final Properties myProperties = new Properties();
properties.forEach((k, v) -> {
String key = (String) k;
if (key.startsWith(getName()))
myProperties.put(key.substring(key.indexOf(".") + 1), v);
});
// configure all properties that belong to us
this.configuration = myProperties;
}
protected String getName() {
return name;
}
/**
* Set the name used to filter through configuration properties. See
* {@link Configurable#configure(Properties)}.
*
* @param name
*/
protected void setName(String name) {
this.name = name;
}
}

View File

@ -0,0 +1,155 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
/**
* Starts a Metric (in its own {@link Thread}), manages its properties and shuts
* it down gracefully. Furthermore, configuration updates and execution are done
* in a thread-save manner. Implementing classes only have to implement the
* {@link Metric#execute()} method.
*
* @author Florian Reimair
*/
@Slf4j
public abstract class Metric extends Configurable implements Runnable {
private static final String INTERVAL = "run.interval";
private volatile boolean shutdown = false;
/**
* our reporter
*/
protected final Reporter reporter;
private Thread thread = new Thread();
/**
* disable execution
*/
private void disable() {
shutdown = true;
}
/**
* enable execution
*/
private void enable() {
shutdown = false;
thread = new Thread(this);
// set human readable name
thread.setName(getName());
// set as daemon, so that the jvm does not terminate the thread
thread.setDaemon(true);
thread.start();
}
/**
* Constructor.
*/
protected Metric(Reporter reporter) {
this.reporter = reporter;
setName(this.getClass().getSimpleName());
// disable by default
disable();
}
protected boolean enabled() {
return !shutdown;
}
@Override
public void configure(final Properties properties) {
synchronized (this) {
log.info("{} (re)loading config...", getName());
super.configure(properties);
reporter.configure(properties);
// decide whether to enable or disable the task
if (configuration.isEmpty() || !configuration.getProperty("enabled", "false").equals("true")
|| !configuration.containsKey(INTERVAL)) {
disable();
// some informative log output
if (configuration.isEmpty())
log.error("{} is not configured at all. Will not run.", getName());
else if (!configuration.getProperty("enabled", "false").equals("true"))
log.info("{} is deactivated. Will not run.", getName());
else if (!configuration.containsKey(INTERVAL))
log.error("{} is missing mandatory '" + INTERVAL + "' property. Will not run.", getName());
else
log.error("{} is mis-configured. Will not run.", getName());
} else if (!enabled() && configuration.getProperty("enabled", "false").equals("true")) {
// check if this Metric got activated after being disabled.
// if so, resume execution
enable();
log.info("{} got activated. Starting up.", getName());
}
}
}
@Override
public void run() {
while (!shutdown) {
// if not, execute all the things
synchronized (this) {
execute();
}
// make sure our configuration is not changed in the moment we want to query it
String interval;
synchronized (this) {
interval = configuration.getProperty(INTERVAL);
}
// and go to sleep for the configured amount of time.
try {
Thread.sleep(Long.parseLong(interval) * 1000);
} catch (InterruptedException ignore) {
}
}
log.info("{} shutdown", getName());
}
/**
* Gets scheduled repeatedly.
*/
protected abstract void execute();
/**
* Initiate graceful shutdown of the Metric.
*/
public void shutdown() {
log.debug("{} shutdown requested", getName());
shutdown = true;
}
protected void join() throws InterruptedException {
thread.join();
}
}

View File

@ -17,28 +17,142 @@
package bisq.monitor;
import bisq.monitor.metrics.MetricsModel;
import bisq.monitor.metric.TorHiddenServiceStartupTime;
import bisq.monitor.metric.TorRoundTripTime;
import bisq.monitor.metric.TorStartupTime;
import bisq.monitor.reporter.GraphiteReporter;
import com.google.inject.Injector;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import sun.misc.Signal;
/**
* Monitor executable for the Bisq network.
*
* @author Florian Reimair
*/
@Slf4j
public class Monitor {
@Setter
private Injector injector;
@Getter
private MetricsModel metricsModel;
public Monitor() {
private static String[] args = {};
public static void main(String[] args) throws Throwable {
Monitor.args = args;
new Monitor().start();
}
public void startApplication() {
metricsModel = injector.getInstance(MetricsModel.class);
/**
* A list of all active {@link Metric}s
*/
private final List<Metric> metrics = new ArrayList<>();
MonitorAppSetup appSetup = injector.getInstance(MonitorAppSetup.class);
appSetup.start();
/**
* Starts up all configured Metrics.
*
* @throws Exception
*/
private void start() throws Throwable {
// start Tor
Tor.setDefault(new NativeTor(new File("monitor/monitor-tor"), null, null, false));
// assemble Metrics
// - create reporters
// ConsoleReporter consoleReporter = new ConsoleReporter();
Reporter graphiteReporter = new GraphiteReporter();
// - add available metrics with their reporters
metrics.add(new TorStartupTime(graphiteReporter));
metrics.add(new TorRoundTripTime(graphiteReporter));
metrics.add(new TorHiddenServiceStartupTime(graphiteReporter));
// prepare configuration reload
// Note that this is most likely only work on Linux
Signal.handle(new Signal("USR1"), signal -> {
reload();
});
// configure Metrics
// - which also starts the metrics if appropriate
Properties properties = getProperties();
for (Metric current : metrics)
current.configure(properties);
// exit Metrics gracefully on shutdown
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
// set the name of the Thread for debugging purposes
setName("shutdownHook");
for (Metric current : metrics) {
current.shutdown();
}
// wait for the metrics to gracefully shut down
for (Metric current : metrics)
try {
current.join();
} catch (InterruptedException ignore) {
}
log.info("shutting down tor");
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
log.info("system halt");
}
});
// prevent the main thread to terminate
log.info("joining metrics...");
for (Metric current : metrics)
current.join();
}
/**
* Reload the configuration from disk.
*/
private void reload() {
try {
Properties properties = getProperties();
for (Metric current : metrics)
current.configure(properties);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Overloads a default set of properties with a file if given
*
* @return a set of properties
* @throws Exception
*/
private Properties getProperties() throws Exception {
Properties defaults = new Properties();
defaults.load(Monitor.class.getClassLoader().getResourceAsStream("metrics.properties"));
Properties result = new Properties(defaults);
if (args.length > 0)
result.load(new FileInputStream(args[0]));
return result;
}
}

View File

@ -1,119 +0,0 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metrics.p2p.MonitorP2PService;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.SetupUtils;
import bisq.core.btc.setup.WalletsSetup;
import bisq.network.crypto.EncryptionService;
import bisq.network.p2p.network.SetupListener;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.app.Version;
import bisq.common.crypto.KeyRing;
import bisq.common.proto.persistable.PersistedDataHost;
import javax.inject.Inject;
import java.util.ArrayList;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MonitorAppSetup {
private MonitorP2PService seedNodeMonitorP2PService;
private final WalletsSetup walletsSetup;
private PeerManager peerManager;
private final KeyRing keyRing;
private final EncryptionService encryptionService;
@Inject
public MonitorAppSetup(MonitorP2PService seedNodeMonitorP2PService,
WalletsSetup walletsSetup,
PeerManager peerManager,
KeyRing keyRing,
EncryptionService encryptionService) {
this.seedNodeMonitorP2PService = seedNodeMonitorP2PService;
this.walletsSetup = walletsSetup;
this.peerManager = peerManager;
this.keyRing = keyRing;
this.encryptionService = encryptionService;
Version.setBaseCryptoNetworkId(BisqEnvironment.getBaseCurrencyNetwork().ordinal());
Version.printVersion();
}
public void start() {
SetupUtils.checkCryptoSetup(keyRing, encryptionService, () -> {
initPersistedDataHosts();
initBasicServices();
}, throwable -> {
log.error(throwable.getMessage());
throwable.printStackTrace();
System.exit(1);
});
}
public void initPersistedDataHosts() {
ArrayList<PersistedDataHost> persistedDataHosts = new ArrayList<>();
persistedDataHosts.add(seedNodeMonitorP2PService);
persistedDataHosts.add(peerManager);
// we apply at startup the reading of persisted data but don't want to get it triggered in the constructor
persistedDataHosts.forEach(e -> {
try {
log.info("call readPersisted at " + e.getClass().getSimpleName());
e.readPersisted();
} catch (Throwable e1) {
log.error("readPersisted error", e1);
}
});
}
protected void initBasicServices() {
SetupUtils.readFromResources(seedNodeMonitorP2PService.getP2PDataStorage()).addListener((observable, oldValue, newValue) -> {
if (newValue) {
seedNodeMonitorP2PService.start(new SetupListener() {
@Override
public void onTorNodeReady() {
walletsSetup.initialize(null,
() -> log.info("walletsSetup completed"),
throwable -> log.error(throwable.toString()));
}
@Override
public void onHiddenServicePublished() {
}
@Override
public void onSetupFailed(Throwable throwable) {
}
@Override
public void onRequestCustomBridges() {
}
});
}
});
}
}

View File

@ -1,129 +0,0 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.core.app.AppOptionKeys;
import bisq.core.app.BisqEnvironment;
import bisq.core.btc.BtcOptionKeys;
import bisq.core.btc.UserAgent;
import bisq.core.dao.DaoOptionKeys;
import bisq.network.NetworkOptionKeys;
import bisq.common.CommonOptionKeys;
import bisq.common.app.Version;
import bisq.common.crypto.KeyStorage;
import bisq.common.storage.Storage;
import org.springframework.core.env.JOptCommandLinePropertySource;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import joptsimple.OptionSet;
import java.nio.file.Paths;
import java.util.Properties;
import static com.google.common.base.Preconditions.checkNotNull;
public class MonitorEnvironment extends BisqEnvironment {
private String slackUrlSeedChannel = "";
private String slackUrlBtcChannel = "";
private String slackUrlProviderChannel = "";
private String port;
public MonitorEnvironment(OptionSet options) {
this(new JOptCommandLinePropertySource(BISQ_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull(options)));
}
public MonitorEnvironment(PropertySource commandLineProperties) {
super(commandLineProperties);
slackUrlSeedChannel = commandLineProperties.containsProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) :
"";
slackUrlBtcChannel = commandLineProperties.containsProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL) :
"";
slackUrlProviderChannel = commandLineProperties.containsProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL) :
"";
port = commandLineProperties.containsProperty(MonitorOptionKeys.PORT) ?
(String) commandLineProperties.getProperty(MonitorOptionKeys.PORT) :
"80";
// hack because defaultProperties() is called from constructor and slackUrlSeedChannel would be null there
getPropertySources().remove("bisqDefaultProperties");
getPropertySources().addLast(defaultPropertiesMonitor());
}
protected PropertySource<?> defaultPropertiesMonitor() {
return new PropertiesPropertySource(BISQ_DEFAULT_PROPERTY_SOURCE_NAME, new Properties() {
{
setProperty(CommonOptionKeys.LOG_LEVEL_KEY, logLevel);
setProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL, slackUrlSeedChannel);
setProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL, slackUrlBtcChannel);
setProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL, slackUrlProviderChannel);
setProperty(MonitorOptionKeys.PORT, port);
setProperty(NetworkOptionKeys.SEED_NODES_KEY, seedNodes);
setProperty(NetworkOptionKeys.MY_ADDRESS, myAddress);
setProperty(NetworkOptionKeys.BAN_LIST, banList);
setProperty(NetworkOptionKeys.TOR_DIR, Paths.get(btcNetworkDir, "tor").toString());
setProperty(NetworkOptionKeys.NETWORK_ID, String.valueOf(baseCurrencyNetwork.ordinal()));
setProperty(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS, socks5ProxyBtcAddress);
setProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS, socks5ProxyHttpAddress);
setProperty(AppOptionKeys.APP_DATA_DIR_KEY, appDataDir);
setProperty(AppOptionKeys.IGNORE_DEV_MSG_KEY, ignoreDevMsg);
setProperty(AppOptionKeys.DUMP_STATISTICS, dumpStatistics);
setProperty(AppOptionKeys.APP_NAME_KEY, appName);
setProperty(AppOptionKeys.MAX_MEMORY, maxMemory);
setProperty(AppOptionKeys.USER_DATA_DIR_KEY, userDataDir);
setProperty(AppOptionKeys.PROVIDERS, providers);
setProperty(DaoOptionKeys.RPC_USER, rpcUser);
setProperty(DaoOptionKeys.RPC_PASSWORD, rpcPassword);
setProperty(DaoOptionKeys.RPC_PORT, rpcPort);
setProperty(DaoOptionKeys.RPC_BLOCK_NOTIFICATION_PORT, rpcBlockNotificationPort);
setProperty(DaoOptionKeys.DUMP_BLOCKCHAIN_DATA, dumpBlockchainData);
setProperty(DaoOptionKeys.FULL_DAO_NODE, fullDaoNode);
setProperty(DaoOptionKeys.GENESIS_TX_ID, genesisTxId);
setProperty(DaoOptionKeys.GENESIS_BLOCK_HEIGHT, genesisBlockHeight);
setProperty(BtcOptionKeys.BTC_NODES, btcNodes);
setProperty(BtcOptionKeys.USE_TOR_FOR_BTC, useTorForBtc);
setProperty(BtcOptionKeys.WALLET_DIR, btcNetworkDir);
setProperty(BtcOptionKeys.USER_AGENT, userAgent);
setProperty(BtcOptionKeys.USE_ALL_PROVIDED_NODES, useAllProvidedNodes);
setProperty(BtcOptionKeys.NUM_CONNECTIONS_FOR_BTC, numConnectionForBtc);
setProperty(UserAgent.NAME_KEY, appName);
setProperty(UserAgent.VERSION_KEY, Version.VERSION);
setProperty(Storage.STORAGE_DIR, Paths.get(btcNetworkDir, "db").toString());
setProperty(KeyStorage.KEY_STORAGE_DIR, Paths.get(btcNetworkDir, "keys").toString());
}
});
}
}

View File

@ -1,142 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.BisqExecutable;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.setup.CommonSetup;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import static spark.Spark.port;
import spark.Spark;
@Slf4j
public class MonitorMain extends ExecutableForAppWithP2p {
private static final String VERSION = "1.0.1";
private Monitor monitor;
public MonitorMain() {
super("Bisq Monitor", "bisq-monitor", VERSION);
}
public static void main(String[] args) throws Exception {
log.info("Monitor.VERSION: " + VERSION);
BisqEnvironment.setDefaultAppName("bisq_monitor");
if (BisqExecutable.setupInitialOptionParser(args))
new MonitorMain().execute(args);
}
@Override
protected void doExecute(OptionSet options) {
super.doExecute(options);
CommonSetup.setup(this);
checkMemory(bisqEnvironment, this);
startHttpServer(bisqEnvironment.getProperty(MonitorOptionKeys.PORT));
keepRunning();
}
@Override
protected void setupEnvironment(OptionSet options) {
bisqEnvironment = new MonitorEnvironment(checkNotNull(options));
}
@Override
protected void launchApplication() {
UserThread.execute(() -> {
try {
monitor = new Monitor();
UserThread.execute(this::onApplicationLaunched);
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
protected void onApplicationLaunched() {
super.onApplicationLaunched();
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new MonitorModule(bisqEnvironment);
}
@Override
protected void applyInjector() {
super.applyInjector();
monitor.setInjector(injector);
}
@Override
protected void startApplication() {
monitor.startApplication();
}
private void startHttpServer(String port) {
port(Integer.parseInt(port));
Spark.get("/", (req, res) -> {
log.info("Incoming request from: " + req.userAgent());
final String resultAsHtml = monitor.getMetricsModel().getResultAsHtml();
return resultAsHtml == null ? "Still starting up..." : resultAsHtml;
});
}
@Override
protected void customizeOptionParsing(OptionParser parser) {
super.customizeOptionParsing(parser);
parser.accepts(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL,
"Set slack secret for seed node monitor")
.withRequiredArg();
parser.accepts(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL,
"Set slack secret for Btc node monitor")
.withRequiredArg();
parser.accepts(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL,
"Set slack secret for provider node monitor")
.withRequiredArg();
parser.accepts(MonitorOptionKeys.PORT,
"Set port to listen on")
.withRequiredArg()
.defaultsTo("80");
}
}

View File

@ -1,56 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metrics.p2p.MonitorP2PModule;
import bisq.core.app.BisqEnvironment;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.network.p2p.P2PModule;
import org.springframework.core.env.Environment;
import static com.google.inject.name.Names.named;
class MonitorModule extends ModuleForAppWithP2p {
public MonitorModule(Environment environment) {
super(environment);
}
@Override
protected void configure() {
super.configure();
bindConstant().annotatedWith(named(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL)).to(environment.getRequiredProperty(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL));
bindConstant().annotatedWith(named(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL)).to(environment.getRequiredProperty(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL));
bindConstant().annotatedWith(named(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL)).to(environment.getRequiredProperty(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL));
bindConstant().annotatedWith(named(MonitorOptionKeys.PORT)).to(environment.getRequiredProperty(MonitorOptionKeys.PORT));
}
@Override
protected void configEnvironment() {
bind(BisqEnvironment.class).toInstance((MonitorEnvironment) environment);
}
@Override
protected P2PModule p2pModule() {
return new MonitorP2PModule(environment);
}
}

View File

@ -1,26 +0,0 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
public class MonitorOptionKeys {
public static final String SLACK_URL_SEED_CHANNEL = "slackUrlSeedChannel";
public static final String SLACK_BTC_SEED_CHANNEL = "slackUrlBtcChannel";
public static final String SLACK_PROVIDER_SEED_CHANNEL = "slackUrlProviderChannel";
public static final String PORT = "port";
}

View File

@ -0,0 +1,64 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import java.util.Map;
/**
* Reports findings to a specific service/file/place using the proper means to
* do so.
*
* @author Florian Reimair
*/
public abstract class Reporter extends Configurable {
protected Reporter() {
setName(this.getClass().getSimpleName());
}
/**
* Report our findings.
*
* @param value
*/
public abstract void report(long value);
/**
* Report our findings
*
* @param value
* @param prefix
*/
public abstract void report(long value, String prefix);
/**
* Report our findings.
*
* @param values Map<metric name, metric value>
*/
public abstract void report(Map<String, String> values);
/**
* Report our findings.
*
* @param values Map<metric name, metric value>
* @param prefix for example "bisq.torStartupTime"
*/
public abstract void report(Map<String, String> values, String prefix);
}

View File

@ -0,0 +1,97 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.Reporter;
import org.berndpruenster.netlayer.tor.HiddenServiceSocket;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
/**
* A Metric to measure the startup time of a Tor Hidden Service on a already
* running Tor.
*
* @author Florian Reimair
*/
@Slf4j
public class TorHiddenServiceStartupTime extends Metric {
private static final String SERVICE_PORT = "run.servicePort";
private static final String LOCAL_PORT = "run.localPort";
private final String hiddenServiceDirectory = "metric_" + getName();
public TorHiddenServiceStartupTime(Reporter reporter) {
super(reporter);
}
/**
* synchronization helper. Required because directly closing the
* HiddenServiceSocket in its ReadyListener causes a deadlock
*/
private void await() {
synchronized (hiddenServiceDirectory) {
try {
hiddenServiceDirectory.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void proceed() {
synchronized (hiddenServiceDirectory) {
hiddenServiceDirectory.notify();
}
}
@Override
protected void execute() {
// prepare settings. Fetch them every time we run the Metric so we do not have to
// restart on a config update
int localPort = Integer.parseInt(configuration.getProperty(LOCAL_PORT, "9998"));
int servicePort = Integer.parseInt(configuration.getProperty(SERVICE_PORT, "9999"));
// clear directory so we get a new onion address every time
new File(hiddenServiceDirectory).delete();
log.debug("creating the hidden service");
// start timer - we do not need System.nanoTime as we expect our result to be in
// the range of tenth of seconds.
long start = System.currentTimeMillis();
HiddenServiceSocket hiddenServiceSocket = new HiddenServiceSocket(localPort, hiddenServiceDirectory,
servicePort);
hiddenServiceSocket.addReadyListener(socket -> {
// stop the timer and report
reporter.report(System.currentTimeMillis() - start, "bisq." + getName());
log.debug("the hidden service is ready");
proceed();
return null;
});
await();
log.debug("going to revoke the hidden service...");
hiddenServiceSocket.close();
log.debug("[going to revoke the hidden service...] done");
}
}

View File

@ -0,0 +1,123 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.Reporter;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import com.runjva.sourceforge.jsocks.protocol.SocksSocket;
import java.net.URL;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.LongSummaryStatistics;
import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A Metric to measure the round-trip time to the Bisq seed nodes via plain tor.
*
* @author Florian Reimair
*/
public class TorRoundTripTime extends Metric {
private static final String SAMPLE_SIZE = "run.sampleSize";
private static final String HOSTS = "run.hosts";
public TorRoundTripTime(Reporter reporter) {
super(reporter);
}
@Override
protected void execute() {
SocksSocket socket;
try {
// fetch proxy
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
Socks5Proxy proxy = tor.getProxy();
// for each configured host
for (String current : configuration.getProperty(HOSTS, "").split(",")) {
// parse Url
URL tmp = new URL(current);
List<Long> samples = new ArrayList<>();
while (samples.size() < Integer.parseInt(configuration.getProperty(SAMPLE_SIZE, "1"))) {
// start timer - we do not need System.nanoTime as we expect our result to be in
// seconds time.
long start = System.currentTimeMillis();
// connect
socket = new SocksSocket(proxy, tmp.getHost(), tmp.getPort());
// by the time we get here, we are connected
samples.add(System.currentTimeMillis() - start);
// cleanup
socket.close();
}
// aftermath
Collections.sort(samples);
// - average, max, min , sample size
LongSummaryStatistics statistics = samples.stream().mapToLong(val -> val).summaryStatistics();
Map<String, String> results = new HashMap<>();
results.put("average", String.valueOf(Math.round(statistics.getAverage())));
results.put("max", String.valueOf(statistics.getMax()));
results.put("min", String.valueOf(statistics.getMin()));
results.put("sampleSize", String.valueOf(statistics.getCount()));
// - p25, median, p75
Integer[] percentiles = new Integer[]{25, 50, 75};
for (Integer percentile : percentiles) {
double rank = statistics.getCount() * percentile / 100;
Long percentileValue;
if (samples.size() <= rank + 1)
percentileValue = samples.get(samples.size() - 1);
else if (Math.floor(rank) == rank)
percentileValue = samples.get((int) rank);
else
percentileValue = Math.round(samples.get((int) Math.floor(rank))
+ (samples.get((int) (Math.floor(rank) + 1)) - samples.get((int) Math.floor(rank)))
/ (rank - Math.floor(rank)));
results.put("p" + percentile, String.valueOf(percentileValue));
}
// report
reporter.report(results, "bisq." + getName());
}
} catch (TorCtlException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,90 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metric;
import bisq.monitor.Metric;
import bisq.monitor.Reporter;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import org.berndpruenster.netlayer.tor.Torrc;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Properties;
/**
* A Metric to measure the deployment and startup time of the packaged Tor
* binaries.
*
* @author Florian Reimair
*/
public class TorStartupTime extends Metric {
private static final String SOCKS_PORT = "run.socksPort";
private final File torWorkingDirectory = new File("metric_torStartupTime");
private Torrc torOverrides;
public TorStartupTime(Reporter reporter) {
super(reporter);
}
@Override
public void configure(Properties properties) {
super.configure(properties);
synchronized (this) {
LinkedHashMap<String, String> overrides = new LinkedHashMap<>();
overrides.put("SOCKSPort", configuration.getProperty(SOCKS_PORT, "90500"));
try {
torOverrides = new Torrc(overrides);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
protected void execute() {
// cleanup installation
torWorkingDirectory.delete();
Tor tor = null;
// start timer - we do not need System.nanoTime as we expect our result to be in
// tenth of seconds time.
long start = System.currentTimeMillis();
try {
tor = new NativeTor(torWorkingDirectory, null, torOverrides);
// stop the timer and set its timestamp
reporter.report(System.currentTimeMillis() - start, "bisq." + getName());
} catch (TorCtlException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// cleanup
if (tor != null)
tor.shutdown();
}
}
}

View File

@ -1,38 +0,0 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metrics;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
@Getter
public class Metrics {
List<Long> requestDurations = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
List<Map<String, Integer>> receivedObjectsList = new ArrayList<>();
@Setter
long lastDataRequestTs;
@Setter
long lastDataResponseTs;
@Setter
long numRequestAttempts;
}

View File

@ -1,453 +0,0 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metrics;
import bisq.monitor.MonitorOptionKeys;
import bisq.core.btc.nodes.BtcNodes;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.locale.Res;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.seed.SeedNodeRepository;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import net.gpedro.integrations.slack.SlackApi;
import net.gpedro.integrations.slack.SlackMessage;
import org.bitcoinj.core.Peer;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.TimeZone;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MetricsModel {
private final DateFormat dateFormat = new SimpleDateFormat("MMMMM dd, HH:mm:ss");
@Getter
private String resultAsString;
@Getter
private String resultAsHtml;
private SeedNodeRepository seedNodeRepository;
private SlackApi slackSeedApi, slackBtcApi, slackProviderApi;
private BtcNodes btcNodes;
@Setter
private long lastCheckTs;
private long btcNodeUptimeTs;
private int totalErrors = 0;
private HashMap<NodeAddress, Metrics> map = new HashMap<>();
private List<Peer> connectedPeers;
private Map<Tuple2<BtcNodes.BtcNode, Boolean>, Integer> btcNodeDownTimeMap = new HashMap<>();
private Map<Tuple2<BtcNodes.BtcNode, Boolean>, Integer> btcNodeUpTimeMap = new HashMap<>();
@Getter
private Set<NodeAddress> nodesInError = new HashSet<>();
@Inject
public MetricsModel(SeedNodeRepository seedNodeRepository,
BtcNodes btcNodes,
WalletsSetup walletsSetup,
@Named(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) String slackUrlSeedChannel,
@Named(MonitorOptionKeys.SLACK_BTC_SEED_CHANNEL) String slackUrlBtcChannel,
@Named(MonitorOptionKeys.SLACK_PROVIDER_SEED_CHANNEL) String slackUrlProviderChannel) {
this.seedNodeRepository = seedNodeRepository;
this.btcNodes = btcNodes;
if (!slackUrlSeedChannel.isEmpty())
slackSeedApi = new SlackApi(slackUrlSeedChannel);
if (!slackUrlBtcChannel.isEmpty())
slackBtcApi = new SlackApi(slackUrlBtcChannel);
if (!slackUrlProviderChannel.isEmpty())
slackProviderApi = new SlackApi(slackUrlProviderChannel);
walletsSetup.connectedPeersProperty().addListener((observable, oldValue, newValue) -> {
connectedPeers = newValue;
});
}
public void addToMap(NodeAddress nodeAddress, Metrics metrics) {
map.put(nodeAddress, metrics);
}
public Metrics getMetrics(NodeAddress nodeAddress) {
return map.get(nodeAddress);
}
public void updateReport() {
if (btcNodeUptimeTs == 0)
btcNodeUptimeTs = new Date().getTime();
Map<String, Double> accumulatedValues = new HashMap<>();
final double[] items = {0};
List<Map.Entry<NodeAddress, Metrics>> entryList = map.entrySet().stream()
.sorted(Comparator.comparing(entrySet -> seedNodeRepository.getOperator(entrySet.getKey())))
.collect(Collectors.toList());
totalErrors = 0;
entryList.stream().forEach(e -> {
totalErrors += e.getValue().errorMessages.stream().filter(s -> !s.isEmpty()).count();
final List<Map<String, Integer>> receivedObjectsList = e.getValue().getReceivedObjectsList();
if (!receivedObjectsList.isEmpty()) {
items[0] += 1;
Map<String, Integer> last = receivedObjectsList.get(receivedObjectsList.size() - 1);
last.entrySet().stream().forEach(e2 -> {
int accuValue = e2.getValue();
if (accumulatedValues.containsKey(e2.getKey()))
accuValue += accumulatedValues.get(e2.getKey());
accumulatedValues.put(e2.getKey(), (double) accuValue);
});
}
});
Map<String, Double> averageValues = new HashMap<>();
accumulatedValues.entrySet().stream().forEach(e -> {
averageValues.put(e.getKey(), e.getValue() / items[0]);
});
Calendar calendar = new GregorianCalendar();
calendar.setTimeZone(TimeZone.getTimeZone("CET"));
calendar.setTimeInMillis(lastCheckTs);
final String time = calendar.getTime().toString();
StringBuilder html = new StringBuilder();
html.append("<html>" +
"<head>" +
"<style>table, th, td {border: 1px solid black;}</style>" +
"</head>" +
"<body>" +
"<h3>")
.append("Seed nodes in error: <b>" + totalErrors + "</b><br/>" +
"Last check started at: " + time + "<br/></h3>" +
"<table style=\"width:100%\">" +
"<tr>" +
"<th align=\"left\">Operator</th>" +
"<th align=\"left\">Node address</th>" +
"<th align=\"left\">Total num requests</th>" +
"<th align=\"left\">Total num errors</th>" +
"<th align=\"left\">Last request</th>" +
"<th align=\"left\">Last response</th>" +
"<th align=\"left\">RRT average</th>" +
"<th align=\"left\">Num requests (retries)</th>" +
"<th align=\"left\">Last error message</th>" +
"<th align=\"left\">Last data</th>" +
"<th align=\"left\">Data deviation last request</th>" +
"</tr>");
StringBuilder sb = new StringBuilder();
sb.append("Seed nodes in error:" + totalErrors);
sb.append("\nLast check started at: " + time + "\n");
entryList.forEach(e -> {
final Metrics metrics = e.getValue();
final List<Long> allDurations = metrics.getRequestDurations();
final String allDurationsString = allDurations.stream().map(Object::toString).collect(Collectors.joining("<br/>"));
final OptionalDouble averageOptional = allDurations.stream().mapToLong(value -> value).average();
double durationAverage = 0;
if (averageOptional.isPresent())
durationAverage = averageOptional.getAsDouble() / 1000;
final NodeAddress nodeAddress = e.getKey();
final String operator = seedNodeRepository.getOperator(nodeAddress);
final List<String> errorMessages = metrics.getErrorMessages();
final int numErrors = (int) errorMessages.stream().filter(s -> !s.isEmpty()).count();
int numRequests = allDurations.size();
String lastErrorMsg = "";
int lastIndexOfError = 0;
for (int i = 0; i < errorMessages.size(); i++) {
final String msg = errorMessages.get(i);
if (!msg.isEmpty()) {
lastIndexOfError = i;
lastErrorMsg = "Error at request " + lastIndexOfError + ":" + msg;
}
}
// String lastErrorMsg = numErrors > 0 ? errorMessages.get(errorMessages.size() - 1) : "";
final List<Map<String, Integer>> allReceivedData = metrics.getReceivedObjectsList();
Map<String, Integer> lastReceivedData = !allReceivedData.isEmpty() ? allReceivedData.get(allReceivedData.size() - 1) : new HashMap<>();
final String lastReceivedDataString = lastReceivedData.entrySet().stream().map(Object::toString).collect(Collectors.joining("<br/>"));
final String allReceivedDataString = allReceivedData.stream().map(Object::toString).collect(Collectors.joining("<br/>"));
final String requestTs = metrics.getLastDataRequestTs() > 0 ? dateFormat.format(new Date(metrics.getLastDataRequestTs())) : "" + "<br/>";
final String responseTs = metrics.getLastDataResponseTs() > 0 ? dateFormat.format(new Date(metrics.getLastDataResponseTs())) : "" + "<br/>";
final String numRequestAttempts = metrics.getNumRequestAttempts() + "<br/>";
sb.append("\nOperator: ").append(operator)
.append("\nNode address: ").append(nodeAddress)
.append("\nTotal num requests: ").append(numRequests)
.append("\nTotal num errors: ").append(numErrors)
.append("\nLast request: ").append(requestTs)
.append("\nLast response: ").append(responseTs)
.append("\nRRT average: ").append(durationAverage)
.append("\nNum requests (retries): ").append(numRequestAttempts)
.append("\nLast error message: ").append(lastErrorMsg)
.append("\nLast data: ").append(lastReceivedDataString);
String colorNumErrors = lastIndexOfError == numErrors ? "black" : "red";
String colorDurationAverage = durationAverage < 30 ? "black" : "red";
html.append("<tr>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + operator + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + nodeAddress + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + numRequests + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + numErrors + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + requestTs + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + responseTs + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorDurationAverage + "\">" + durationAverage + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + numRequestAttempts + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + lastErrorMsg + "</font> ").append("</td>")
.append("<td>").append(lastReceivedDataString).append("</td><td>");
if (!allReceivedData.isEmpty()) {
sb.append("\nData deviation last request:\n");
lastReceivedData.entrySet().stream().forEach(e2 -> {
final String dataItem = e2.getKey();
double deviation = MathUtils.roundDouble((double) e2.getValue() / averageValues.get(dataItem) * 100, 2);
String str = dataItem + ": " + deviation + "%";
sb.append(str).append("\n");
String color;
final double devAbs = Math.abs(deviation - 100);
if (devAbs < 5)
color = "black";
else if (devAbs < 10)
color = "blue";
else
color = "red";
html.append("<font color=\"" + color + "\">" + str + "</font>").append("<br/>");
if (devAbs >= 20) {
if (slackSeedApi != null)
slackSeedApi.call(new SlackMessage("Warning: " + nodeAddress.getFullAddress(),
"<" + seedNodeRepository.getOperator(nodeAddress) + ">" + " Your seed node delivers diverging results for " + dataItem + ". " +
"Please check the monitoring status page at http://seedmonitor.0-2-1.net:8080/"));
}
});
sb.append("Duration all requests: ").append(allDurationsString)
.append("\nAll data: ").append(allReceivedDataString).append("\n");
html.append("</td></tr>");
}
});
html.append("</table>");
// btc nodes
sb.append("\n\n####################################\n\nBitcoin nodes\n");
final long elapsed = new Date().getTime() - btcNodeUptimeTs;
Set<String> connectedBtcPeers = connectedPeers.stream()
.map(e -> {
String hostname = e.getAddress().getHostname();
InetAddress inetAddress = e.getAddress().getAddr();
int port = e.getAddress().getPort();
if (hostname != null)
return hostname + ":" + port;
else if (inetAddress != null)
return inetAddress.getHostAddress() + ":" + port;
else
return "";
})
.collect(Collectors.toSet());
List<BtcNodes.BtcNode> onionBtcNodes = new ArrayList<>(btcNodes.getProvidedBtcNodes().stream()
.filter(BtcNodes.BtcNode::hasOnionAddress)
.collect(Collectors.toSet()));
onionBtcNodes.sort((o1, o2) -> o1.getOperator() != null && o2.getOperator() != null ?
o1.getOperator().compareTo(o2.getOperator()) : 0);
printTableHeader(html, "Onion");
printTable(html, sb, onionBtcNodes, connectedBtcPeers, elapsed, true);
html.append("</tr></table>");
List<BtcNodes.BtcNode> clearNetNodes = new ArrayList<>(btcNodes.getProvidedBtcNodes().stream()
.filter(BtcNodes.BtcNode::hasClearNetAddress)
.collect(Collectors.toSet()));
clearNetNodes.sort((o1, o2) -> o1.getOperator() != null && o2.getOperator() != null ?
o1.getOperator().compareTo(o2.getOperator()) : 0);
printTableHeader(html, "Clear net");
printTable(html, sb, clearNetNodes, connectedBtcPeers, elapsed, false);
sb.append("\nConnected Bitcoin nodes: " + connectedBtcPeers + "\n");
html.append("</tr></table>");
html.append("<br>Connected Bitcoin nodes: " + connectedBtcPeers + "<br>");
btcNodeUptimeTs = new Date().getTime();
html.append("</body></html>");
resultAsString = sb.toString();
resultAsHtml = html.toString();
}
private void printTableHeader(StringBuilder html, String type) {
html.append("<br><h3>Bitcoin " + type + " nodes<h3><table style=\"width:100%\">" +
"<tr>" +
"<th align=\"left\">Operator</th>" +
"<th align=\"left\">Domain name</th>" +
"<th align=\"left\">IP address</th>" +
"<th align=\"left\">Btc node onion address</th>" +
"<th align=\"left\">UpTime</th>" +
"<th align=\"left\">DownTime</th>" +
"</tr>");
}
private void printTable(StringBuilder html, StringBuilder sb, List<BtcNodes.BtcNode> allBtcNodes, Set<String> connectedBtcPeers, long elapsed, boolean isOnion) {
allBtcNodes.stream().forEach(node -> {
int upTime = 0;
int downTime = 0;
Tuple2<BtcNodes.BtcNode, Boolean> key = new Tuple2<>(node, isOnion);
if (btcNodeUpTimeMap.containsKey(key))
upTime = btcNodeUpTimeMap.get(key);
key = new Tuple2<>(node, isOnion);
if (btcNodeDownTimeMap.containsKey(key))
downTime = btcNodeDownTimeMap.get(key);
boolean isConnected = false;
// return !connectedBtcPeers.contains(host);
if (node.hasOnionAddress() && connectedBtcPeers.contains(node.getOnionAddress() + ":" + node.getPort()))
isConnected = true;
final String clearNetHost = node.getAddress() != null ? node.getAddress() + ":" + node.getPort() : node.getHostName() + ":" + node.getPort();
if (node.hasClearNetAddress() && connectedBtcPeers.contains(clearNetHost))
isConnected = true;
if (isConnected) {
upTime += elapsed;
btcNodeUpTimeMap.put(key, upTime);
} else {
downTime += elapsed;
btcNodeDownTimeMap.put(key, downTime);
}
String upTimeString = formatDurationAsWords(upTime, true);
String downTimeString = formatDurationAsWords(downTime, true);
String colorNumErrors = isConnected ? "black" : "red";
html.append("<tr>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getOperator() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getHostName() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getAddress() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + node.getOnionAddress() + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + upTimeString + "</font> ").append("</td>")
.append("<td>").append("<font color=\"" + colorNumErrors + "\">" + downTimeString + "</font> ").append("</td>");
sb.append("\nOperator: ").append(node.getOperator()).append("\n");
sb.append("Domain name: ").append(node.getHostName()).append("\n");
sb.append("IP address: ").append(node.getAddress()).append("\n");
sb.append("Btc node onion address: ").append(node.getOnionAddress()).append("\n");
sb.append("UpTime: ").append(upTimeString).append("\n");
sb.append("DownTime: ").append(downTimeString).append("\n");
});
}
public void log() {
log.info("\n\n#################################################################\n" +
resultAsString +
"#################################################################\n\n");
}
public static String formatDurationAsWords(long durationMillis, boolean showSeconds) {
String format;
String second = Res.get("time.second");
String minute = Res.get("time.minute");
String hour = Res.get("time.hour").toLowerCase();
String day = Res.get("time.day").toLowerCase();
String days = Res.get("time.days");
String hours = Res.get("time.hours");
String minutes = Res.get("time.minutes");
String seconds = Res.get("time.seconds");
if (showSeconds) {
format = "d\' " + days + ", \'H\' " + hours + ", \'m\' " + minutes + ", \'s\' " + seconds + "\'";
} else
format = "d\' " + days + ", \'H\' " + hours + ", \'m\' " + minutes + "\'";
String duration = DurationFormatUtils.formatDuration(durationMillis, format);
String tmp;
duration = " " + duration;
tmp = StringUtils.replaceOnce(duration, " 0 " + days, "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 " + hours, "");
if (tmp.length() != duration.length()) {
tmp = StringUtils.replaceOnce(tmp, " 0 " + minutes, "");
duration = tmp;
if (tmp.length() != tmp.length()) {
duration = StringUtils.replaceOnce(tmp, " 0 " + seconds, "");
}
}
}
if (duration.length() != 0) {
duration = duration.substring(1);
}
tmp = StringUtils.replaceOnce(duration, " 0 " + seconds, "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 " + minutes, "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 " + hours, "");
if (tmp.length() != duration.length()) {
duration = StringUtils.replaceOnce(tmp, " 0 " + days, "");
}
}
}
duration = " " + duration;
duration = StringUtils.replaceOnce(duration, " 1 " + seconds, " 1 " + second);
duration = StringUtils.replaceOnce(duration, " 1 " + minutes, " 1 " + minute);
duration = StringUtils.replaceOnce(duration, " 1 " + hours, " 1 " + hour);
duration = StringUtils.replaceOnce(duration, " 1 " + days, " 1 " + day);
duration = duration.trim();
if (duration.equals(","))
duration = duration.replace(",", "");
if (duration.startsWith(" ,"))
duration = duration.replace(" ,", "");
else if (duration.startsWith(", "))
duration = duration.replace(", ", "");
return duration;
}
public void addNodesInError(NodeAddress nodeAddress) {
nodesInError.add(nodeAddress);
}
public void removeNodesInError(NodeAddress nodeAddress) {
nodesInError.remove(nodeAddress);
}
}

View File

@ -1,89 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metrics.p2p;
import bisq.monitor.metrics.MetricsModel;
import bisq.network.NetworkOptionKeys;
import bisq.network.Socks5ProxyProvider;
import bisq.network.p2p.NetworkNodeProvider;
import bisq.network.p2p.P2PModule;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.BanList;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.network.p2p.peers.getdata.RequestDataManager;
import bisq.network.p2p.peers.keepalive.KeepAliveManager;
import bisq.network.p2p.peers.peerexchange.PeerExchangeManager;
import bisq.network.p2p.storage.P2PDataStorage;
import org.springframework.core.env.Environment;
import com.google.inject.Singleton;
import com.google.inject.name.Names;
import java.io.File;
import static com.google.inject.name.Names.named;
public class MonitorP2PModule extends P2PModule {
public MonitorP2PModule(Environment environment) {
super(environment);
}
@Override
protected void configure() {
bind(MetricsModel.class).in(Singleton.class);
bind(MonitorP2PService.class).in(Singleton.class);
bind(PeerManager.class).in(Singleton.class);
bind(P2PDataStorage.class).in(Singleton.class);
bind(RequestDataManager.class).in(Singleton.class);
bind(PeerExchangeManager.class).in(Singleton.class);
bind(KeepAliveManager.class).in(Singleton.class);
bind(Broadcaster.class).in(Singleton.class);
bind(BanList.class).in(Singleton.class);
bind(NetworkNode.class).toProvider(NetworkNodeProvider.class).in(Singleton.class);
bind(Socks5ProxyProvider.class).in(Singleton.class);
Boolean useLocalhostForP2P = environment.getProperty(NetworkOptionKeys.USE_LOCALHOST_FOR_P2P, boolean.class, false);
bind(boolean.class).annotatedWith(Names.named(NetworkOptionKeys.USE_LOCALHOST_FOR_P2P)).toInstance(useLocalhostForP2P);
File torDir = new File(environment.getRequiredProperty(NetworkOptionKeys.TOR_DIR));
bind(File.class).annotatedWith(named(NetworkOptionKeys.TOR_DIR)).toInstance(torDir);
// use a fixed port as arbitrator use that for his ID
Integer port = environment.getProperty(NetworkOptionKeys.PORT_KEY, int.class, 9999);
bind(int.class).annotatedWith(Names.named(NetworkOptionKeys.PORT_KEY)).toInstance(port);
Integer maxConnections = environment.getProperty(NetworkOptionKeys.MAX_CONNECTIONS, int.class, P2PService.MAX_CONNECTIONS_DEFAULT);
bind(int.class).annotatedWith(Names.named(NetworkOptionKeys.MAX_CONNECTIONS)).toInstance(maxConnections);
Integer networkId = environment.getProperty(NetworkOptionKeys.NETWORK_ID, int.class, 1);
bind(int.class).annotatedWith(Names.named(NetworkOptionKeys.NETWORK_ID)).toInstance(networkId);
bindConstant().annotatedWith(named(NetworkOptionKeys.SEED_NODES_KEY)).to(environment.getRequiredProperty(NetworkOptionKeys.SEED_NODES_KEY));
bindConstant().annotatedWith(named(NetworkOptionKeys.MY_ADDRESS)).to(environment.getRequiredProperty(NetworkOptionKeys.MY_ADDRESS));
bindConstant().annotatedWith(named(NetworkOptionKeys.BAN_LIST)).to(environment.getRequiredProperty(NetworkOptionKeys.BAN_LIST));
bindConstant().annotatedWith(named(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS)).to(environment.getRequiredProperty(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS));
bindConstant().annotatedWith(named(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS)).to(environment.getRequiredProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS));
}
}

View File

@ -1,129 +0,0 @@
/*
* This file is part of Bisq.
*
* bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metrics.p2p;
import bisq.network.Socks5ProxyProvider;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.network.SetupListener;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.app.Log;
import bisq.common.proto.persistable.PersistedDataHost;
import javax.inject.Inject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class MonitorP2PService implements SetupListener, PersistedDataHost {
private final NetworkNode networkNode;
@Getter
private final P2PDataStorage p2PDataStorage;
private final MonitorRequestManager requestDataManager;
private final Socks5ProxyProvider socks5ProxyProvider;
private SetupListener listener;
private volatile boolean shutDownInProgress;
private boolean shutDownComplete;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public MonitorP2PService(NetworkNode networkNode,
P2PDataStorage p2PDataStorage,
MonitorRequestManager requestDataManager,
Socks5ProxyProvider socks5ProxyProvider) {
this.networkNode = networkNode;
this.p2PDataStorage = p2PDataStorage;
this.requestDataManager = requestDataManager;
this.socks5ProxyProvider = socks5ProxyProvider;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void readPersisted() {
p2PDataStorage.readPersisted();
}
public void start(SetupListener listener) {
this.listener = listener;
networkNode.start(this);
}
public void shutDown(Runnable shutDownCompleteHandler) {
Log.traceCall();
if (!shutDownInProgress) {
shutDownInProgress = true;
if (requestDataManager != null) {
requestDataManager.shutDown();
}
if (networkNode != null) {
networkNode.shutDown(() -> {
shutDownComplete = true;
});
} else {
shutDownComplete = true;
}
} else {
log.debug("shutDown already in progress");
if (shutDownComplete) {
shutDownCompleteHandler.run();
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// SetupListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onTorNodeReady() {
socks5ProxyProvider.setSocks5ProxyInternal(networkNode);
listener.onTorNodeReady();
}
@Override
public void onHiddenServicePublished() {
checkArgument(networkNode.getNodeAddress() != null, "Address must be set when we have the hidden service ready");
requestDataManager.start();
listener.onHiddenServicePublished();
}
@Override
public void onSetupFailed(Throwable throwable) {
listener.onSetupFailed(throwable);
}
@Override
public void onRequestCustomBridges() {
listener.onRequestCustomBridges();
}
}

View File

@ -1,300 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metrics.p2p;
import bisq.monitor.metrics.Metrics;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.getdata.messages.GetDataRequest;
import bisq.network.p2p.peers.getdata.messages.GetDataResponse;
import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.app.Log;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.network.NetworkPayload;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@Slf4j
class MonitorRequestHandler implements MessageListener {
private static final long TIMEOUT = 120;
private NodeAddress peersNodeAddress;
private long requestTs;
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onComplete();
@SuppressWarnings("UnusedParameters")
void onFault(String errorMessage, NodeAddress nodeAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final NetworkNode networkNode;
private final P2PDataStorage dataStorage;
private final Metrics metrics;
private final Listener listener;
private Timer timeoutTimer;
private final int nonce = new Random().nextInt();
private boolean stopped;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public MonitorRequestHandler(NetworkNode networkNode, P2PDataStorage dataStorage, Metrics metrics, Listener listener) {
this.networkNode = networkNode;
this.dataStorage = dataStorage;
this.metrics = metrics;
this.listener = listener;
}
public void cancel() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestData(NodeAddress nodeAddress) {
Log.traceCall("nodeAddress=" + nodeAddress);
peersNodeAddress = nodeAddress;
requestTs = new Date().getTime();
if (!stopped) {
Set<byte[]> excludedKeys = dataStorage.getAppendOnlyDataStoreMap().entrySet().stream()
.map(e -> e.getKey().bytes)
.collect(Collectors.toSet());
GetDataRequest getDataRequest = new PreliminaryGetDataRequest(nonce, excludedKeys);
metrics.setLastDataRequestTs(System.currentTimeMillis());
if (timeoutTimer != null) {
log.warn("timeoutTimer was already set. That must not happen.");
timeoutTimer.stop();
if (DevEnv.isDevMode())
throw new RuntimeException("timeoutTimer was already set. That must not happen.");
}
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
if (!stopped) {
String errorMessage = "A timeout occurred at sending getDataRequest:" + getDataRequest +
" on nodeAddress:" + nodeAddress;
log.warn(errorMessage + " / RequestDataHandler=" + MonitorRequestHandler.this);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT);
} else {
log.trace("We have stopped already. We ignore that timeoutTimer.run call. " +
"Might be caused by an previous networkNode.sendMessage.onFailure.");
}
},
TIMEOUT);
log.info("We send a PreliminaryGetDataRequest to peer {}. ", nodeAddress);
networkNode.addMessageListener(this);
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getDataRequest);
Futures.addCallback(future, new FutureCallback<Connection>() {
@Override
public void onSuccess(Connection connection) {
if (!stopped) {
log.info("Send PreliminaryGetDataRequest to " + nodeAddress + " has succeeded.");
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." +
"Might be caused by an previous timeout.");
}
}
@Override
public void onFailure(@NotNull Throwable throwable) {
if (!stopped) {
String errorMessage = "Sending getDataRequest to " + nodeAddress +
" failed.\n\t" +
"getDataRequest=" + getDataRequest + "." +
"\n\tException=" + throwable.getMessage();
log.warn(errorMessage);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE);
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " +
"Might be caused by an previous timeout.");
}
}
});
} else {
log.warn("We have stopped already. We ignore that requestData call.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// MessageListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onMessage(NetworkEnvelope networkEnvelop, Connection connection) {
if (networkEnvelop instanceof GetDataResponse &&
connection.getPeersNodeAddressOptional().isPresent() &&
connection.getPeersNodeAddressOptional().get().equals(peersNodeAddress)) {
Log.traceCall(networkEnvelop.toString() + "\n\tconnection=" + connection);
if (!stopped) {
GetDataResponse getDataResponse = (GetDataResponse) networkEnvelop;
if (getDataResponse.getRequestNonce() == nonce) {
stopTimeoutTimer();
Map<String, Set<NetworkPayload>> payloadByClassName = new HashMap<>();
final Set<ProtectedStorageEntry> dataSet = getDataResponse.getDataSet();
dataSet.stream().forEach(e -> {
final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload();
if (protectedStoragePayload == null) {
log.warn("StoragePayload was null: {}", networkEnvelop.toString());
return;
}
// For logging different data types
String className = protectedStoragePayload.getClass().getSimpleName();
if (!payloadByClassName.containsKey(className))
payloadByClassName.put(className, new HashSet<>());
payloadByClassName.get(className).add(protectedStoragePayload);
});
Set<PersistableNetworkPayload> persistableNetworkPayloadSet = getDataResponse.getPersistableNetworkPayloadSet();
if (persistableNetworkPayloadSet != null) {
persistableNetworkPayloadSet.stream().forEach(persistableNetworkPayload -> {
// For logging different data types
String className = persistableNetworkPayload.getClass().getSimpleName();
if (!payloadByClassName.containsKey(className))
payloadByClassName.put(className, new HashSet<>());
payloadByClassName.get(className).add(persistableNetworkPayload);
});
}
// Log different data types
StringBuilder sb = new StringBuilder();
sb.append("\n#################################################################\n");
sb.append("Connected to node: ").append(peersNodeAddress.getFullAddress()).append("\n");
final int items = dataSet.size() +
(persistableNetworkPayloadSet != null ? persistableNetworkPayloadSet.size() : 0);
sb.append("Received ").append(items).append(" instances\n");
Map<String, Integer> receivedObjects = new HashMap<>();
final boolean[] arbitratorReceived = new boolean[1];
payloadByClassName.entrySet().stream().forEach(e -> {
final String dataItemName = e.getKey();
// We expect always at least an Arbitrator
if (!arbitratorReceived[0] && dataItemName.equals("Arbitrator"))
arbitratorReceived[0] = true;
sb.append(dataItemName)
.append(": ")
.append(e.getValue().size())
.append("\n");
receivedObjects.put(dataItemName, e.getValue().size());
});
sb.append("#################################################################");
log.info(sb.toString());
metrics.getReceivedObjectsList().add(receivedObjects);
final long duration = new Date().getTime() - requestTs;
log.info("Requesting data took {} ms", duration);
metrics.getRequestDurations().add(duration);
metrics.getErrorMessages().add(arbitratorReceived[0] ? "" : "No Arbitrator objects received! Seed node need to be restarted!");
metrics.setLastDataResponseTs(System.currentTimeMillis());
cleanup();
connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER, listener::onComplete);
} else {
log.debug("Nonce not matching. That can happen rarely if we get a response after a canceled " +
"handshake (timeout causes connection close but peer might have sent a msg before " +
"connection was closed).\n\t" +
"We drop that message. nonce={} / requestNonce={}",
nonce, getDataResponse.getRequestNonce());
}
} else {
log.warn("We have stopped already. We ignore that onDataRequest call.");
}
}
}
public void stop() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void handleFault(String errorMessage, NodeAddress nodeAddress, CloseConnectionReason closeConnectionReason) {
cleanup();
// We do not log every error only if it fails several times in a row.
// We do not close the connection as it might be we have opened a new connection for that peer and
// we don't want to close that. We do not know the connection at fault as the fault handler does not contain that,
// so we could only search for connections for that nodeAddress but that would close an new connection attempt.
listener.onFault(errorMessage, nodeAddress);
}
private void cleanup() {
Log.traceCall();
stopped = true;
networkNode.removeMessageListener(this);
stopTimeoutTimer();
}
private void stopTimeoutTimer() {
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
}

View File

@ -1,283 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.metrics.p2p;
import bisq.monitor.MonitorOptionKeys;
import bisq.monitor.metrics.Metrics;
import bisq.monitor.metrics.MetricsModel;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.ConnectionListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.seed.SeedNodeRepository;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.Timer;
import bisq.common.UserThread;
import net.gpedro.integrations.slack.SlackApi;
import net.gpedro.integrations.slack.SlackMessage;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MonitorRequestManager implements ConnectionListener {
private static final long RETRY_DELAY_SEC = 30;
private static final long CLEANUP_TIMER = 60;
private static final long REQUEST_PERIOD_MIN = 10;
private static final int MAX_RETRIES = 5;
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final NetworkNode networkNode;
private final int numNodes;
private SlackApi slackApi;
private P2PDataStorage dataStorage;
private SeedNodeRepository seedNodeRepository;
private MetricsModel metricsModel;
private final Set<NodeAddress> seedNodeAddresses;
private final Map<NodeAddress, MonitorRequestHandler> handlerMap = new HashMap<>();
private Map<NodeAddress, Timer> retryTimerMap = new HashMap<>();
private Map<NodeAddress, Integer> retryCounterMap = new HashMap<>();
private boolean stopped;
private int completedRequestIndex;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public MonitorRequestManager(NetworkNode networkNode,
P2PDataStorage dataStorage,
SeedNodeRepository seedNodeRepository,
MetricsModel metricsModel,
@Named(MonitorOptionKeys.SLACK_URL_SEED_CHANNEL) String slackUrlSeedChannel) {
this.networkNode = networkNode;
this.dataStorage = dataStorage;
this.seedNodeRepository = seedNodeRepository;
this.metricsModel = metricsModel;
if (!slackUrlSeedChannel.isEmpty())
slackApi = new SlackApi(slackUrlSeedChannel);
this.networkNode.addConnectionListener(this);
seedNodeAddresses = new HashSet<>(seedNodeRepository.getSeedNodeAddresses());
seedNodeAddresses.stream().forEach(nodeAddress -> metricsModel.addToMap(nodeAddress, new Metrics()));
numNodes = seedNodeAddresses.size();
}
public void shutDown() {
stopped = true;
stopAllRetryTimers();
networkNode.removeConnectionListener(this);
closeAllHandlers();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void start() {
requestAllNodes();
UserThread.runPeriodically(this::requestAllNodes, REQUEST_PERIOD_MIN, TimeUnit.MINUTES);
// We want to update the data for the btc nodes more frequently
UserThread.runPeriodically(metricsModel::updateReport, 10);
}
private void requestAllNodes() {
stopAllRetryTimers();
closeAllConnections();
// we give 1 sec. for all connection shutdown
final int[] delay = {1000};
metricsModel.setLastCheckTs(System.currentTimeMillis());
seedNodeAddresses.stream().forEach(nodeAddress -> {
UserThread.runAfter(() -> requestFromNode(nodeAddress), delay[0], TimeUnit.MILLISECONDS);
delay[0] += 100;
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// ConnectionListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConnection(Connection connection) {
}
@Override
public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) {
closeHandler(connection);
}
@Override
public void onError(Throwable throwable) {
}
///////////////////////////////////////////////////////////////////////////////////////////
// RequestData
///////////////////////////////////////////////////////////////////////////////////////////
private void requestFromNode(NodeAddress nodeAddress) {
if (!stopped) {
if (!handlerMap.containsKey(nodeAddress)) {
final Metrics metrics = metricsModel.getMetrics(nodeAddress);
MonitorRequestHandler requestDataHandler = new MonitorRequestHandler(networkNode,
dataStorage,
metrics,
new MonitorRequestHandler.Listener() {
@Override
public void onComplete() {
log.trace("RequestDataHandshake of outbound connection complete. nodeAddress={}",
nodeAddress);
stopRetryTimer(nodeAddress);
retryCounterMap.remove(nodeAddress);
metrics.setNumRequestAttempts(retryCounterMap.getOrDefault(nodeAddress, 1));
// need to remove before listeners are notified as they cause the update call
handlerMap.remove(nodeAddress);
metricsModel.updateReport();
completedRequestIndex++;
if (completedRequestIndex == numNodes)
metricsModel.log();
if (metricsModel.getNodesInError().contains(nodeAddress)) {
metricsModel.removeNodesInError(nodeAddress);
if (slackApi != null)
slackApi.call(new SlackMessage("Fixed: " + nodeAddress.getFullAddress(),
"<" + seedNodeRepository.getOperator(nodeAddress) + ">" + " Your seed node is recovered."));
}
}
@Override
public void onFault(String errorMessage, NodeAddress nodeAddress) {
handlerMap.remove(nodeAddress);
stopRetryTimer(nodeAddress);
int retryCounter = retryCounterMap.getOrDefault(nodeAddress, 0);
metrics.setNumRequestAttempts(retryCounter);
if (retryCounter < MAX_RETRIES) {
log.info("We got an error at peer={}. We will try again after a delay of {} sec. error={} ",
nodeAddress, RETRY_DELAY_SEC, errorMessage);
final Timer timer = UserThread.runAfter(() -> requestFromNode(nodeAddress), RETRY_DELAY_SEC);
retryTimerMap.put(nodeAddress, timer);
retryCounterMap.put(nodeAddress, ++retryCounter);
} else {
log.warn("We got repeated errors at peer={}. error={} ",
nodeAddress, errorMessage);
metricsModel.addNodesInError(nodeAddress);
metrics.getErrorMessages().add(errorMessage + " (" + new Date().toString() + ")");
metricsModel.updateReport();
completedRequestIndex++;
if (completedRequestIndex == numNodes)
metricsModel.log();
retryCounterMap.remove(nodeAddress);
if (slackApi != null)
slackApi.call(new SlackMessage("Error: " + nodeAddress.getFullAddress(),
"<" + seedNodeRepository.getOperator(nodeAddress) + ">" + " Your seed node failed " + RETRY_DELAY_SEC + " times with error message: " + errorMessage));
}
}
});
handlerMap.put(nodeAddress, requestDataHandler);
requestDataHandler.requestData(nodeAddress);
} else {
log.warn("We have started already a requestDataHandshake to peer. nodeAddress=" + nodeAddress + "\n" +
"We start a cleanup timer if the handler has not closed by itself in between 2 minutes.");
UserThread.runAfter(() -> {
if (handlerMap.containsKey(nodeAddress)) {
MonitorRequestHandler handler = handlerMap.get(nodeAddress);
handler.stop();
handlerMap.remove(nodeAddress);
}
}, CLEANUP_TIMER);
}
} else {
log.warn("We have stopped already. We ignore that requestData call.");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void closeAllConnections() {
networkNode.getAllConnections().stream().forEach(connection -> connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER));
}
private void stopAllRetryTimers() {
retryTimerMap.values().stream().forEach(Timer::stop);
retryTimerMap.clear();
retryCounterMap.clear();
}
private void stopRetryTimer(NodeAddress nodeAddress) {
retryTimerMap.entrySet().stream()
.filter(e -> e.getKey().equals(nodeAddress))
.forEach(e -> e.getValue().stop());
retryTimerMap.remove(nodeAddress);
}
private void closeHandler(Connection connection) {
Optional<NodeAddress> peersNodeAddressOptional = connection.getPeersNodeAddressOptional();
if (peersNodeAddressOptional.isPresent()) {
NodeAddress nodeAddress = peersNodeAddressOptional.get();
if (handlerMap.containsKey(nodeAddress)) {
handlerMap.get(nodeAddress).cancel();
handlerMap.remove(nodeAddress);
}
} else {
log.trace("closeRequestDataHandler: nodeAddress not set in connection " + connection);
}
}
private void closeAllHandlers() {
handlerMap.values().stream().forEach(MonitorRequestHandler::cancel);
handlerMap.clear();
}
}

View File

@ -0,0 +1,61 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.reporter;
import bisq.monitor.Reporter;
import java.util.HashMap;
import java.util.Map;
/**
* A simple console reporter.
*
* @author Florian Reimair
*/
public class ConsoleReporter extends Reporter {
@Override
public void report(long value, String prefix) {
HashMap<String, String> result = new HashMap<>();
result.put("", String.valueOf(value));
report(result, "bisq");
}
@Override
public void report(long value) {
HashMap<String, String> result = new HashMap<>();
result.put("", String.valueOf(value));
report(result);
}
@Override
public void report(Map<String, String> values, String prefix) {
long timestamp = System.currentTimeMillis();
values.forEach((key, value) -> {
String report = prefix + ("".equals(key) ? "" : (prefix.isEmpty() ? "" : ".") + key) + " " + value + " "
+ timestamp;
System.err.println("Report: " + report);
});
}
@Override
public void report(Map<String, String> values) {
report(values, "bisq");
}
}

View File

@ -0,0 +1,77 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor.reporter;
import bisq.monitor.Reporter;
import org.berndpruenster.netlayer.tor.TorSocket;
import java.net.URL;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Reports our findings to a graphite service.
*
* @author Florian Reimair
*/
public class GraphiteReporter extends Reporter {
@Override
public void report(long value, String prefix) {
HashMap<String, String> result = new HashMap<>();
result.put("", String.valueOf(value));
report(result, prefix);
}
@Override
public void report(long value) {
report(value, "bisq");
}
@Override
public void report(Map<String, String> values, String prefix) {
long timestamp = System.currentTimeMillis() / 1000;
values.forEach((key, value) -> {
String report = prefix + ("".equals(key) ? "" : (prefix.isEmpty() ? "" : ".") + key) + " " + value + " "
+ timestamp + "\n";
URL url;
try {
url = new URL(configuration.getProperty("serviceUrl"));
TorSocket socket = new TorSocket(url.getHost(), url.getPort());
socket.getOutputStream().write(report.getBytes());
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
@Override
public void report(Map<String, String> values) {
report(values, "bisq");
}
}

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="bisq.common.storage.Storage" level="WARN"/>
<logger name="bisq.common.storage.FileManager" level="WARN"/>
<logger name="com.neemre.btcdcli4j" level="WARN"/>
<logger name="com.msopentech.thali.toronionproxy.OnionProxyManagerEventHandler" level="INFO"/>
</configuration>

View File

@ -0,0 +1,35 @@
## Each Metric is configured via a set of properties.
##
## The minimal set of properties required to run a Metric is:
##
## YourMetricName.enabled=true|false
## YourMetricName.run.interval=10 [seconds]
#Edit and uncomment the lines below for your liking
#TorStartupTime Metric
TorStartupTime.enabled=true
TorStartupTime.run.interval=100
TorStartupTime.run.socksPort=90500
TorRoundTripTime.enabled=true
TorRoundTripTime.run.interval=100
TorRoundTripTime.run.sampleSize=3
# torproject.org hidden service
TorRoundTripTime.run.hosts=http://expyuzz4wqqyqhjn.onion:80
#TorHiddenServiceStartupTime Metric
TorHiddenServiceStartupTime.enabled=true
TorHiddenServiceStartupTime.run.interval=100
TorHiddenServiceStartupTime.run.localPort=90501
TorHiddenServiceStartupTime.run.servicePort=90511
#Another Metric
Another.run.interval=5
## Reporters are configured via a set of properties as well.
##
## In contrast to Metrics, Reporters do not have a minimal set of properties.
#GraphiteReporter
GraphiteReporter.serviceUrl=http://k6evlhg44acpchtc.onion:2003

View File

@ -0,0 +1,127 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.reporter.ConsoleReporter;
import java.util.HashMap;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class MonitorInfrastructureTests {
/**
* A dummy metric for development purposes.
*/
public class Dummy extends Metric {
public Dummy() {
super(new ConsoleReporter());
}
public boolean active() {
return enabled();
}
@Override
protected void execute() {
// do nothing
}
}
@ParameterizedTest
@ValueSource(strings = { "empty", "no interval", "typo" })
public void basicConfigurationError(String configuration) {
HashMap<String, Properties> lut = new HashMap<>();
lut.put("empty", new Properties());
Properties noInterval = new Properties();
noInterval.put("Dummy.enabled", "true");
lut.put("no interval", noInterval);
Properties typo = new Properties();
typo.put("Dummy.enabled", "true");
//noinspection SpellCheckingInspection
typo.put("Dummy.run.inteval", "1");
lut.put("typo", typo);
Dummy DUT = new Dummy();
DUT.configure(lut.get(configuration));
Assert.assertFalse(DUT.active());
}
@Test
public void basicConfigurationSuccess() throws Exception {
Properties correct = new Properties();
correct.put("Dummy.enabled", "true");
correct.put("Dummy.run.interval", "1");
Dummy DUT = new Dummy();
DUT.configure(correct);
Assert.assertTrue(DUT.active());
// graceful shutdown
DUT.shutdown();
DUT.join();
}
@Test
public void reloadConfig() throws InterruptedException {
// our dummy
Dummy DUT = new Dummy();
// a second dummy to run as well
Dummy DUT2 = new Dummy();
DUT2.setName("Dummy2");
Properties dummy2Properties = new Properties();
dummy2Properties.put("Dummy2.enabled", "true");
dummy2Properties.put("Dummy2.run.interval", "1");
DUT2.configure(dummy2Properties);
// disable
DUT.configure(new Properties());
Assert.assertFalse(DUT.active());
Assert.assertTrue(DUT2.active());
// enable
Properties properties = new Properties();
properties.put("Dummy.enabled", "true");
properties.put("Dummy.run.interval", "1");
DUT.configure(properties);
Assert.assertTrue(DUT.active());
Assert.assertTrue(DUT2.active());
// disable again
DUT.configure(new Properties());
Assert.assertFalse(DUT.active());
Assert.assertTrue(DUT2.active());
// enable again
DUT.configure(properties);
Assert.assertTrue(DUT.active());
Assert.assertTrue(DUT2.active());
// graceful shutdown
DUT.shutdown();
DUT.join();
DUT2.shutdown();
DUT2.join();
}
}

View File

@ -0,0 +1,110 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.TorHiddenServiceStartupTime;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import java.io.File;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static com.google.common.base.Preconditions.checkNotNull;
@Disabled // Ignore for normal test runs as the tests take lots of time
public class TorHiddenServiceStartupTimeTests {
private final static File torWorkingDirectory = new File("monitor/" + TorHiddenServiceStartupTimeTests.class.getSimpleName());
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends Reporter {
private long result;
@Override
public void report(long value) {
result = value;
}
public long results() {
return result;
}
@Override
public void report(Map<String, String> values) {
report(Long.parseLong(values.values().iterator().next()));
}
@Override
public void report(Map<String, String> values, String prefix) {
report(values);
}
@Override
public void report(long value, String prefix) {
report(value);
}
}
@BeforeAll
public static void setup() throws TorCtlException {
// simulate the tor instance available to all metrics
Tor.setDefault(new NativeTor(torWorkingDirectory));
}
@Test
public void run() throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("TorHiddenServiceStartupTime.enabled", "true");
configuration.put("TorHiddenServiceStartupTime.run.interval", "5");
Metric DUT = new TorHiddenServiceStartupTime(reporter);
// start
DUT.configure(configuration);
// give it some time and then stop
Thread.sleep(180 * 1000);
DUT.shutdown();
// observe results
Assert.assertTrue(reporter.results() > 0);
}
@AfterAll
public static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
torWorkingDirectory.delete();
}
}

View File

@ -0,0 +1,138 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.TorRoundTripTime;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import java.io.File;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Test the round trip time metric against the hidden service of tor project.org.
*
* @author Florian Reimair
*/
@Disabled // Ignore for normal test runs as the tests take lots of time
public class TorRoundTripTimeTests {
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends Reporter {
private Map<String, String> results;
@Override
public void report(long value) {
Assert.fail();
}
public Map<String, String> hasResults() {
return results;
}
@Override
public void report(Map<String, String> values) {
results = values;
}
@Override
public void report(Map<String, String> values, String prefix) {
report(values);
}
@Override
public void report(long value, String prefix) {
report(value);
}
}
private static final File workingDirectory = new File(TorRoundTripTimeTests.class.getSimpleName());
@BeforeAll
public static void setup() throws TorCtlException {
// simulate the tor instance available to all metrics
Tor.setDefault(new NativeTor(workingDirectory));
}
@ParameterizedTest
@ValueSource(strings = {"default", "3", "4", "10"})
public void run(String sampleSize) throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("TorRoundTripTime.enabled", "true");
configuration.put("TorRoundTripTime.run.interval", "2");
if (!"default".equals(sampleSize))
configuration.put("TorRoundTripTime.run.sampleSize", sampleSize);
// torproject.org hidden service
configuration.put("TorRoundTripTime.run.hosts", "http://expyuzz4wqqyqhjn.onion:80");
Metric DUT = new TorRoundTripTime(reporter);
// start
DUT.configure(configuration);
// give it some time to start and then stop
Thread.sleep(100);
DUT.shutdown();
DUT.join();
// observe results
Map<String, String> results = reporter.hasResults();
Assert.assertFalse(results.isEmpty());
Assert.assertEquals(results.get("sampleSize"), sampleSize.equals("default") ? "1" : sampleSize);
Integer p25 = Integer.valueOf(results.get("p25"));
Integer p50 = Integer.valueOf(results.get("p50"));
Integer p75 = Integer.valueOf(results.get("p75"));
Integer min = Integer.valueOf(results.get("min"));
Integer max = Integer.valueOf(results.get("max"));
Integer average = Integer.valueOf(results.get("average"));
Assert.assertTrue(0 < min);
Assert.assertTrue(min <= p25 && p25 <= p50);
Assert.assertTrue(p50 <= p75);
Assert.assertTrue(p75 <= max);
Assert.assertTrue(min <= average && average <= max);
}
@AfterAll
public static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
workingDirectory.delete();
}
}

View File

@ -0,0 +1,87 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.monitor;
import bisq.monitor.metric.TorStartupTime;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled // Ignore for normal test runs as the tests take lots of time
public class TorStartupTimeTests {
/**
* A dummy Reporter for development purposes.
*/
private class DummyReporter extends Reporter {
private long result;
@Override
public void report(long value) {
result = value;
}
public long results() {
return result;
}
@Override
public void report(Map<String, String> values) {
report(Long.parseLong(values.values().iterator().next()));
}
@Override
public void report(Map<String, String> values, String prefix) {
report(values);
}
@Override
public void report(long value, String prefix) {
report(value);
}
}
@Test
public void run() throws Exception {
DummyReporter reporter = new DummyReporter();
// configure
Properties configuration = new Properties();
configuration.put("TorStartupTime.enabled", "true");
configuration.put("TorStartupTime.run.interval", "2");
configuration.put("TorStartupTime.run.socksPort", "9999");
Metric DUT = new TorStartupTime(reporter);
// start
DUT.configure(configuration);
// give it some time and then stop
Thread.sleep(15 * 1000);
DUT.shutdown();
// TODO Test fails due timing issue
// observe results
Assert.assertTrue(reporter.results() > 0);
}
}