/**
* Log namespace.
* This namespace provides loggers to log messages to the openHAB Log.
*
* @example <caption>Basic logging</caption>
* let log = require('openhab').log('my_logger');
* log.info("Hello World!")
*
* @namespace log
*/
/**
* Logger prefix
*
* @memberof log
*/
const LOGGER_PREFIX = 'org.openhab.automation.openhab-js';
const MessageFormatter = Java.type('org.slf4j.helpers.MessageFormatter');
/**
* Logger class. A named logger providing the ability to log formatted messages.
*
* @memberof log
* @hideconstructor
*/
class Logger {
/**
* Creates a new logger. Don't use directly, use {@link log} on module.
*
* @param {string} _name the name of the logger. Will be prefixed by {@link LOGGER_PREFIX}
* @param {*} _listener a callback to receive logging calls. Can be used to send calls elsewhere, such as escalate errors.
*/
constructor (_name, appenderProvider) {
/** @private */
this._name = _name || this._getCallerDetails('', 3).fileName.replace(/\.[^/.]+$/, '');
/** @private */
this.appenderProvider = appenderProvider;
/** @private */
this._logger = Java.type('org.slf4j.LoggerFactory').getLogger(LOGGER_PREFIX + '.' + this.name.toString().toLowerCase());
}
/**
* Method to determine caller. Don't use directly.
*
* @private
* @param {object} msg the message to get caller details for
* @param {number} ignoreStackDepth the number of stack frames which to ignore in calculating caller
* @returns {Error} message as an error object, with fileName, caller and optional lineNumber properties
*/
_getCallerDetails (msg, ignoreStackDepth) {
let stackLine = null;
if (!(msg instanceof Error)) {
msg = Error(msg);
stackLine = msg.stack.split('\n')[ignoreStackDepth];
} else {
stackLine = msg.stack.split('\n')[1];
}
// pick out the call, fileName & lineNumber from the specific frame
let match = stackLine.match(/^\s+at\s*(?<caller>[^ ]*) \(?(?<fileName>[^:]+):(?<lineNumber>[0-9]+):[0-9]+\)?/);
if (match) {
Object.assign(msg, match.groups);
} else { // won't match an 'eval'd string, so retry
match = stackLine.match(/\s+at\s+<eval>:(?<lineNumber>[0-9]+):[0-9]+/);
if (match) {
Object.assign(msg, {
fileName: '<unknown>',
caller: '<root script>'
}, match.groups);
} else {
Object.assign(msg, {
fileName: '<unknown>',
caller: '<root script>'
});
} // throw Error(`Failed to parse stack line: ${stackLine}`);
}
return msg;
}
/**
* Method to format a log message. Don't use directly.
*
* @private
* @param {object} msg the message to get caller details for
* @param {string} levelString the level being logged at
* @param {number} ignoreStackDepth the number of stack frames which to ignore in calculating caller
* @param {string} [prefix=log] the prefix type, such as none, level, short or log.
* @returns {string} message with 'message' String property
*/
_formatLogMessage (msg, levelString, ignoreStackDepth, prefix = 'none') {
const clazz = this;
const msgData = {
message: msg.toString(),
get caller () { // don't run this unless we need to, then cache it
this.cached = this.cached || clazz._getCallerDetails(msg, ignoreStackDepth);
return this.cached;
}
};
levelString = levelString.toUpperCase();
switch (prefix) {
case 'none': return msgData.message;
case 'level': return `[${levelString}] ${msgData.message}`;
case 'short': return `${msgData.message}\t\t[${this.name}, ${msgData.caller.fileName}:${msgData.caller.lineNumber}]`;
case 'log': return `${msgData.message}\t\t[${this.name} at source ${msgData.caller.fileName}, line ${msgData.caller.lineNumber}]`;
default: throw Error(`Unknown prefix type ${prefix}`);
}
}
/**
* Logs at ERROR level.
* @see atLevel
*/
error () { this.atLevel('error', ...arguments); }
/**
* Logs at ERROR level.
* @see atLevel
*/
warn () { this.atLevel('warn', ...arguments); }
/**
* Logs at INFO level.
* @see atLevel
*/
info () { this.atLevel('info', ...arguments); }
/**
* Logs at DEBUG level.
* @see atLevel
*/
debug () { this.atLevel('debug', ...arguments); }
/**
* Logs at TRACE level.
* @see atLevel
*/
trace () { this.atLevel('trace', ...arguments); }
/**
* Logs a message at the supplied level. The message may include placeholders {} which
* will be substituted into the message string only if the message is actually logged.
*
* @example
* log.atLevel('INFO', 'The widget was created as {}', widget);
*
*
* @param {string} level The level at which to log, such as 'INFO', or 'DEBUG'
* @param {string|Error} msg the message to log, possibly with object placeholders
* @param {object[]} objects=[] the objects to substitute into the log message
*/
atLevel (level, msg, ...objects) {
const titleCase = level[0].toUpperCase() + level.slice(1);
try {
if (this._logger[`is${titleCase}Enabled`]()) {
this.maybeLogWithThrowable(level, msg, objects) ||
this.writeLogLine(level, this._formatLogMessage(msg, level, 6), objects);
}
} catch (err) {
this._logger.error(this._formatLogMessage(err, 'error', 0));
}
}
maybeLogWithThrowable (level, msg, objects) {
if (objects.length === 1) {
const obj = objects[0];
if ((obj instanceof Error || (obj.message && obj.name && obj.stack)) && !msg.includes('{}')) { // todo: better substitution detected
// log the basic message
this.writeLogLine(level, msg, objects);
// and log the exception
this.writeLogLine(level, `${obj.name} : ${obj.message}\n${obj.stack}`);
return true;
}
}
return false;
}
writeLogLine (level, message, objects = []) {
const formatted = MessageFormatter.arrayFormat(message, objects).getMessage();
this._logger[level](formatted);
}
/**
* The listener function attached to this logger.
* @return {*} the listener function
*/
get listener () { return this._listener; }
/**
* The name of this logger
* @return {string} the logger name
*/
get name () { return this._name; }
}
/**
* Creates a logger.
* @see Logger
* @param {string} name the name of the logger
* @memberof log
*/
function newLogger (_name) {
return new Logger(_name);
}
module.exports = newLogger;
module.exports.Logger = Logger;