blob: 935b3aeffec2b16d705fc10da992e149f9449e03 [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/**
* Provides APIs for debugging and error logging. This library introduces
* abstractions similar to those used in other languages, such as the Closure JS
* Logger and java.util.logging.Logger.
*/
library logging;
/**
* Whether to allow fine-grain logging and configuration of loggers in a
* hierarchy. When false, all logging is merged in the root logger.
*/
bool hierarchicalLoggingEnabled = false;
/**
* Level for the root-logger. This will be the level of all loggers if
* [hierarchicalLoggingEnabled] is false.
*/
Level _rootLevel = Level.INFO;
/**
* Use a [Logger] to log debug messages. [Logger]s are named using a
* hierarchical dot-separated name convention.
*/
class Logger {
/** Simple name of this logger. */
final String name;
/** The full name of this logger, which includes the parent's full name. */
String get fullName =>
(parent == null || parent.name == '') ? name : '${parent.fullName}.$name';
/** Parent of this logger in the hierarchy of loggers. */
final Logger parent;
/** Logging [Level] used for entries generated on this logger. */
Level _level;
/** Children in the hierarchy of loggers, indexed by their simple names. */
Map<String, Logger> children;
/** Handlers used to process log entries in this logger. */
List<LoggerHandler> _handlers;
/**
* Singleton constructor. Calling `new Logger(name)` will return the same
* actual instance whenever it is called with the same string name.
*/
factory Logger(String name) {
if (name.startsWith('.')) {
throw new ArgumentError("name shouldn't start with a '.'");
}
if (_loggers == null) _loggers = <String, Logger>{};
if (_loggers.containsKey(name)) return _loggers[name];
// Split hierarchical names (separated with '.').
int dot = name.lastIndexOf('.');
Logger parent = null;
String thisName;
if (dot == -1) {
if (name != '') parent = new Logger('');
thisName = name;
} else {
parent = new Logger(name.substring(0, dot));
thisName = name.substring(dot + 1);
}
final res = new Logger._internal(thisName, parent);
_loggers[name] = res;
return res;
}
Logger._internal(this.name, this.parent)
: children = new Map<String, Logger>() {
if (parent != null) parent.children[name] = this;
}
/**
* Effective level considering the levels established in this logger's parents
* (when [hierarchicalLoggingEnabled] is true).
*/
Level get level {
if (hierarchicalLoggingEnabled) {
if (_level != null) return _level;
if (parent != null) return parent.level;
}
return _rootLevel;
}
/** Override the level for this particular [Logger] and its children. */
set level(value) {
if (hierarchicalLoggingEnabled && parent != null) {
_level = value;
} else {
if (parent != null) {
throw new UnsupportedError(
'Please set "hierarchicalLoggingEnabled" to true if you want to '
'change the level on a non-root logger.');
}
_rootLevel = value;
}
}
/**
* Returns an event manager for this [Logger]. You can listen for log messages
* by adding a [LoggerHandler] to an event from the event manager, for
* instance:
* logger.on.record.add((record) { ... });
*/
LoggerEvents get on => new LoggerEvents(this);
/** Adds a handler to listen whenever a log record is added to this logger. */
void _addHandler(LoggerHandler handler) {
if (hierarchicalLoggingEnabled || parent == null) {
if (_handlers == null) {
_handlers = new List<LoggerHandler>();
}
_handlers.add(handler);
} else {
root._addHandler(handler);
}
}
/** Remove a previously added handler. */
void _removeHandler(LoggerHandler handler) {
if (hierarchicalLoggingEnabled || parent == null) {
if (_handlers == null) return;
int index = _handlers.indexOf(handler);
if (index != -1) _handlers.removeRange(index, 1);
} else {
root._removeHandler(handler);
}
}
/** Removes all handlers previously added to this logger. */
void _clearHandlers() {
if (hierarchicalLoggingEnabled || parent == null) {
_handlers = null;
} else {
root._clearHandlers();
}
}
/** Whether a message for [value]'s level is loggable in this logger. */
bool isLoggable(Level value) => (value >= level);
/**
* Adds a log record for a [message] at a particular [logLevel] if
* `isLoggable(logLevel)` is true. Use this method to create log entries for
* user-defined levels. To record a message at a predefined level (e.g.
* [Level.INFO], [Level.WARNING], etc) you can use their specialized methods
* instead (e.g. [info], [warning], etc).
*/
// TODO(sigmund): add support for logging exceptions.
void log(Level logLevel, String message) {
if (isLoggable(logLevel)) {
var record = new LogRecord(logLevel, message, fullName);
if (hierarchicalLoggingEnabled) {
var target = this;
while (target != null) {
target._publish(record);
target = target.parent;
}
} else {
root._publish(record);
}
}
}
/** Log message at level [Level.FINEST]. */
void finest(String message) => log(Level.FINEST, message);
/** Log message at level [Level.FINER]. */
void finer(String message) => log(Level.FINER, message);
/** Log message at level [Level.FINE]. */
void fine(String message) => log(Level.FINE, message);
/** Log message at level [Level.CONFIG]. */
void config(String message) => log(Level.CONFIG, message);
/** Log message at level [Level.INFO]. */
void info(String message) => log(Level.INFO, message);
/** Log message at level [Level.WARNING]. */
void warning(String message) => log(Level.WARNING, message);
/** Log message at level [Level.SEVERE]. */
void severe(String message) => log(Level.SEVERE, message);
/** Log message at level [Level.SHOUT]. */
void shout(String message) => log(Level.SHOUT, message);
void _publish(LogRecord record) {
if (_handlers != null) {
_handlers.forEach((h) => h(record));
}
}
/** Top-level root [Logger]. */
static get root => new Logger('');
/** All [Logger]s in the system. */
static Map<String, Logger> _loggers;
}
/** Handler callback to process log entries as they are added to a [Logger]. */
typedef void LoggerHandler(LogRecord);
/** Event manager for a [Logger] (holds events that a [Logger] can fire). */
class LoggerEvents {
final Logger _logger;
LoggerEvents(this._logger);
/** Event fired when a log record is added to a [Logger]. */
LoggerHandlerList get record => new LoggerHandlerList(_logger);
}
/** List of handlers that will be called on a logger event. */
class LoggerHandlerList {
Logger _logger;
LoggerHandlerList(this._logger);
void add(LoggerHandler handler) => _logger._addHandler(handler);
void remove(LoggerHandler handler) => _logger._removeHandler(handler);
void clear() => _logger._clearHandlers();
}
/**
* [Level]s to control logging output. Logging can be enabled to include all
* levels above certain [Level]. [Level]s are ordered using an integer
* value [Level.value]. The predefined [Level] constants below are sorted as
* follows (in descending order): [Level.SHOUT], [Level.SEVERE],
* [Level.WARNING], [Level.INFO], [Level.CONFIG], [Level.FINE], [Level.FINER],
* [Level.FINEST], and [Level.ALL].
*
* We recommend using one of the predefined logging levels. If you define your
* own level, make sure you use a value between those used in [Level.ALL] and
* [Level.OFF].
*/
class Level implements Comparable<Level> {
// TODO(sigmund): mark name/value as 'const' when the language supports it.
final String name;
/**
* Unique value for this level. Used to order levels, so filtering can exclude
* messages whose level is under certain value.
*/
final int value;
const Level(this.name, this.value);
/** Special key to turn on logging for all levels ([value] = 0). */
static const Level ALL = const Level('ALL', 0);
/** Special key to turn off all logging ([value] = 2000). */
static const Level OFF = const Level('OFF', 2000);
/** Key for highly detailed tracing ([value] = 300). */
static const Level FINEST = const Level('FINEST', 300);
/** Key for fairly detailed tracing ([value] = 400). */
static const Level FINER = const Level('FINER', 400);
/** Key for tracing information ([value] = 500). */
static const Level FINE = const Level('FINE', 500);
/** Key for static configuration messages ([value] = 700). */
static const Level CONFIG = const Level('CONFIG', 700);
/** Key for informational messages ([value] = 800). */
static const Level INFO = const Level('INFO', 800);
/** Key for potential problems ([value] = 900). */
static const Level WARNING = const Level('WARNING', 900);
/** Key for serious failures ([value] = 1000). */
static const Level SEVERE = const Level('SEVERE', 1000);
/** Key for extra debugging loudness ([value] = 1200). */
static const Level SHOUT = const Level('SHOUT', 1200);
bool operator ==(Level other) => other != null && value == other.value;
bool operator <(Level other) => value < other.value;
bool operator <=(Level other) => value <= other.value;
bool operator >(Level other) => value > other.value;
bool operator >=(Level other) => value >= other.value;
int compareTo(Level other) => value - other.value;
int get hashCode => value;
String toString() => name;
}
/**
* A log entry representation used to propagate information from [Logger] to
* individual [Handler]s.
*/
class LogRecord {
final Level level;
final String message;
/** Logger where this record is stored. */
final String loggerName;
/** Time when this record was created. */
final DateTime time;
/** Unique sequence number greater than all log records created before it. */
final int sequenceNumber;
static int _nextNumber = 0;
/** Associated exception (if any) when recording errors messages. */
Exception exception;
/** Associated exception message (if any) when recording errors messages. */
String exceptionText;
LogRecord(
this.level, this.message, this.loggerName,
[time, this.exception, this.exceptionText]) :
this.time = (time == null) ? new DateTime.now() : time,
this.sequenceNumber = LogRecord._nextNumber++;
}