Created
September 29, 2020 06:01
-
-
Save avioli/fe25b6c77c11b4c5c2ae771c8d05d751 to your computer and use it in GitHub Desktop.
A clone of https://pub.dev/packages/logging in JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import EventEmitter from 'EventEmitter'; // from https://www.npmjs.com/package/event-emitter | |
const create = Object.create; | |
const defineProperties = Object.defineProperties; | |
const _loggers = new Map(); | |
// Use a [Logger] to log debug messages. | |
// | |
// [Logger]s are named using a hierarchical dot-separated name convention. | |
function Logger(name) { | |
if (this instanceof Logger) { | |
throw new TypeError('Logger is not a constructor'); | |
} | |
if (!_loggers.has(name)) { | |
_loggers.set(name, createNamed(name)); | |
} | |
return _loggers.get(name); | |
} | |
export default Logger; | |
Logger.prototype = create(null); | |
Logger.prototype.constructor = Logger; | |
// 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. | |
Logger.hierarchicalLoggingEnabled = false; | |
export class Level { | |
constructor(name, value) { | |
this.name = name; | |
this.value = value; | |
} | |
static ALL = new Level('ALL', 0); | |
static OFF = new Level('OFF', 2000); | |
static FINEST = new Level('FINEST', 300); | |
static FINER = new Level('FINER', 400); | |
static FINE = new Level('FINE', 500); | |
static CONFIG = new Level('CONFIG', 700); | |
static INFO = new Level('INFO', 800); | |
static WARNING = new Level('WARNING', 900); | |
static SEVERE = new Level('SEVERE', 1000); | |
static SHOUT = new Level('SHOUT', 1200); | |
toString() { | |
return this.name; | |
} | |
} | |
const defaultLevel = Level.INFO; | |
function createNamed(name) { | |
if (name.startsWith('.')) { | |
throw Error("name shouldn't start with a '.'"); | |
} | |
// Split hierarchical names (separated with '.'). | |
let dot = name.lastIndexOf('.'); | |
let parent; | |
let thisName; | |
if (dot === -1) { | |
if (name != '') parent = Logger(''); | |
thisName = name; | |
} else { | |
parent = Logger(name.substring(0, dot)); | |
thisName = name.substring(dot + 1); | |
} | |
return createLogger(thisName, parent, {}); | |
} | |
function createLogger(name, parent, children) { | |
const logger = create(Logger.prototype); | |
if (!parent) { | |
logger.level = defaultLevel; | |
} else { | |
parent.children[name] = logger; | |
} | |
return defineProperties(logger, { | |
name: {value: name}, | |
parent: {value: parent}, | |
children: {value: children}, | |
}); | |
} | |
Object.assign(Logger.prototype, { | |
// Whether a message for [value]'s level is loggable in this logger. | |
isLoggable(level) { | |
return level.value >= this.level.value; | |
}, | |
// 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. | |
// | |
// If [message] is an instance of an Error and the [error] is `undefined`, | |
// then the latter will be set to the former. | |
log(logLevel, message, error) { | |
let object; | |
if (this.isLoggable(logLevel)) { | |
if (typeof message === 'function') { | |
message = message(); | |
} else if (message instanceof Error && error === undefined) { | |
error = message; | |
} | |
let msg; | |
if (typeof message === 'string') { | |
msg = message; | |
} else { | |
msg = message.toString(); | |
object = message; | |
} | |
const record = new LogRecord(logLevel, msg, this.fullName, error, object); | |
if (!this.parent) { | |
this._publish(record); | |
} else if (!Logger.hierarchicalLoggingEnabled) { | |
Logger.root._publish(record); | |
} else { | |
let target = this; | |
while (target) { | |
target._publish(record); | |
target = target.parent; | |
} | |
} | |
} | |
}, | |
finest(message, error) { | |
this.log(Level.FINEST, message, error); | |
}, | |
finer(message, error) { | |
this.log(Level.FINER, message, error); | |
}, | |
fine(message, error) { | |
this.log(Level.FINE, message, error); | |
}, | |
config(message, error) { | |
this.log(Level.CONFIG, message, error); | |
}, | |
info(message, error) { | |
this.log(Level.INFO, message, error); | |
}, | |
warning(message, error) { | |
this.log(Level.WARNING, message, error); | |
}, | |
severe(message, error) { | |
this.log(Level.SEVERE, message, error); | |
}, | |
shout(message, error) { | |
this.log(Level.SHOUT, message, error); | |
}, | |
// Attaches a listener for new [LogRecord]s added to this [Logger]. | |
// | |
// Returns a function to unsubscribe. | |
onRecord(callback) { | |
const sub = this._getEmitter().addListener('record', callback); | |
return () => this._getEmitter().removeSubscription(sub); | |
}, | |
// Clears all listeners from the [Logger] instance (or from the root). | |
clearListeners() { | |
if (Logger.hierarchicalLoggingEnabled || !this.parent) { | |
if (this._emitter) { | |
this._emitter.removeAllListeners(); | |
this._emitter = undefined; | |
} | |
} else { | |
this.root.clearListeners(); | |
} | |
}, | |
_getEmitter() { | |
if (Logger.hierarchicalLoggingEnabled || !this.parent) { | |
if (!this._emitter) { | |
this._emitter = new EventEmitter(); | |
} | |
return this._emitter; | |
} else { | |
return this.root._getEmitter(); | |
} | |
}, | |
_publish(record) { | |
if (this._emitter) { | |
this._emitter.emit('record', record); | |
} | |
}, | |
}); | |
defineProperties(Logger.prototype, { | |
// The full name of this logger, which includes the parent's full name. | |
fullName: { | |
get() { | |
return !this.parent || !this.parent.name | |
? this.name | |
: `${this.parent.fullName}.${this.name}`; | |
}, | |
}, | |
// Effective level considering the levels established in this logger's | |
// parents (when [hierarchicalLoggingEnabled] is true). | |
// | |
// Setting it overrides the level for this particular [Logger] and its | |
// children. | |
level: { | |
get() { | |
let effectiveLevel; | |
if (!this.parent) { | |
// We're either the root logger or a detached logger. | |
// Return our own level. | |
effectiveLevel = this._level; | |
} else if (!Logger.hierarchicalLoggingEnabled) { | |
effectiveLevel = Logger.root._level; | |
} else { | |
effectiveLevel = this._level || this.parent.level; | |
} | |
return effectiveLevel; | |
}, | |
set(value) { | |
if (!Logger.hierarchicalLoggingEnabled && this.parent) { | |
throw Error( | |
'Please set "Logger.hierarchicalLoggingEnabled" to true' + | |
' if you want to change the level on a non-root logger.', | |
); | |
} | |
this._level = value; | |
}, | |
}, | |
}); | |
// Top-level root [Logger]. | |
const root = Logger(''); | |
defineProperties(Logger, { | |
root: {value: root}, | |
}); | |
// Creates a new detached [Logger]. | |
// | |
// Returns a new [Logger] instance (unlike `Logger(name)`, 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. | |
Logger.detached = function detached(name) { | |
return createLogger(name, null, {}); | |
}; | |
// A log entry representation used to propagate information from [Logger] to | |
// individual handlers. | |
export class LogRecord { | |
constructor(level, message, loggerName, error, object) { | |
this.level = level; | |
this.message = message; | |
// Logger where this record is stored. | |
this.loggerName = loggerName; | |
// Associated error (if any) when recording errors messages. | |
this.error = error; | |
// Non-string message passed to Logger. | |
this.object = object; | |
} | |
static _nextNumber = 0; | |
// Time when this record was created. | |
time = new Date(); | |
// Unique sequence number greater than all log records created before it. | |
sequenceNumber = LogRecord._nextNumber++; | |
toString() { | |
return `[${this.level.name}] ${this.loggerName}: ${this.message}`; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment