package net.i2p.router;
/*
* free (adj.): unencumbered; not under the control of others
* Written by jrandom in 2003 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*
*/
import java.io.Writer;
import java.security.GeneralSecurityException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Properties;
import net.i2p.CoreVersion;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.crypto.FamilyKeyCrypto;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.stat.Rate;
import net.i2p.stat.RateStat;
import net.i2p.util.Log;
/**
* Publishes some statistics about the router in the netDB.
*
*/
public class StatisticsManager {
private final Log _log;
private final RouterContext _context;
private final String _networkID;
public final static String PROP_PUBLISH_RANKINGS = "router.publishPeerRankings";
private static final String PROP_CONTACT_NAME = "netdb.contact";
/** enhance anonymity by only including build stats one out of this many times */
private static final int RANDOM_INCLUDE_STATS = 16;
private final DecimalFormat _fmt;
private final DecimalFormat _pct;
public StatisticsManager(RouterContext context) {
_context = context;
_fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK));
_pct = new DecimalFormat("#0.00%", new DecimalFormatSymbols(Locale.UK));
_log = context.logManager().getLog(StatisticsManager.class);
_networkID = Integer.toString(context.router().getNetworkID());
}
/**
* Retrieve a snapshot of the statistics that should be published.
*
* This includes all standard options (as of 0.9.24, network ID and caps)
*/
public Properties publishStatistics() {
// if hash is null, will be caught in fkc.sign()
return publishStatistics(_context.routerHash());
}
/**
* Retrieve a snapshot of the statistics that should be published.
*
* This includes all standard options (as of 0.9.24, network ID and caps)
*
* @param h current router hash, non-null
* @since 0.9.24
*/
public Properties publishStatistics(Hash h) {
Properties stats = new Properties();
stats.setProperty("router.version", RouterVersion.VERSION);
// never used
//stats.setProperty("coreVersion", CoreVersion.VERSION);
stats.setProperty(RouterInfo.PROP_NETWORK_ID, _networkID);
stats.setProperty(RouterInfo.PROP_CAPABILITIES, _context.router().getCapabilities());
// No longer expose, to make build tracking more expensive
// stats.setProperty("router.id", RouterVersion.ID);
// stats.setProperty("core.id", CoreVersion.ID);
/***
int newlines = 0;
FileInputStream in = null;
try {
in = new FileInputStream(Router.IDENTLOG);
int c = -1;
// perhaps later filter this to only include ident changes this
// day/week/month
while ( (c = in.read()) != -1) {
if (c == '\n')
newlines++;
}
} catch (IOException ioe) {
// ignore
} finally {
if (in != null)
try { in.close(); } catch (IOException ioe) {}
}
if (newlines > 0)
stats.setProperty("stat_identities", newlines+"");
***/
if (_context.getBooleanPropertyDefaultTrue(PROP_PUBLISH_RANKINGS) &&
_context.random().nextInt(RANDOM_INCLUDE_STATS) == 0) {
//long publishedUptime = _context.router().getUptime();
// Don't publish these for first hour
// Disabled in 0.9
//if (publishedUptime > 62*60*1000)
// includeAverageThroughput(stats);
//includeRate("router.invalidMessageTime", stats, new long[] { 10*60*1000 });
//includeRate("router.duplicateMessageId", stats, new long[] { 24*60*60*1000 });
//includeRate("tunnel.duplicateIV", stats, new long[] { 24*60*60*1000 });
//includeRate("tunnel.fragmentedDropped", stats, new long[] { 10*60*1000, 3*60*60*1000 });
//includeRate("tunnel.fullFragments", stats, new long[] { 10*60*1000, 3*60*60*1000 });
//includeRate("tunnel.smallFragments", stats, new long[] { 10*60*1000, 3*60*60*1000 });
//includeRate("tunnel.testFailedTime", stats, new long[] { 10*60*1000 });
//includeRate("tunnel.batchDelaySent", stats, new long[] { 10*60*1000, 60*60*1000 });
//includeRate("tunnel.batchMultipleCount", stats, new long[] { 10*60*1000, 60*60*1000 });
//includeRate("tunnel.corruptMessage", stats, new long[] { 60*60*1000l, 3*60*60*1000l });
//includeRate("router.throttleTunnelProbTestSlow", stats, new long[] { 60*60*1000 });
//includeRate("router.throttleTunnelProbTooFast", stats, new long[] { 60*60*1000 });
//includeRate("router.throttleTunnelProcessingTime1m", stats, new long[] { 60*60*1000 });
//includeRate("router.fastPeers", stats, new long[] { 60*60*1000 });
//includeRate("udp.statusOK", stats, new long[] { 20*60*1000 });
//includeRate("udp.statusDifferent", stats, new long[] { 20*60*1000 });
//includeRate("udp.statusReject", stats, new long[] { 20*60*1000 });
//includeRate("udp.statusUnknown", stats, new long[] { 20*60*1000 });
//includeRate("udp.statusKnownCharlie", stats, new long[] { 1*60*1000, 10*60*1000 });
//includeRate("udp.addressUpdated", stats, new long[] { 1*60*1000 });
//includeRate("udp.addressTestInsteadOfUpdate", stats, new long[] { 1*60*1000 });
//includeRate("clock.skew", stats, new long[] { 10*60*1000, 3*60*60*1000, 24*60*60*1000 });
//includeRate("transport.sendProcessingTime", stats, new long[] { 60*60*1000 });
//includeRate("jobQueue.jobRunSlow", stats, new long[] { 10*60*1000l, 60*60*1000l });
//includeRate("crypto.elGamal.encrypt", stats, new long[] { 60*60*1000 });
// total event count can be used to track uptime
includeRate("tunnel.participatingTunnels", stats, new long[] { 60*60*1000 }, true);
//includeRate("tunnel.testSuccessTime", stats, new long[] { 10*60*1000l });
//includeRate("client.sendAckTime", stats, new long[] { 60*60*1000 }, true);
//includeRate("udp.sendConfirmTime", stats, new long[] { 10*60*1000 });
//includeRate("udp.sendVolleyTime", stats, new long[] { 10*60*1000 });
//includeRate("udp.ignoreRecentDuplicate", stats, new long[] { 60*1000 });
//includeRate("udp.congestionOccurred", stats, new long[] { 10*60*1000 });
//includeRate("stream.con.sendDuplicateSize", stats, new long[] { 60*60*1000 });
//includeRate("stream.con.receiveDuplicateSize", stats, new long[] { 60*60*1000 });
//stats.setProperty("stat__rateKey", "avg;maxAvg;pctLifetime;[sat;satLim;maxSat;maxSatLim;][num;lifetimeFreq;maxFreq]");
//includeRate("tunnel.decryptRequestTime", stats, new long[] { 60*1000, 10*60*1000 });
//includeRate("udp.packetDequeueTime", stats, new long[] { 60*1000 });
//includeRate("udp.packetVerifyTime", stats, new long[] { 60*1000 });
//includeRate("tunnel.buildRequestTime", stats, new long[] { 10*60*1000 });
long rate = 60*60*1000;
//includeTunnelRates("Client", stats, rate);
includeTunnelRates("Exploratory", stats, rate);
//includeRate("tunnel.rejectTimeout", stats, new long[] { 10*60*1000 });
//includeRate("tunnel.rejectOverloaded", stats, new long[] { 10*60*1000 });
//includeRate("tunnel.acceptLoad", stats, new long[] { 10*60*1000 });
}
// So that we will still get build requests - not required since 0.7.9 2010-01-12
//stats.setProperty("stat_uptime", "90m");
if (FloodfillNetworkDatabaseFacade.isFloodfill(_context.router().getRouterInfo())) {
int ri = _context.router().getUptime() > 30*60*1000 ?
_context.netDb().getKnownRouters() :
3000 + _context.random().nextInt(1000); // so it isn't obvious we restarted
stats.setProperty("netdb.knownRouters", String.valueOf(ri));
int ls = _context.router().getUptime() > 30*60*1000 ?
_context.netDb().getKnownLeaseSets() :
30 + _context.random().nextInt(40); // so it isn't obvious we restarted
stats.setProperty("netdb.knownLeaseSets", String.valueOf(ls));
}
String contact = _context.getProperty(PROP_CONTACT_NAME);
if (contact != null)
stats.setProperty("contact", contact);
String family = _context.getProperty(FamilyKeyCrypto.PROP_FAMILY_NAME);
if (family != null) {
stats.setProperty(FamilyKeyCrypto.OPT_NAME, family);
String sig = null;
String key = null;
RouterInfo oldRI = _context.router().getRouterInfo();
if (oldRI != null) {
// don't do it if family changed
if (family.equals(oldRI.getOption(FamilyKeyCrypto.OPT_NAME))) {
// copy over the pubkey and signature
key = oldRI.getOption(FamilyKeyCrypto.OPT_KEY);
if (key != null) {
if (key.contains(";")) {
// we changed the separator from ';' to ':'
key = null;
} else {
stats.setProperty(FamilyKeyCrypto.OPT_KEY, key);
sig = oldRI.getOption(FamilyKeyCrypto.OPT_SIG);
if (sig != null)
stats.setProperty(FamilyKeyCrypto.OPT_SIG, sig);
}
}
}
}
if (sig == null || key == null) {
FamilyKeyCrypto fkc = _context.router().getFamilyKeyCrypto();
if (fkc != null) {
try {
stats.putAll(fkc.sign(family, h));
} catch (GeneralSecurityException gse) {
_log.error("Failed to sign router family", gse);
stats.remove(FamilyKeyCrypto.OPT_KEY);
stats.remove(FamilyKeyCrypto.OPT_SIG);
}
}
}
}
return stats;
}
/*****
private void includeRate(String rateName, Properties stats, long selectedPeriods[]) {
includeRate(rateName, stats, selectedPeriods, false);
}
*****/
/**
* @param fudgeQuantity the data being published in this stat is too sensitive to, uh
* publish, so we're kludge the quantity (allowing the fairly safe
* publication of the average values
*/
private void includeRate(String rateName, Properties stats, long selectedPeriods[],
boolean fudgeQuantity) {
RateStat rate = _context.statManager().getRate(rateName);
if (rate == null) return;
long periods[] = rate.getPeriods();
for (int i = 0; i < periods.length; i++) {
if (periods[i] > _context.router().getUptime()) continue;
if (selectedPeriods != null) {
boolean found = false;
for (int j = 0; j < selectedPeriods.length; j++) {
if (selectedPeriods[j] == periods[i]) {
found = true;
break;
}
}
if (!found) continue;
}
Rate curRate = rate.getRate(periods[i]);
if (curRate == null) continue;
if (curRate.getLifetimeEventCount() <= 0) continue;
stats.setProperty("stat_" + rateName + '.' + getPeriod(curRate), renderRate(curRate, fudgeQuantity));
}
}
private String renderRate(Rate rate, boolean fudgeQuantity) {
StringBuilder buf = new StringBuilder(128);
buf.append(num(rate.getAverageValue())).append(';');
buf.append(num(rate.getExtremeAverageValue())).append(';');
buf.append(pct(rate.getPercentageOfLifetimeValue())).append(';');
if (rate.getLifetimeTotalEventTime() > 0) {
buf.append(pct(rate.getLastEventSaturation())).append(';');
buf.append(num(rate.getLastSaturationLimit())).append(';');
buf.append(pct(rate.getExtremeEventSaturation())).append(';');
buf.append(num(rate.getExtremeSaturationLimit())).append(';');
}
long numPeriods = rate.getLifetimePeriods();
if (fudgeQuantity) {
buf.append("555;");
if (numPeriods > 0) {
buf.append("555;555;");
}
} else {
buf.append(num(rate.getLastEventCount())).append(';');
if (numPeriods > 0) {
double avgFrequency = rate.getLifetimeEventCount() / (double)numPeriods;
buf.append(num(avgFrequency)).append(';');
buf.append(num(rate.getExtremeEventCount())).append(';');
buf.append(num(rate.getLifetimeEventCount())).append(';');
}
}
return buf.toString();
}
private static final String[] tunnelStats = { "Expire", "Reject", "Success" };
/**
* Add tunnel build rates with some mods to hide absolute quantities
* In particular, report counts normalized to 100 (i.e. a percentage)
*/
private void includeTunnelRates(String tunnelType, Properties stats, long selectedPeriod) {
long totalEvents = 0;
for (String tunnelStat : tunnelStats) {
String rateName = "tunnel.build" + tunnelType + tunnelStat;
RateStat stat = _context.statManager().getRate(rateName);
if (stat == null) continue;
Rate curRate = stat.getRate(selectedPeriod);
if (curRate == null) continue;
totalEvents += curRate.getLastEventCount();
}
if (totalEvents <= 0)
return;
for (String tunnelStat : tunnelStats) {
String rateName = "tunnel.build" + tunnelType + tunnelStat;
RateStat stat = _context.statManager().getRate(rateName);
if (stat == null) continue;
Rate curRate = stat.getRate(selectedPeriod);
if (curRate == null) continue;
double fudgeQuantity = 100.0d * curRate.getLastEventCount() / totalEvents;
stats.setProperty("stat_" + rateName + '.' + getPeriod(curRate), renderRate(curRate, fudgeQuantity));
}
}
private String renderRate(Rate rate, double fudgeQuantity) {
StringBuilder buf = new StringBuilder(128);
buf.append(num(rate.getAverageValue())).append(';');
buf.append(num(rate.getExtremeAverageValue())).append(';');
buf.append(pct(rate.getPercentageOfLifetimeValue())).append(';');
if (rate.getLifetimeTotalEventTime() > 0) {
// bah saturation
buf.append("0;0;0;0;");
}
buf.append(num(fudgeQuantity)).append(';');
return buf.toString();
}
/* report the same data for tx and rx, for enhanced anonymity */
private void includeAverageThroughput(Properties stats) {
RateStat sendRate = _context.statManager().getRate("bw.sendRate");
RateStat recvRate = _context.statManager().getRate("bw.recvRate");
if (sendRate == null || recvRate == null)
return;
Rate s = sendRate.getRate(60*60*1000);
Rate r = recvRate.getRate(60*60*1000);
if (s == null || r == null)
return;
double speed = (s.getAverageValue() + r.getAverageValue()) / 2;
double max = Math.max(s.getExtremeAverageValue(), r.getExtremeAverageValue());
String str = num(speed) + ';' + num(max) + ";0;0;";
stats.setProperty("stat_bandwidthSendBps.60m", str);
stats.setProperty("stat_bandwidthReceiveBps.60m", str);
}
private static String getPeriod(Rate rate) { return DataHelper.formatDuration(rate.getPeriod()); }
private final String num(double num) {
if (num < 0) num = 0;
synchronized (_fmt) { return _fmt.format(num); }
}
private final String pct(double num) {
if (num < 0) num = 0;
synchronized (_pct) { return _pct.format(num); }
}
public void renderStatusHTML(Writer out) { }
}