Skip to content

Instantly share code, notes, and snippets.

@avioli
Created September 29, 2020 06:01
Show Gist options
  • Save avioli/fe25b6c77c11b4c5c2ae771c8d05d751 to your computer and use it in GitHub Desktop.
Save avioli/fe25b6c77c11b4c5c2ae771c8d05d751 to your computer and use it in GitHub Desktop.
A clone of https://pub.dev/packages/logging in JavaScript
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