| // Copyright (c) 2019, 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. | |
| import 'dart:async'; | |
| import 'dart:collection'; | |
| import 'level.dart'; | |
| import 'log_record.dart'; | |
| /// Whether to allow fine-grain logging and configuration of loggers in a | |
| /// hierarchy. | |
| /// | |
| /// When false, all hierarchical logging instead is merged in the root logger. | |
| bool hierarchicalLoggingEnabled = false; | |
| /// Automatically record stack traces for any message of this level or above. | |
| /// | |
| /// Because this is expensive, this is off by default. | |
| Level recordStackTraceAtLevel = Level.OFF; | |
| /// The default [Level]. | |
| const defaultLevel = 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?.name.isNotEmpty ?? false ? '${parent!.fullName}.$name' : name; | |
| /// Parent of this logger in the hierarchy of loggers. | |
| final Logger? parent; | |
| /// Logging [Level] used for entries generated on this logger. | |
| /// | |
| /// Only the root logger is guaranteed to have a non-null [Level]. | |
| Level? _level; | |
| /// Private modifiable map of child loggers, indexed by their simple names. | |
| final Map<String, Logger> _children; | |
| /// Children in the hierarchy of loggers, indexed by their simple names. | |
| /// | |
| /// This is an unmodifiable map. | |
| final Map<String, Logger> children; | |
| /// Controller used to notify when log entries are added to this logger. | |
| /// | |
| /// If hierarchical logging is disabled then this is `null` for all but the | |
| /// root [Logger]. | |
| StreamController<LogRecord>? _controller; | |
| /// Controller used to notify when the log level of this logger is changed. | |
| StreamController<Level?>? _levelChangedController; | |
| /// Create or find a Logger by name. | |
| /// | |
| /// Calling `Logger(name)` will return the same instance whenever it is called | |
| /// with the same string name. Loggers created with this constructor are | |
| /// retained indefinitely and available through [attachedLoggers]. | |
| factory Logger(String name) => | |
| _loggers.putIfAbsent(name, () => Logger._named(name)); | |
| /// Creates a new detached [Logger]. | |
| /// | |
| /// Returns a new [Logger] instance (unlike `new Logger`, which returns a | |
| /// [Logger] singleton), which doesn't have any parent or children, | |
| /// and is not a part of the global hierarchical loggers structure. | |
| /// | |
| /// It can be useful when you just need a local short-living logger, | |
| /// which you'd like to be garbage-collected later. | |
| factory Logger.detached(String name) => | |
| Logger._internal(name, null, <String, Logger>{}); | |
| factory Logger._named(String name) { | |
| if (name.startsWith('.')) { | |
| throw ArgumentError("name shouldn't start with a '.'"); | |
| } | |
| if (name.endsWith('.')) { | |
| throw ArgumentError("name shouldn't end with a '.'"); | |
| } | |
| // Split hierarchical names (separated with '.'). | |
| final dot = name.lastIndexOf('.'); | |
| Logger? parent; | |
| String thisName; | |
| if (dot == -1) { | |
| if (name != '') parent = Logger(''); | |
| thisName = name; | |
| } else { | |
| parent = Logger(name.substring(0, dot)); | |
| thisName = name.substring(dot + 1); | |
| } | |
| return Logger._internal(thisName, parent, <String, Logger>{}); | |
| } | |
| Logger._internal(this.name, this.parent, Map<String, Logger> children) | |
| : _children = children, | |
| children = UnmodifiableMapView(children) { | |
| if (parent == null) { | |
| _level = defaultLevel; | |
| } else { | |
| parent!._children[name] = this; | |
| } | |
| } | |
| /// Effective level considering the levels established in this logger's | |
| /// parents (when [hierarchicalLoggingEnabled] is true). | |
| Level get level { | |
| Level effectiveLevel; | |
| if (parent == null) { | |
| // We're either the root logger or a detached logger. Return our own | |
| // level. | |
| effectiveLevel = _level!; | |
| } else if (!hierarchicalLoggingEnabled) { | |
| effectiveLevel = root._level!; | |
| } else { | |
| effectiveLevel = _level ?? parent!.level; | |
| } | |
| // ignore: unnecessary_null_comparison | |
| assert(effectiveLevel != null); | |
| return effectiveLevel; | |
| } | |
| /// Override the level for this particular [Logger] and its children. | |
| /// | |
| /// Setting this to `null` makes it inherit the [parent]s level. | |
| set level(Level? value) { | |
| if (!hierarchicalLoggingEnabled && parent != null) { | |
| throw UnsupportedError( | |
| 'Please set "hierarchicalLoggingEnabled" to true if you want to ' | |
| 'change the level on a non-root logger.', | |
| ); | |
| } | |
| if (parent == null && value == null) { | |
| throw UnsupportedError( | |
| 'Cannot set the level to `null` on a logger with no parent.', | |
| ); | |
| } | |
| final isLevelChanged = _level != value; | |
| _level = value; | |
| if (isLevelChanged) { | |
| _levelChangedController?.add(value); | |
| } | |
| } | |
| /// Returns a stream of level values set to this [Logger]. | |
| /// | |
| /// You can listen for set levels using the standard stream APIs, | |
| /// for instance: | |
| /// | |
| /// ```dart | |
| /// logger.onLevelChanged.listen((level) { ... }); | |
| /// ``` | |
| /// A state error will be thrown if the level is changed | |
| /// inside the callback. | |
| Stream<Level?> get onLevelChanged { | |
| _levelChangedController ??= StreamController<Level?>.broadcast(sync: true); | |
| return _levelChangedController!.stream; | |
| } | |
| /// Returns a stream of messages added to this [Logger]. | |
| /// | |
| /// You can listen for messages using the standard stream APIs, for instance: | |
| /// | |
| /// ```dart | |
| /// logger.onRecord.listen((record) { ... }); | |
| /// ``` | |
| Stream<LogRecord> get onRecord => _getStream(); | |
| void clearListeners() { | |
| if (hierarchicalLoggingEnabled || parent == null) { | |
| _controller?.close(); | |
| _controller = null; | |
| } else { | |
| root.clearListeners(); | |
| } | |
| } | |
| /// 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). | |
| /// | |
| /// If [message] is a [Function], it will be lazy evaluated. Additionally, if | |
| /// [message] or its evaluated value is not a [String], then 'toString()' will | |
| /// be called on the object and the result will be logged. The log record will | |
| /// contain a field holding the original object. | |
| /// | |
| /// The log record will also contain a field for the zone in which this call | |
| /// was made. This can be advantageous if a log listener wants to handler | |
| /// records of different zones differently (e.g. group log records by HTTP | |
| /// request if each HTTP request handler runs in it's own zone). | |
| /// | |
| /// If this record is logged at a level equal to or higher than | |
| /// [recordStackTraceAtLevel] and [stackTrace] is `null` or [StackTrace.empty] | |
| /// it will be defaulted to the current stack trace for this call. | |
| void log( | |
| Level logLevel, | |
| Object? message, [ | |
| Object? error, | |
| StackTrace? stackTrace, | |
| Zone? zone, | |
| ]) { | |
| Object? object; | |
| if (isLoggable(logLevel)) { | |
| if (message is Function) { | |
| message = (message as Object? Function())(); | |
| } | |
| String msg; | |
| if (message is String) { | |
| msg = message; | |
| } else { | |
| msg = message.toString(); | |
| object = message; | |
| } | |
| if ((stackTrace == null || stackTrace == StackTrace.empty) && | |
| logLevel >= recordStackTraceAtLevel) { | |
| stackTrace = StackTrace.current; | |
| error ??= 'autogenerated stack trace for $logLevel $msg'; | |
| } | |
| zone ??= Zone.current; | |
| final record = LogRecord( | |
| logLevel, | |
| msg, | |
| fullName, | |
| error, | |
| stackTrace, | |
| zone, | |
| object, | |
| ); | |
| if (parent == null) { | |
| _publish(record); | |
| } else if (!hierarchicalLoggingEnabled) { | |
| root._publish(record); | |
| } else { | |
| Logger? target = this; | |
| while (target != null) { | |
| target._publish(record); | |
| target = target.parent; | |
| } | |
| } | |
| } | |
| } | |
| /// Log message at level [Level.FINEST]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void finest(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.FINEST, message, error, stackTrace); | |
| /// Log message at level [Level.FINER]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void finer(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.FINER, message, error, stackTrace); | |
| /// Log message at level [Level.FINE]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void fine(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.FINE, message, error, stackTrace); | |
| /// Log message at level [Level.CONFIG]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void config(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.CONFIG, message, error, stackTrace); | |
| /// Log message at level [Level.INFO]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void info(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.INFO, message, error, stackTrace); | |
| /// Log message at level [Level.WARNING]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void warning(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.WARNING, message, error, stackTrace); | |
| /// Log message at level [Level.SEVERE]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void severe(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.SEVERE, message, error, stackTrace); | |
| /// Log message at level [Level.SHOUT]. | |
| /// | |
| /// See [log] for information on how non-String [message] arguments are | |
| /// handled. | |
| void shout(Object? message, [Object? error, StackTrace? stackTrace]) => | |
| log(Level.SHOUT, message, error, stackTrace); | |
| Stream<LogRecord> _getStream() { | |
| if (hierarchicalLoggingEnabled || parent == null) { | |
| return (_controller ??= StreamController<LogRecord>.broadcast( | |
| sync: true, | |
| )) | |
| .stream; | |
| } else { | |
| return root._getStream(); | |
| } | |
| } | |
| void _publish(LogRecord record) => _controller?.add(record); | |
| /// Top-level root [Logger]. | |
| static final Logger root = Logger(''); | |
| /// All attached [Logger]s in the system. | |
| static final Map<String, Logger> _loggers = <String, Logger>{}; | |
| /// All attached [Logger]s in the system. | |
| /// | |
| /// Loggers created with [Logger.detached] are not included. | |
| static Iterable<Logger> get attachedLoggers => _loggers.values; | |
| } |