log.js

  1. /**
  2. * Log namespace.
  3. * This namespace provides loggers to log messages to the openHAB Log.
  4. *
  5. * @example <caption>Basic logging</caption>
  6. * let log = require('openhab').log('my_logger');
  7. * log.info("Hello World!")
  8. *
  9. * @namespace log
  10. */
  11. /**
  12. * Logger prefix
  13. *
  14. * @memberof log
  15. */
  16. const LOGGER_PREFIX = 'org.openhab.automation.openhab-js';
  17. const MessageFormatter = Java.type('org.slf4j.helpers.MessageFormatter');
  18. /**
  19. * Logger class. A named logger providing the ability to log formatted messages.
  20. *
  21. * @memberof log
  22. * @hideconstructor
  23. */
  24. class Logger {
  25. /**
  26. * Creates a new logger. Don't use directly, use {@link log} on module.
  27. *
  28. * @param {string} _name the name of the logger. Will be prefixed by {@link LOGGER_PREFIX}
  29. * @param {*} _listener a callback to receive logging calls. Can be used to send calls elsewhere, such as escalate errors.
  30. */
  31. constructor (_name, appenderProvider) {
  32. /** @private */
  33. this._name = _name || this._getCallerDetails('', 3).fileName.replace(/\.[^/.]+$/, '');
  34. /** @private */
  35. this.appenderProvider = appenderProvider;
  36. /** @private */
  37. this._logger = Java.type('org.slf4j.LoggerFactory').getLogger(LOGGER_PREFIX + '.' + this.name.toString());
  38. }
  39. /**
  40. * Method to determine caller. Don't use directly.
  41. *
  42. * @private
  43. * @param {object} msg the message to get caller details for
  44. * @param {number} ignoreStackDepth the number of stack frames which to ignore in calculating caller
  45. * @returns {Error} message as an error object, with fileName, caller and optional lineNumber properties
  46. */
  47. _getCallerDetails (msg, ignoreStackDepth) {
  48. let stackLine = null;
  49. if (!(msg instanceof Error)) {
  50. msg = Error(msg);
  51. stackLine = msg.stack.split('\n')[ignoreStackDepth];
  52. } else {
  53. stackLine = msg.stack.split('\n')[1];
  54. }
  55. // pick out the call, fileName & lineNumber from the specific frame
  56. let match = stackLine.match(/^\s+at\s*(?<caller>[^ ]*) \(?(?<fileName>[^:]+):(?<lineNumber>[0-9]+):[0-9]+\)?/);
  57. if (match) {
  58. Object.assign(msg, match.groups);
  59. } else { // won't match an 'eval'd string, so retry
  60. match = stackLine.match(/\s+at\s+<eval>:(?<lineNumber>[0-9]+):[0-9]+/);
  61. if (match) {
  62. Object.assign(msg, {
  63. fileName: '<unknown>',
  64. caller: '<root script>'
  65. }, match.groups);
  66. } else {
  67. Object.assign(msg, {
  68. fileName: '<unknown>',
  69. caller: '<root script>'
  70. });
  71. } // throw Error(`Failed to parse stack line: ${stackLine}`);
  72. }
  73. return msg;
  74. }
  75. /**
  76. * Method to format a log message. Don't use directly.
  77. *
  78. * @private
  79. * @param {object} msg the message to get caller details for
  80. * @param {string} levelString the level being logged at
  81. * @param {number} ignoreStackDepth the number of stack frames which to ignore in calculating caller
  82. * @param {string} [prefix=log] the prefix type, such as none, level, short or log.
  83. * @returns {string} message with 'message' String property
  84. */
  85. _formatLogMessage (msg, levelString, ignoreStackDepth, prefix = 'none') {
  86. const clazz = this;
  87. const msgData = {
  88. message: msg.toString(),
  89. get caller () { // don't run this unless we need to, then cache it
  90. this.cached = this.cached || clazz._getCallerDetails(msg, ignoreStackDepth);
  91. return this.cached;
  92. }
  93. };
  94. levelString = levelString.toUpperCase();
  95. switch (prefix) {
  96. case 'none': return msgData.message;
  97. case 'level': return `[${levelString}] ${msgData.message}`;
  98. case 'short': return `${msgData.message}\t\t[${this.name}, ${msgData.caller.fileName}:${msgData.caller.lineNumber}]`;
  99. case 'log': return `${msgData.message}\t\t[${this.name} at source ${msgData.caller.fileName}, line ${msgData.caller.lineNumber}]`;
  100. default: throw Error(`Unknown prefix type ${prefix}`);
  101. }
  102. }
  103. /**
  104. * Logs at ERROR level.
  105. * @see atLevel
  106. */
  107. error () { this.atLevel('error', ...arguments); }
  108. /**
  109. * Logs at ERROR level.
  110. * @see atLevel
  111. */
  112. warn () { this.atLevel('warn', ...arguments); }
  113. /**
  114. * Logs at INFO level.
  115. * @see atLevel
  116. */
  117. info () { this.atLevel('info', ...arguments); }
  118. /**
  119. * Logs at DEBUG level.
  120. * @see atLevel
  121. */
  122. debug () { this.atLevel('debug', ...arguments); }
  123. /**
  124. * Logs at TRACE level.
  125. * @see atLevel
  126. */
  127. trace () { this.atLevel('trace', ...arguments); }
  128. /**
  129. * Logs a message at the supplied level. The message may include placeholders {} which
  130. * will be substituted into the message string only if the message is actually logged.
  131. *
  132. * @example
  133. * log.atLevel('INFO', 'The widget was created as {}', widget);
  134. *
  135. *
  136. * @param {string} level The level at which to log, such as 'INFO', or 'DEBUG'
  137. * @param {string|Error} msg the message to log, possibly with object placeholders
  138. * @param {object[]} objects=[] the objects to substitute into the log message
  139. */
  140. atLevel (level, msg, ...objects) {
  141. const titleCase = level[0].toUpperCase() + level.slice(1);
  142. try {
  143. if (this._logger[`is${titleCase}Enabled`]()) {
  144. this.maybeLogWithThrowable(level, msg, objects) ||
  145. this.writeLogLine(level, this._formatLogMessage(msg, level, 6), objects);
  146. }
  147. } catch (err) {
  148. this._logger.error(this._formatLogMessage(err, 'error', 0));
  149. }
  150. }
  151. maybeLogWithThrowable (level, msg, objects) {
  152. if (objects.length === 1) {
  153. const obj = objects[0];
  154. if ((obj instanceof Error || (obj.message && obj.name && obj.stack)) && !msg.includes('{}')) { // todo: better substitution detected
  155. // log the basic message
  156. this.writeLogLine(level, msg, objects);
  157. // and log the exception
  158. this.writeLogLine(level, `${obj.name} : ${obj.message}\n${obj.stack}`);
  159. return true;
  160. }
  161. }
  162. return false;
  163. }
  164. writeLogLine (level, message, objects = []) {
  165. const formatted = MessageFormatter.arrayFormat(message, objects).getMessage();
  166. this._logger[level](formatted);
  167. }
  168. /**
  169. * The listener function attached to this logger.
  170. * @return {*} the listener function
  171. */
  172. get listener () { return this._listener; }
  173. /**
  174. * The name of this logger
  175. * @return {string} the logger name
  176. */
  177. get name () { return this._name; }
  178. }
  179. /**
  180. * Creates a logger.
  181. * @see Logger
  182. * @param {string} name the name of the logger
  183. * @memberof log
  184. */
  185. function newLogger (_name) {
  186. return new Logger(_name);
  187. }
  188. module.exports = newLogger;
  189. module.exports.Logger = Logger;