Home Reference Source

src/Logs.js

import Logger from 'js-logger';

const singleton = Symbol();
const singletonEnforcer = Symbol();

/**
 * @protected
 * Class that provides logger instances
 * Each log instance has an associated name used to identify the file
 * that created log records.
 *
 * This class implements a singleton pattern. Instances of {Logs} are created
 * with Logs.instance
 *
 * @example
 * // Log a debug message
 * var log = Logs.instance.getLogger('test.js');
 * log.debug('a message');
 *
 * // Generates next log message:
 * // [14:55:03] test.js a message
 *
 * @example
 * // Log an error message
 * var log = Logs.instance.getLogger('test.js');
 * log.error('an error message');
 *
 * // Generates next error message:
 * // [14:55:03] test.js a message
 */
export default class Logs {

	/**
	 * Get a {Logs} instance
	 *
	 * @type {Logs}
	 */
	static get instance() {
		if (!this[singleton]) {
			this[singleton] = new Logs(singletonEnforcer);
		}
		return this[singleton];
	}

	/**
	 * Class constructor
	 *
	 * @private
	 *
	 * @param {Symbol} enforcer symbol used internally to implement singleton access
	 */
	constructor(enforcer) {
		if (enforcer != singletonEnforcer) {
			throw new Error('The constructor is private, use Logs.instance instead');
		}

		this._handlers = [];

		Logger.useDefaults();
		Logger.setHandler((messages, context) => {
			this._handlers.forEach((handler) => {
				handler(messages, context);
			});
		});

		this.addHandler(Logger.createDefaultHandler({formatter: this._formatter}));
	}

	/**
	 * Supported log levels
	 *
	 * @type {Object}
	 */
	get LEVELS() {
		return {
			DEBUG: Logger.DEBUG,
			INFO: Logger.INFO,
			TIME: Logger.TIME,
			WARN: Logger.WARN,
			ERROR: Logger.ERROR,
			OFF: Logger.OFF,
		};
	}

	/**
	 * Get a stored log level associated with given name.
	 *
	 * If environment doesn't support localStorage Logs.LEVELS.OFF is returned
	 *
	 * Log levels are stored as a comma separated list in localStorage.sippodebug. Each
	 * element of this list follows next syntax: "name<=level>". level defaults to DEBUG.
	 *
	 * @param {String} name element name
	 *
	 * @return {Object} log level
	 */
	getLevelFromLocalStorage(name) {
		if (!localStorage || !localStorage.sippodebug) {
			return this.LEVELS.WARN;
		}
		let any = false;
		let entries = localStorage.sippodebug.split(',').map((entry) => {
			let [id, value] = entry.split('=');
			any = any || entry === '*';
			return {
				id: id,
				value: value || 'DEBUG',
			};
		});
		for (let i = 0; i < entries.length; i++) {
			let entry = entries[i];
			if (entry.id === name && this.LEVELS[entry.value]) {
				return this.LEVELS[entry.value];
			}
		}
		return any ? this.LEVELS.DEBUG : this.LEVELS.WARN;
	}

	/**
	 * Get a js-logger named logger
	 *
	 * @param {String} name logger identifier
	 *
	 * @return {Logger} js-logger named instance
	 */
	getLogger(name) {
		let logger = Logger.get(name);
		logger.setLevel(this.getLevelFromLocalStorage(name));
		return logger;
	}

	/**
	 * Set global log level
	 *
	 * @param {String} name named logger identifier
	 * @param {Object} level selected log level.
	 *
	 * @return {Void} this method doesn't return anything
	 */
	setLevel(name, level) {
		Logger.get(name).setLevel(level);
	}

	/**
	 * Add log handler: function called every time a log message is
	 * created.
	 *
	 * @param {Function} fn log handler. Expects two arguments; the first
	 * being the log messages to output and the latter being a context
	 * object which can be inspected by the log handler.
	 *
	 * @return {Logs} self
	 */
	addHandler(fn) {
		if (this._handlers.indexOf(fn) === -1) {
			this._handlers.push(fn);
		}
		return this;
	}

	/**
	 * Remove a log handler
	 *
	 * @param {Function} fn log handler
	 *
	 * @return {Logs} self
	 */
	removeHandler(fn) {
		this._handlers = this._handlers.filter(function(handler) {
			return handler !== fn;
		});
	}

	/**
	 * Add Logger name and current time to log messages.
	 *
	 * @private
	 *
	 * @param {Array<Object>} messages message array to log
	 * @param {Object} context js-logger context
	 *
	 * @return {Void} this method doesn't return anything
	 */
	_formatter(messages, context) {
		let time = new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
		for (let i = 0; i < messages.length; i++) {
			if (messages[i] instanceof Error && messages[i].stack) {
				messages[i] = messages[i].stack;
			}
		}
		messages.unshift(`[${time}] ${context.name}`);
	}
}