/** * 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()); } /** * 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;