package net.i2p.router.web; import java.io.File; import java.io.IOException; import java.io.OutputStream; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.stat.Rate; import net.i2p.stat.RateStat; import net.i2p.stat.RateSummaryListener; import net.i2p.util.Log; import net.i2p.util.SecureFile; import net.i2p.util.SecureFileOutputStream; import org.jrobin.core.Archive; import org.jrobin.core.RrdBackendFactory; import org.jrobin.core.RrdDb; import org.jrobin.core.RrdDef; import org.jrobin.core.RrdException; import org.jrobin.core.RrdMemoryBackendFactory; import org.jrobin.core.RrdNioBackendFactory; import org.jrobin.core.Sample; /** * Creates and updates the in-memory or on-disk RRD database, * and provides methods to generate graphs of the data * * @since 0.6.1.13 */ class SummaryListener implements RateSummaryListener { static final String PROP_PERSISTENT = "routerconsole.graphPersistent"; /** note that .jrb files are NOT compatible with .rrd files */ static final String RRD_DIR = "rrd"; private static final String RRD_PREFIX = "rrd-"; private static final String RRD_SUFFIX = ".jrb"; static final String CF = "AVERAGE"; private static final double XFF = 0.9d; private static final int STEPS = 1; private final I2PAppContext _context; private final Log _log; private final Rate _rate; private final boolean _isPersistent; private String _name; private String _eventName; private RrdDb _db; private Sample _sample; private SummaryRenderer _renderer; private int _rows; static final int PERIODS = 60 * 24; // 1440 private static final int MIN_ROWS = PERIODS; static final int MAX_ROWS = 91 * MIN_ROWS; private static final long THREE_MONTHS = 91l * 24 * 60 * 60 * 1000; public SummaryListener(Rate r) { _context = I2PAppContext.getGlobalContext(); _rate = r; _log = _context.logManager().getLog(SummaryListener.class); _isPersistent = _context.getBooleanPropertyDefaultTrue(PROP_PERSISTENT); } public void add(double totalValue, long eventCount, double totalEventTime, long period) { long now = now(); long when = now / 1000; //System.out.println("add to " + getRate().getRateStat().getName() + " on " + System.currentTimeMillis() + " / " + now + " / " + when); if (_db != null) { // add one value to the db (the average value for the period) try { _sample.setTime(when); double val = eventCount > 0 ? (totalValue / eventCount) : 0d; _sample.setValue(_name, val); _sample.setValue(_eventName, eventCount); //_sample.setValue(0, val); //_sample.setValue(1, eventCount); _sample.update(); //String names[] = _sample.getDsNames(); //System.out.println("Add " + val + " over " + eventCount + " for " + _name // + " [" + names[0] + ", " + names[1] + "]"); } catch (IllegalArgumentException iae) { // ticket #1186 // apparently a corrupt file, thrown from update() _log.error("Error adding", iae); String path = _isPersistent ? _db.getPath() : null; stopListening(); if (path != null) (new File(path)).delete(); } catch (IOException ioe) { _log.error("Error adding", ioe); stopListening(); } catch (RrdException re) { // this can happen after the time slews backwards, so don't make it an error // org.jrobin.core.RrdException: Bad sample timestamp 1264343107. Last update time was 1264343172, at least one second step is required if (_log.shouldLog(Log.WARN)) _log.warn("Error adding", re); } } } /** * JRobin can only deal with 20 character data source names, so we need to create a unique, * munged version from the user/developer-visible name. * */ static String createName(I2PAppContext ctx, String wanted) { return ctx.sha().calculateHash(DataHelper.getUTF8(wanted)).toBase64().substring(0,20); } public Rate getRate() { return _rate; } /** * @return success */ public boolean startListening() { RateStat rs = _rate.getRateStat(); long period = _rate.getPeriod(); String baseName = rs.getName() + "." + period; _name = createName(_context, baseName); _eventName = createName(_context, baseName + ".events"); File rrdFile = null; try { RrdBackendFactory factory = RrdBackendFactory.getFactory(getBackendName()); String rrdDefName; if (_isPersistent) { // generate full path for persistent RRD files File rrdDir = new SecureFile(_context.getRouterDir(), RRD_DIR); rrdFile = new File(rrdDir, RRD_PREFIX + _name + RRD_SUFFIX); rrdDefName = rrdFile.getAbsolutePath(); if (rrdFile.exists()) { _db = new RrdDb(rrdDefName, factory); Archive arch = _db.getArchive(CF, STEPS); if (arch == null) throw new IOException("No average CF in " + rrdDefName); _rows = arch.getRows(); if (_log.shouldLog(Log.INFO)) _log.info("Existing RRD " + baseName + " (" + rrdDefName + ") with " + _rows + " rows consuming " + _db.getRrdBackend().getLength() + " bytes"); } else { rrdDir.mkdir(); } } else { rrdDefName = _name; } if (_db == null) { // not persistent or not previously existing RrdDef def = new RrdDef(rrdDefName, now()/1000, period/1000); // for info on the heartbeat, xff, steps, etc, see the rrdcreate man page, aka // http://www.jrobin.org/support/man/rrdcreate.html long heartbeat = period*10/1000; def.addDatasource(_name, "GAUGE", heartbeat, Double.NaN, Double.NaN); def.addDatasource(_eventName, "GAUGE", heartbeat, 0, Double.NaN); if (_isPersistent) { _rows = (int) Math.max(MIN_ROWS, Math.min(MAX_ROWS, THREE_MONTHS / period)); } else { _rows = MIN_ROWS; } def.addArchive(CF, XFF, STEPS, _rows); _db = new RrdDb(def, factory); if (_isPersistent) SecureFileOutputStream.setPerms(new File(rrdDefName)); if (_log.shouldLog(Log.INFO)) _log.info("New RRD " + baseName + " (" + rrdDefName + ") with " + _rows + " rows consuming " + _db.getRrdBackend().getLength() + " bytes"); } _sample = _db.createSample(); _renderer = new SummaryRenderer(_context, this); _rate.setSummaryListener(this); return true; } catch (OutOfMemoryError oom) { _log.error("Error starting RRD for stat " + baseName, oom); } catch (RrdException re) { _log.error("Error starting RRD for stat " + baseName, re); // corrupt file? if (_isPersistent && rrdFile != null) rrdFile.delete(); } catch (IOException ioe) { _log.error("Error starting RRD for stat " + baseName, ioe); } catch (Throwable t) { _log.error("Error starting RRD for stat " + baseName, t); } return false; } public void stopListening() { if (_db == null) return; try { _db.close(); } catch (IOException ioe) { _log.error("Error closing", ioe); } _rate.setSummaryListener(null); if (!_isPersistent) { // close() does not release resources for memory backend try { ((RrdMemoryBackendFactory)RrdBackendFactory.getFactory(RrdMemoryBackendFactory.NAME)).delete(_db.getPath()); } catch (RrdException re) {} } _db = null; } /** * Single graph. * * @param end number of periods before now */ public void renderPng(OutputStream out, int width, int height, boolean hideLegend, boolean hideGrid, boolean hideTitle, boolean showEvents, int periodCount, int end, boolean showCredit) throws IOException { renderPng(out, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, end, showCredit, null, null); } /** * Single or two-data-source graph. * * @param lsnr2 2nd data source to plot on same graph, or null. Not recommended for events. * @param titleOverride If non-null, overrides the title * @since 0.9.6 */ public void renderPng(OutputStream out, int width, int height, boolean hideLegend, boolean hideGrid, boolean hideTitle, boolean showEvents, int periodCount, int end, boolean showCredit, SummaryListener lsnr2, String titleOverride) throws IOException { if (_renderer == null || _db == null) throw new IOException("No RRD, check logs for previous errors"); _renderer.render(out, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, end, showCredit, lsnr2, titleOverride); } public void renderPng(OutputStream out) throws IOException { if (_renderer == null || _db == null) throw new IOException("No RRD, check logs for previous errors"); _renderer.render(out); } String getName() { return _name; } String getEventName() { return _eventName; } RrdDb getData() { return _db; } long now() { return _context.clock().now(); } /** @since 0.8.7 */ String getBackendName() { return _isPersistent ? RrdNioBackendFactory.NAME : RrdMemoryBackendFactory.NAME; } /** @since 0.8.7 */ int getRows() { return _rows; } @Override public boolean equals(Object obj) { return ((obj instanceof SummaryListener) && ((SummaryListener)obj)._rate.equals(_rate)); } @Override public int hashCode() { return _rate.hashCode(); } }