package net.i2p.util;
/*
* 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.File;
import java.io.Flushable;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
/**
* Manages the logging system, loading (and reloading) the configuration file,
* coordinating the log limits, and storing the set of log records pending.
* This also fires off a LogWriter thread that pulls pending records off and
* writes them where appropriate.
*
*/
public class LogManager implements Flushable {
public final static String CONFIG_LOCATION_PROP = "loggerConfigLocation";
public final static String FILENAME_OVERRIDE_PROP = "loggerFilenameOverride";
public final static String CONFIG_LOCATION_DEFAULT = "logger.config";
/**
* These define the characters in the format line of the config file
*/
public static final char DATE = 'd', CLASS = 'c', THREAD = 't', PRIORITY = 'p', MESSAGE = 'm';
public final static String PROP_FORMAT = "logger.format";
public final static String PROP_DATEFORMAT = "logger.dateFormat";
public final static String PROP_FILENAME = "logger.logFileName";
public final static String PROP_FILESIZE = "logger.logFileSize";
public final static String PROP_ROTATIONLIMIT = "logger.logRotationLimit";
public final static String PROP_DISPLAYONSCREEN = "logger.displayOnScreen";
public final static String PROP_CONSOLEBUFFERSIZE = "logger.consoleBufferSize";
public final static String PROP_DISPLAYONSCREENLEVEL = "logger.minimumOnScreenLevel";
public final static String PROP_DEFAULTLEVEL = "logger.defaultLevel";
/** @since 0.9.2 */
private static final String PROP_LOG_BUFFER_SIZE = "logger.logBufferSize";
/** @since 0.9.2 */
private static final String PROP_DROP = "logger.dropOnOverflow";
/** @since 0.9.3 */
private static final String PROP_DUP = "logger.dropDuplicates";
/** @since 0.9.18 */
private static final String PROP_FLUSH = "logger.flushInterval";
public final static String PROP_RECORD_PREFIX = "logger.record.";
public final static String DEFAULT_FORMAT = DATE + " " + PRIORITY + " [" + THREAD + "] " + CLASS + ": " + MESSAGE;
//public final static String DEFAULT_DATEFORMAT = "HH:mm:ss.SSS";
/** blank means default short date and medium time for the locale - see DateFormat */
public final static String DEFAULT_DATEFORMAT = "";
public final static String DEFAULT_FILENAME = "logs/log-@.txt";
public final static String DEFAULT_FILESIZE = "10m";
public final static boolean DEFAULT_DISPLAYONSCREEN = true;
public final static int DEFAULT_CONSOLEBUFFERSIZE = 20;
public final static String DEFAULT_ROTATIONLIMIT = "2";
public final static String DEFAULT_DEFAULTLEVEL = Log.STR_ERROR;
public final static String DEFAULT_ONSCREENLEVEL = Log.STR_CRIT;
private final I2PAppContext _context;
private final Log _log;
/** when was the config file last read (or -1 if never) */
private long _configLastRead;
/** the config file */
private File _locationFile;
/** max to LogRecords to buffer in memory before we start blocking */
private static final int MAX_BUFFER = 1024;
/** Ordered list of LogRecord elements that have not been written out yet */
private final LinkedBlockingQueue<LogRecord> _records;
/** List of explicit overrides of log levels (LogLimit objects) */
private final Set<LogLimit> _limits;
/** String (scope) or Log.LogScope to Log object */
private final ConcurrentHashMap<Object, Log> _logs;
/** who clears and writes our records */
private LogWriter _writer;
/**
* default log level for logs that aren't explicitly controlled
* through a LogLimit in _limits
*/
private int _defaultLimit;
/** Log record format string */
private char[] _format;
/** Date format instance */
private SimpleDateFormat _dateFormat;
/** Date format string (for the SimpleDateFormat instance) */
private String _dateFormatPattern;
/** log filename pattern */
private String _baseLogfilename;
/** max # bytes in the logfile before rotation */
private int _fileSize;
/** max # rotated logs */
private int _rotationLimit;
/** minimum log level to be displayed on stdout */
private int _onScreenLimit;
/** whether or not we even want to display anything on stdout */
private boolean _displayOnScreen;
/** how many records we want to buffer in the "recent logs" list */
private int _consoleBufferSize = DEFAULT_CONSOLEBUFFERSIZE;
/** the actual "recent logs" list */
private final LogConsoleBuffer _consoleBuffer;
private int _logBufferSize = MAX_BUFFER;
private boolean _dropOnOverflow;
private boolean _dropDuplicates;
private final AtomicLong _droppedRecords = new AtomicLong();
// in seconds
private int _flushInterval = (int) (LogWriter.FLUSH_INTERVAL / 1000);
private boolean _alreadyNoticedMissingConfig;
public LogManager(I2PAppContext context) {
_displayOnScreen = true;
_alreadyNoticedMissingConfig = false;
_limits = new ConcurrentHashSet<LogLimit>();
_logs = new ConcurrentHashMap<Object, Log>(128);
_defaultLimit = Log.ERROR;
_context = context;
_log = getLog(LogManager.class);
String location = context.getProperty(CONFIG_LOCATION_PROP, CONFIG_LOCATION_DEFAULT);
setConfig(location);
_records = new LinkedBlockingQueue<LogRecord>(_logBufferSize);
_consoleBuffer = new LogConsoleBuffer(_consoleBufferSize);
// If we aren't in the router context, delay creating the LogWriter until required,
// so it doesn't create a log directory and log files unless there is output.
// In the router context, we have to rotate to a new log file at startup or the logs.jsp
// page will display the old log.
if (context.isRouterContext())
startLogWriter();
try {
Runtime.getRuntime().addShutdownHook(new ShutdownHook());
} catch (IllegalStateException ise) {
// shutdown in progress, fsck it
}
//System.out.println("Created logManager " + this + " with context: " + context);
}
/** @since 0.8.2 */
private synchronized void startLogWriter() {
// yeah, this doesn't always work, _writer should be volatile
if (_writer != null)
return;
if (SystemVersion.isAndroid()) {
try {
Class<? extends LogWriter> clazz = Class.forName(
"net.i2p.util.AndroidLogWriter"
).asSubclass(LogWriter.class);
Constructor<? extends LogWriter> ctor = clazz.getDeclaredConstructor(LogManager.class);
_writer = ctor.newInstance(this);
} catch (ClassNotFoundException e) {
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
} catch (NoSuchMethodException e) {
}
}
// Default writer
if (_writer == null)
_writer = new FileLogWriter(this);
_writer.setFlushInterval(_flushInterval * 1000);
// if you enable logging in I2PThread again, you MUST change this back to Thread
Thread t = new I2PThread(_writer, "LogWriter");
t.setDaemon(true);
t.start();
}
public Log getLog(Class<?> cls) { return getLog(cls, null); }
public Log getLog(String name) { return getLog(null, name); }
public Log getLog(Class<?> cls, String name) {
String scope = Log.getScope(name, cls);
boolean isNew = false;
Log rv = _logs.get(scope);
if (rv == null) {
rv = new Log(this, cls, name);
Log old = _logs.putIfAbsent(scope, rv);
isNew = old == null;
if (!isNew)
rv = old;
}
if (isNew)
updateLimit(rv);
return rv;
}
/** now used by ConfigLogingHelper */
public List<Log> getLogs() {
return new ArrayList<Log>(_logs.values());
}
/**
* If the log already exists, its priority is set here but cannot
* be changed later, as it becomes an "orphan" not tracked by the manager.
*/
void addLog(Log log) {
Log old = _logs.putIfAbsent(log.getScope(), log);
updateLimit(log);
if (old != null) {
if (_log.shouldLog(Log.INFO))
_log.info("Duplicate log for " + log.getName());
}
}
public LogConsoleBuffer getBuffer() { return _consoleBuffer; }
/** @deprecated unused */
@Deprecated
public void setDisplayOnScreen(boolean yes) {
_displayOnScreen = yes;
}
public boolean displayOnScreen() {
return _displayOnScreen;
}
public int getDisplayOnScreenLevel() {
return _onScreenLimit;
}
/** @deprecated unused */
@Deprecated
public void setDisplayOnScreenLevel(int level) {
_onScreenLimit = level;
}
public int getConsoleBufferSize() {
return _consoleBufferSize;
}
/** @deprecated unused */
@Deprecated
public void setConsoleBufferSize(int numRecords) {
_consoleBufferSize = numRecords;
}
public void setConfig(String filename) {
_locationFile = new File(filename);
if (!_locationFile.isAbsolute())
_locationFile = new File(_context.getConfigDir(), filename);
loadConfig();
}
/**
* File may not exist or have old logs in it if not opened yet
* @return non-null
*/
public String currentFile() {
if (_writer == null)
return ("No log file created yet");
return _writer.currentFile();
}
/**
* Used by Log to add records to the queue.
* This is generally nonblocking and unsyncrhonized but may block when under
* massive logging load as a way of throttling logging threads.
*/
void addRecord(LogRecord record) {
if ((!_context.isRouterContext()) && _writer == null)
startLogWriter();
boolean success = _records.offer(record);
if (!success) {
if (_dropOnOverflow) {
// TODO use the counter in a periodic drop msg
_droppedRecords.incrementAndGet();
return;
}
// the writer waits 10 seconds *or* until we tell them to wake up
// before rereading the config and writing out any log messages
synchronized (_writer) {
_writer.notifyAll();
}
// block as a way of slowing down out-of-control loggers (a little)
try {
_records.put(record);
} catch (InterruptedException ie) {}
} else if (_flushInterval <= 0) {
synchronized (_writer) {
_writer.notifyAll();
}
}
}
/**
* Called periodically by the log writer's thread
* Do not log here, deadlock of LogWriter
*/
void rereadConfig() {
// perhaps check modification time
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Rereading configuration file");
loadConfig();
}
/**
* @since 0.9.3
*/
boolean shouldDropDuplicates() {
return _dropDuplicates;
}
/**
* Do not log here, deadlock of LogWriter via rereadConfig().
*/
private void loadConfig() {
File cfgFile = _locationFile;
if (!cfgFile.exists()) {
if (!_alreadyNoticedMissingConfig) {
//if (_log.shouldLog(Log.WARN))
// _log.warn("Log file " + _locationFile.getAbsolutePath() + " does not exist");
_alreadyNoticedMissingConfig = true;
}
parseConfig(new Properties());
updateLimits();
return;
}
_alreadyNoticedMissingConfig = false;
if ((_configLastRead > 0) && (_configLastRead >= cfgFile.lastModified())) {
//if (_log.shouldLog(Log.INFO))
// _log.info("Short circuiting config read (last read: "
// + (_context.clock().now() - _configLastRead) + "ms ago, config file modified "
// + (_context.clock().now() - cfgFile.lastModified()) + "ms ago");
return;
}
Properties p = new Properties();
try {
DataHelper.loadProps(p, cfgFile);
_configLastRead = _context.clock().now();
} catch (IOException ioe) {
System.err.println("Error loading logger config from " + cfgFile.getAbsolutePath());
}
parseConfig(p);
updateLimits();
}
/**
* Do not log here, deadlock of LogWriter via rereadConfig().
*/
private void parseConfig(Properties config) {
String fmt = config.getProperty(PROP_FORMAT, DEFAULT_FORMAT);
_format = fmt.toCharArray();
String df = config.getProperty(PROP_DATEFORMAT, DEFAULT_DATEFORMAT);
setDateFormat(df);
String disp = config.getProperty(PROP_DISPLAYONSCREEN);
if (disp == null)
_displayOnScreen = DEFAULT_DISPLAYONSCREEN;
else {
if ("TRUE".equals(disp.toUpperCase(Locale.US).trim()))
_displayOnScreen = true;
else if ("YES".equals(disp.toUpperCase(Locale.US).trim()))
_displayOnScreen = true;
else
_displayOnScreen = false;
}
// prior to 0.9.5, override prop trumped config file
// as of 0.9.5, override prop trumps config file only if config file is set to default,
// so it may be set in the UI.
String filename = config.getProperty(PROP_FILENAME, DEFAULT_FILENAME);
String filenameOverride = _context.getProperty(FILENAME_OVERRIDE_PROP);
if (filenameOverride != null && filename.equals(DEFAULT_FILENAME))
setBaseLogfilename(filenameOverride);
else
setBaseLogfilename(filename);
_fileSize = getFileSize(config.getProperty(PROP_FILESIZE, DEFAULT_FILESIZE));
_rotationLimit = -1;
try {
_rotationLimit = Integer.parseInt(config.getProperty(PROP_ROTATIONLIMIT, DEFAULT_ROTATIONLIMIT));
} catch (NumberFormatException nfe) {
System.err.println("Invalid rotation limit");
nfe.printStackTrace();
}
_defaultLimit = Log.getLevel(config.getProperty(PROP_DEFAULTLEVEL, DEFAULT_DEFAULTLEVEL));
_onScreenLimit = Log.getLevel(config.getProperty(PROP_DISPLAYONSCREENLEVEL, DEFAULT_ONSCREENLEVEL));
try {
String str = config.getProperty(PROP_CONSOLEBUFFERSIZE);
if (str != null)
_consoleBufferSize = Integer.parseInt(str);
} catch (NumberFormatException nfe) {}
try {
String str = config.getProperty(PROP_LOG_BUFFER_SIZE);
if (str != null)
_logBufferSize = Integer.parseInt(str);
} catch (NumberFormatException nfe) {}
try {
String str = config.getProperty(PROP_FLUSH);
if (str != null) {
_flushInterval = Integer.parseInt(str);
synchronized(this) {
if (_writer != null)
_writer.setFlushInterval(_flushInterval * 1000);
}
}
} catch (NumberFormatException nfe) {}
_dropOnOverflow = Boolean.parseBoolean(config.getProperty(PROP_DROP));
String str = config.getProperty(PROP_DUP);
_dropDuplicates = str == null || Boolean.parseBoolean(str);
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Log set to use the base log file as " + _baseLogfilename);
parseLimits(config);
}
/**
* Do not log here, deadlock of LogWriter via rereadConfig().
*/
private void parseLimits(Properties config) {
parseLimits(config, PROP_RECORD_PREFIX);
}
/**
* Do not log here, deadlock of LogWriter via rereadConfig().
*/
private void parseLimits(Properties config, String recordPrefix) {
_limits.clear();
if (config != null) {
for (Map.Entry<Object, Object> e : config.entrySet()) {
String key = (String) e.getKey();
// if we're filtering the records (e.g. logger.record.*) then
// filter accordingly (stripping off that prefix for matches)
if (recordPrefix != null) {
if (key.startsWith(recordPrefix)) {
key = key.substring(recordPrefix.length());
} else {
continue;
}
}
String val = (String) e.getValue();
LogLimit lim = new LogLimit(key, Log.getLevel(val));
//_log.debug("Limit found for " + name + " as " + val);
if (!_limits.contains(lim))
_limits.add(lim);
}
}
updateLimits();
}
/**
* Update the existing limit overrides
*
* @param limits mapping of prefix to log level string (not the log #)
*/
public void setLimits(Properties limits) {
parseLimits(limits, null);
}
/**
* Update the date format
* Do not log here, deadlock of LogWriter via rereadConfig().
*
* @param format null or empty string means use default format for the locale
* (with a SHORT date and a MEDIUM time - see DateFormat)
* @return true if the format was updated, false if it was invalid
*/
public boolean setDateFormat(String format) {
if (format == null)
format = "";
if (format.equals(_dateFormatPattern) && _dateFormat != null)
return true;
try {
SimpleDateFormat fmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM);
if (!format.equals(""))
fmt.applyPattern(format);
// the router sets the JVM time zone to UTC but saves the original here so we can get it
fmt.setTimeZone(SystemVersion.getSystemTimeZone(_context));
_dateFormatPattern = format;
_dateFormat = fmt;
return true;
} catch (IllegalArgumentException iae) {
//getLog(LogManager.class).error("Date format is invalid [" + format + "]", iae);
return false;
}
}
/**
* Update the log file size limit
*/
public void setFileSize(int numBytes) {
if (numBytes > 0)
_fileSize = numBytes;
}
public String getDefaultLimit() { return Log.toLevelString(_defaultLimit); }
public void setDefaultLimit(String lim) {
_defaultLimit = Log.getLevel(lim);
updateLimits();
}
/**
* Return a mapping of the explicit overrides - path prefix to (text
* formatted) limit.
*
*/
public Properties getLimits() {
Properties rv = new Properties();
for (LogLimit lim : _limits) {
rv.setProperty(lim.getRootName(), Log.toLevelString(lim.getLimit()));
}
return rv;
}
/**
* Determine how many bytes are in the given formatted string (5m, 60g, 100k, etc)
* Size may be k, m, or g; a trailing b is ignored. Upper-case is allowed.
* Spaces between the number and letter is are allowed.
* The number may be in floating point.
* 4096 min, 2 GB max (returns int)
*/
public static int getFileSize(String size) {
try {
String v = size.trim().toUpperCase(Locale.US);
if (v.length() < 2)
return -1;
if (v.endsWith("B"))
v = v.substring(0, v.length() - 1);
char mod = v.charAt(v.length() - 1);
if (!Character.isDigit(mod)) v = v.substring(0, v.length() - 1);
// output to form was in current locale, so have to parse it back that way
double val = (new DecimalFormat()).parse(v.trim()).doubleValue();
switch (mod) {
case 'K':
val *= 1024;
break;
case 'M':
val *= 1024 * 1024;
break;
case 'G':
val *= 1024 * 1024 * 1024;
break;
default:
// blah, noop
break;
}
if (val < 4096 || val > Integer.MAX_VALUE)
return -1;
return (int) val;
} catch (Throwable t) {
System.err.println("Error parsing config for filesize: [" + size + "]");
return -1;
}
}
/**
* Do not log here, deadlock of LogWriter via rereadConfig().
*/
private void updateLimits() {
for (Log log : _logs.values()) {
updateLimit(log);
}
}
/**
* Do not log here, deadlock of LogWriter via rereadConfig().
*/
private void updateLimit(Log log) {
List<LogLimit> limits = getLimits(log);
LogLimit max = null;
if (limits != null) {
for (LogLimit cur : limits) {
if (max == null)
max = cur;
else {
if (cur.getRootName().length() > max.getRootName().length()) {
max = cur;
}
}
}
}
if (max != null) {
log.setMinimumPriority(max.getLimit());
} else {
//if (_log != null)
// _log.debug("The log for " + log.getClass() + " has no matching limits");
log.setMinimumPriority(_defaultLimit);
}
}
/**
* Do not log here, deadlock of LogWriter via rereadConfig().
* @return null if no matches
*/
private List<LogLimit> getLimits(Log log) {
ArrayList<LogLimit> limits = null; // new ArrayList(4);
for (LogLimit limit : _limits) {
if (limit.matches(log)) {
if (limits == null)
limits = new ArrayList<LogLimit>(4);
limits.add(limit);
}
}
return limits;
}
///
/// would be friend methods for LogWriter...
///
public String getBaseLogfilename() {
return _baseLogfilename;
}
public void setBaseLogfilename(String filenamePattern) {
// '#' is a comment character in loadProps/storeProps
_baseLogfilename = filenamePattern.replace('#', '@');
}
public int getFileSize() {
return _fileSize;
}
public int getRotationLimit() {
return _rotationLimit;
}
/** @return success */
public boolean saveConfig() {
Properties props = createConfig();
try {
DataHelper.storeProps(props, _locationFile);
return true;
} catch (IOException ioe) {
getLog(LogManager.class).error("Error saving the config", ioe);
return false;
}
}
private Properties createConfig() {
Properties rv = new OrderedProperties();
rv.setProperty(PROP_FORMAT, new String(_format));
rv.setProperty(PROP_DATEFORMAT, _dateFormatPattern);
rv.setProperty(PROP_DISPLAYONSCREEN, Boolean.toString(_displayOnScreen));
rv.setProperty(PROP_DROP, Boolean.toString(_dropOnOverflow));
rv.setProperty(PROP_DUP, Boolean.toString(_dropDuplicates));
rv.setProperty(PROP_LOG_BUFFER_SIZE, Integer.toString(_logBufferSize));
// prior to 0.9.5, override prop trumped config file
// as of 0.9.5, override prop trumps config file only if config file is set to default,
// so it may be set in the UI.
rv.setProperty(PROP_FILENAME, _baseLogfilename);
if (_fileSize >= 1024*1024)
rv.setProperty(PROP_FILESIZE, (_fileSize / (1024*1024)) + "m");
else if (_fileSize >= 1024)
rv.setProperty(PROP_FILESIZE, (_fileSize / (1024))+ "k");
else if (_fileSize > 0)
rv.setProperty(PROP_FILESIZE, Integer.toString(_fileSize));
// if <= 0, dont specify
rv.setProperty(PROP_ROTATIONLIMIT, Integer.toString(_rotationLimit));
rv.setProperty(PROP_DEFAULTLEVEL, Log.toLevelString(_defaultLimit));
rv.setProperty(PROP_DISPLAYONSCREENLEVEL, Log.toLevelString(_onScreenLimit));
rv.setProperty(PROP_CONSOLEBUFFERSIZE, Integer.toString(_consoleBufferSize));
rv.setProperty(PROP_FLUSH, Integer.toString(_flushInterval));
for (LogLimit lim : _limits) {
rv.setProperty(PROP_RECORD_PREFIX + lim.getRootName(), Log.toLevelString(lim.getLimit()));
}
return rv;
}
/**
* Zero-copy.
* For the LogWriter
* @since 0.8.2
*/
Queue<LogRecord> getQueue() {
return _records;
}
public char[] getFormat() {
return _format;
}
public void setFormat(char fmt[]) {
_format = fmt;
}
public SimpleDateFormat getDateFormat() {
return _dateFormat;
}
public String getDateFormatPattern() {
return _dateFormatPattern;
}
/*****
public static void main(String args[]) {
I2PAppContext ctx = new I2PAppContext();
Log l1 = ctx.logManager().getLog("test.1");
Log l2 = ctx.logManager().getLog("test.2");
Log l21 = ctx.logManager().getLog("test.2.1");
Log l = ctx.logManager().getLog("test");
l.debug("this should fail");
l.info("this should pass");
l1.warn("this should pass");
l1.info("this should fail");
l2.error("this should fail");
l21.debug("this should pass");
l1.error("test exception", new Exception("test"));
l1.error("test exception", new Exception("test"));
try {
Thread.sleep(2 * 1000);
} catch (Throwable t) { // nop
}
System.exit(0);
}
*****/
/**
* Flush any pending records to disk.
* Blocking up to 250 ms.
* @since 0.9.3
*/
public void flush() {
if (_writer != null) {
int i = 50;
while ((!_records.isEmpty()) && i-- > 0) {
synchronized (_writer) {
_writer.notifyAll();
}
try {
Thread.sleep(5);
} catch (InterruptedException ie) {}
}
}
}
public void shutdown() {
if (_writer != null) {
//_log.log(Log.WARN, "Shutting down logger");
// try to prevent out-of-order logging at shutdown
flush();
// this could generate out-of-order messages
_writer.flushRecords(false);
_writer.stopWriting();
synchronized (_writer) {
_writer.notifyAll();
}
}
_records.clear();
_limits.clear();
_logs.clear();
_consoleBuffer.clear();
}
private static final AtomicInteger __id = new AtomicInteger();
private class ShutdownHook extends I2PAppThread {
private final int _id;
public ShutdownHook() {
_id = __id.incrementAndGet();
}
@Override
public void run() {
setName("Log " + _id + " shutdown ");
shutdown();
}
}
/**
* Convenience method for LogRecordFormatter
* @since 0.7.14
*/
I2PAppContext getContext() {
return _context;
}
}