diff --git a/packages/rocketchat-logger/client/viewLogs.coffee b/packages/rocketchat-logger/client/viewLogs.coffee deleted file mode 100644 index b325b4502def..000000000000 --- a/packages/rocketchat-logger/client/viewLogs.coffee +++ /dev/null @@ -1,17 +0,0 @@ -@stdout = new Mongo.Collection 'stdout' - -Meteor.startup -> - RocketChat.AdminBox.addOption - href: 'admin-view-logs' - i18nLabel: 'View_Logs' - permissionGranted: -> - return RocketChat.authz.hasAllPermission('view-logs') - -FlowRouter.route '/admin/view-logs', - name: 'admin-view-logs' - action: (params) -> - BlazeLayout.render 'main', - center: 'pageSettingsContainer' - pageTitle: t('View_Logs') - pageTemplate: 'viewLogs' - noScroll: true diff --git a/packages/rocketchat-logger/client/viewLogs.js b/packages/rocketchat-logger/client/viewLogs.js new file mode 100644 index 000000000000..680ff3813fd8 --- /dev/null +++ b/packages/rocketchat-logger/client/viewLogs.js @@ -0,0 +1,24 @@ + +this.stdout = new Mongo.Collection('stdout'); + +Meteor.startup(function() { + return RocketChat.AdminBox.addOption({ + href: 'admin-view-logs', + i18nLabel: 'View_Logs', + permissionGranted() { + return RocketChat.authz.hasAllPermission('view-logs'); + } + }); +}); + +FlowRouter.route('/admin/view-logs', { + name: 'admin-view-logs', + action() { + return BlazeLayout.render('main', { + center: 'pageSettingsContainer', + pageTitle: t('View_Logs'), + pageTemplate: 'viewLogs', + noScroll: true + }); + } +}); diff --git a/packages/rocketchat-logger/client/views/viewLogs.coffee b/packages/rocketchat-logger/client/views/viewLogs.coffee deleted file mode 100644 index 29a96bf9e15a..000000000000 --- a/packages/rocketchat-logger/client/views/viewLogs.coffee +++ /dev/null @@ -1,106 +0,0 @@ -import moment from 'moment' - -Template.viewLogs.onCreated -> - @subscribe 'stdout' - @atBottom = true - - -Template.viewLogs.helpers - hasPermission: -> - return RocketChat.authz.hasAllPermission 'view-logs' - - logs: -> - return stdout.find({}, {sort: {ts: 1}}) - - ansispan: (string) -> - string = ansispan(string.replace(/\s/g, ' ').replace(/(\\n|\n)/g, '
')) - string = string.replace(/(.\d{8}-\d\d:\d\d:\d\d\.\d\d\d\(?.{0,2}\)?)/, '$1') - return string - - formatTS: (date) -> - return moment(date).format('YMMDD-HH:mm:ss.SSS(ZZ)') - - -Template.viewLogs.events - 'click .new-logs': (e) -> - Template.instance().atBottom = true - Template.instance().sendToBottomIfNecessary() - - -Template.viewLogs.onRendered -> - wrapper = this.find('.terminal') - wrapperUl = this.find('.terminal') - newLogs = this.find('.new-logs') - - template = this - - template.isAtBottom = (scrollThreshold) -> - if not scrollThreshold? then scrollThreshold = 0 - if wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight - newLogs.className = "new-logs not" - return true - return false - - template.sendToBottom = -> - wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight - newLogs.className = "new-logs not" - - template.checkIfScrollIsAtBottom = -> - template.atBottom = template.isAtBottom(100) - readMessage.enable() - readMessage.read() - - template.sendToBottomIfNecessary = -> - if template.atBottom is true and template.isAtBottom() isnt true - template.sendToBottom() - else if template.atBottom is false - newLogs.className = "new-logs" - - template.sendToBottomIfNecessaryDebounced = _.debounce template.sendToBottomIfNecessary, 10 - - template.sendToBottomIfNecessary() - - if not window.MutationObserver? - wrapperUl.addEventListener 'DOMSubtreeModified', -> - template.sendToBottomIfNecessaryDebounced() - else - observer = new MutationObserver (mutations) -> - mutations.forEach (mutation) -> - template.sendToBottomIfNecessaryDebounced() - - observer.observe wrapperUl, - childList: true - - template.onWindowResize = -> - Meteor.defer -> - template.sendToBottomIfNecessaryDebounced() - - window.addEventListener 'resize', template.onWindowResize - - wrapper.addEventListener 'mousewheel', -> - template.atBottom = false - Meteor.defer -> - template.checkIfScrollIsAtBottom() - - wrapper.addEventListener 'wheel', -> - template.atBottom = false - Meteor.defer -> - template.checkIfScrollIsAtBottom() - - wrapper.addEventListener 'touchstart', -> - template.atBottom = false - - wrapper.addEventListener 'touchend', -> - Meteor.defer -> - template.checkIfScrollIsAtBottom() - Meteor.setTimeout -> - template.checkIfScrollIsAtBottom() - , 1000 - Meteor.setTimeout -> - template.checkIfScrollIsAtBottom() - , 2000 - - wrapper.addEventListener 'scroll', -> - template.atBottom = false - Meteor.defer -> - template.checkIfScrollIsAtBottom() diff --git a/packages/rocketchat-logger/client/views/viewLogs.js b/packages/rocketchat-logger/client/views/viewLogs.js new file mode 100644 index 000000000000..d6664b34af75 --- /dev/null +++ b/packages/rocketchat-logger/client/views/viewLogs.js @@ -0,0 +1,124 @@ +import moment from 'moment'; +// TODO: remove this globals +/* globals ansispan stdout readMessage*/ + +Template.viewLogs.onCreated(function() { + this.subscribe('stdout'); + return this.atBottom = true; +}); + +Template.viewLogs.helpers({ + hasPermission() { + return RocketChat.authz.hasAllPermission('view-logs'); + }, + logs() { + return stdout.find({}, { + sort: { + ts: 1 + } + }); + }, + ansispan(string) { + string = ansispan(string.replace(/\s/g, ' ').replace(/(\\n|\n)/g, '
')); + string = string.replace(/(.\d{8}-\d\d:\d\d:\d\d\.\d\d\d\(?.{0,2}\)?)/, '$1'); + return string; + }, + formatTS(date) { + return moment(date).format('YMMDD-HH:mm:ss.SSS(ZZ)'); + } +}); + +Template.viewLogs.events({ + 'click .new-logs'() { + Template.instance().atBottom = true; + return Template.instance().sendToBottomIfNecessary(); + } +}); + +Template.viewLogs.onRendered(function() { + + const wrapper = this.find('.terminal'); + const wrapperUl = this.find('.terminal'); + const newLogs = this.find('.new-logs'); + const template = this; + template.isAtBottom = function(scrollThreshold) { + if (scrollThreshold == null) { + scrollThreshold = 0; + } + if (wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight) { + newLogs.className = 'new-logs not'; + return true; + } + return false; + }; + template.sendToBottom = function() { + wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight; + return newLogs.className = 'new-logs not'; + }; + template.checkIfScrollIsAtBottom = function() { + template.atBottom = template.isAtBottom(100); + readMessage.enable(); + return readMessage.read(); + }; + template.sendToBottomIfNecessary = function() { + if (template.atBottom === true && template.isAtBottom() !== true) { + return template.sendToBottom(); + } else if (template.atBottom === false) { + return newLogs.className = 'new-logs'; + } + }; + template.sendToBottomIfNecessaryDebounced = _.debounce(template.sendToBottomIfNecessary, 10); + template.sendToBottomIfNecessary(); + if (window.MutationObserver == null) { + wrapperUl.addEventListener('DOMSubtreeModified', function() { + return template.sendToBottomIfNecessaryDebounced(); + }); + } else { + const observer = new MutationObserver(function(mutations) { + return mutations.forEach(function() { + return template.sendToBottomIfNecessaryDebounced(); + }); + }); + observer.observe(wrapperUl, { + childList: true + }); + } + template.onWindowResize = function() { + return Meteor.defer(function() { + return template.sendToBottomIfNecessaryDebounced(); + }); + }; + window.addEventListener('resize', template.onWindowResize); + wrapper.addEventListener('mousewheel', function() { + template.atBottom = false; + return Meteor.defer(function() { + return template.checkIfScrollIsAtBottom(); + }); + }); + wrapper.addEventListener('wheel', function() { + template.atBottom = false; + return Meteor.defer(function() { + return template.checkIfScrollIsAtBottom(); + }); + }); + wrapper.addEventListener('touchstart', function() { + return template.atBottom = false; + }); + wrapper.addEventListener('touchend', function() { + Meteor.defer(function() { + return template.checkIfScrollIsAtBottom(); + }); + Meteor.setTimeout(function() { + return template.checkIfScrollIsAtBottom(); + }, 1000); + return Meteor.setTimeout(function() { + return template.checkIfScrollIsAtBottom(); + }, 2000); + }); + return wrapper.addEventListener('scroll', function() { + template.atBottom = false; + return Meteor.defer(function() { + return template.checkIfScrollIsAtBottom(); + }); + }); +}); diff --git a/packages/rocketchat-logger/logger.coffee b/packages/rocketchat-logger/logger.coffee deleted file mode 100644 index dd6a6983cc7e..000000000000 --- a/packages/rocketchat-logger/logger.coffee +++ /dev/null @@ -1,73 +0,0 @@ -Template = Package.templating.Template - -Template.log = false -Template.logMatch = /.*/ - -Template.enableLogs = (log) -> - Template.logMatch = /.*/ - - if log is false - Template.log = false - else - Template.log = true - if log instanceof RegExp - Template.logMatch = log - - -wrapHelpersAndEvents = (original, prefix, color) -> - return (dict) -> - template = @ - - for name, fn of dict - do (name, fn) -> - if fn instanceof Function - dict[name] = -> - result = fn.apply @, arguments - - if Template.log is true - completeName = "#{prefix}:#{template.viewName.replace('Template.', '')}.#{name}" - - if Template.logMatch.test completeName - console.log "%c#{completeName}", "color: #{color}", {args: arguments, scope: @, result: result} - - return result - - original.call template, dict - - -Template.prototype.helpers = wrapHelpersAndEvents Template.prototype.helpers, 'helper', 'blue' -Template.prototype.events = wrapHelpersAndEvents Template.prototype.events, 'event', 'green' - - -wrapLifeCycle = (original, prefix, color) -> - return (fn) -> - template = @ - - if fn instanceof Function - wrap = -> - result = fn.apply @, arguments - - if Template.log is true - completeName = "#{prefix}:#{template.viewName.replace('Template.', '')}" - - if Template.logMatch.test completeName - console.log "%c#{completeName}", "color: #{color}; font-weight: bold", {args: arguments, scope: @, result: result} - - return result - - original.call template, wrap - else - original.call template, fn - - -Template.prototype.onCreated = wrapLifeCycle Template.prototype.onCreated, 'onCreated', 'blue' -Template.prototype.onRendered = wrapLifeCycle Template.prototype.onRendered, 'onRendered', 'green' -Template.prototype.onDestroyed = wrapLifeCycle Template.prototype.onDestroyed, 'onDestroyed', 'red' - -# stdout = new Mongo.Collection 'stdout' - -# Meteor.subscribe 'stdout' - -# stdout.find().observe -# added: (record) -> -# console.log ansispan record.string diff --git a/packages/rocketchat-logger/logger.js b/packages/rocketchat-logger/logger.js new file mode 100644 index 000000000000..2533b23828d1 --- /dev/null +++ b/packages/rocketchat-logger/logger.js @@ -0,0 +1,82 @@ +const Template = Package.templating.Template; + +Template.log = false; + +Template.logMatch = /.*/; + +Template.enableLogs = function(log) { + Template.logMatch = /.*/; + if (log === false) { + return Template.log = false; + } else { + Template.log = true; + if (log instanceof RegExp) { + return Template.logMatch = log; + } + } +}; + +const wrapHelpersAndEvents = function(original, prefix, color) { + return function(dict) { + + const template = this; + const fn1 = function(name, fn) { + if (fn instanceof Function) { + return dict[name] = function() { + + const result = fn.apply(this, arguments); + if (Template.log === true) { + const completeName = `${ prefix }:${ template.viewName.replace('Template.', '') }.${ name }`; + if (Template.logMatch.test(completeName)) { + console.log(`%c${ completeName }`, `color: ${ color }`, { + args: arguments, + scope: this, + result + }); + } + } + return result; + }; + } + }; + _.each(name, (fn, name) => { + fn1(name, fn); + }); + return original.call(template, dict); + }; +}; + +Template.prototype.helpers = wrapHelpersAndEvents(Template.prototype.helpers, 'helper', 'blue'); + +Template.prototype.events = wrapHelpersAndEvents(Template.prototype.events, 'event', 'green'); + +const wrapLifeCycle = function(original, prefix, color) { + return function(fn) { + const template = this; + if (fn instanceof Function) { + const wrap = function() { + const result = fn.apply(this, arguments); + if (Template.log === true) { + const completeName = `${ prefix }:${ template.viewName.replace('Template.', '') }.${ name }`; + if (Template.logMatch.test(completeName)) { + console.log(`%c${ completeName }`, `color: ${ color }; font-weight: bold`, { + args: arguments, + scope: this, + result + }); + } + } + return result; + }; + return original.call(template, wrap); + } else { + return original.call(template, fn); + } + }; +}; + +Template.prototype.onCreated = wrapLifeCycle(Template.prototype.onCreated, 'onCreated', 'blue'); + +Template.prototype.onRendered = wrapLifeCycle(Template.prototype.onRendered, 'onRendered', 'green'); + +Template.prototype.onDestroyed = wrapLifeCycle(Template.prototype.onDestroyed, 'onDestroyed', 'red'); diff --git a/packages/rocketchat-logger/package.js b/packages/rocketchat-logger/package.js index 33bc00344bfd..f42adb6ff48c 100644 --- a/packages/rocketchat-logger/package.js +++ b/packages/rocketchat-logger/package.js @@ -7,7 +7,6 @@ Package.describe({ Package.onUse(function(api) { api.use('mongo'); api.use('ecmascript'); - api.use('coffeescript'); api.use('underscore'); api.use('random'); api.use('logging'); @@ -17,12 +16,14 @@ Package.onUse(function(api) { api.use('kadira:flow-router', 'client'); api.addFiles('ansispan.js', 'client'); - api.addFiles('logger.coffee', 'client'); - api.addFiles('client/viewLogs.coffee', 'client'); + api.addFiles('logger.js', 'client'); + api.addFiles('client/viewLogs.js', 'client'); api.addFiles('client/views/viewLogs.html', 'client'); - api.addFiles('client/views/viewLogs.coffee', 'client'); + api.addFiles('client/views/viewLogs.js', 'client'); - api.addFiles('server.coffee', 'server'); + api.addFiles('server.js', 'server'); api.export('Logger'); + api.export('SystemLogger'); + api.export('LoggerManager'); }); diff --git a/packages/rocketchat-logger/server.coffee b/packages/rocketchat-logger/server.coffee deleted file mode 100644 index ceae9e07d4f8..000000000000 --- a/packages/rocketchat-logger/server.coffee +++ /dev/null @@ -1,352 +0,0 @@ -@LoggerManager = new class extends EventEmitter - constructor: -> - @enabled = false - @loggers = {} - @queue = [] - - @showPackage = false - @showFileAndLine = false - @logLevel = 0 - - register: (logger) -> - if not logger instanceof Logger - return - - @loggers[logger.name] = logger - - @emit 'register', logger - - addToQueue: (logger, args)-> - @queue.push - logger: logger - args: args - - dispatchQueue: -> - for item in @queue - item.logger._log.apply item.logger, item.args - - @clearQueue() - - clearQueue: -> - @queue = [] - - disable: -> - @enabled = false - - enable: (dispatchQueue=false) -> - @enabled = true - if dispatchQueue is true - @dispatchQueue() - else - @clearQueue() - - -# @LoggerManager.on 'register', -> -# console.log('on register', arguments) - - -@Logger = class Logger - defaultTypes: - debug: - name: 'debug' - color: 'blue' - level: 2 - log: - name: 'info' - color: 'blue' - level: 1 - info: - name: 'info' - color: 'blue' - level: 1 - success: - name: 'info' - color: 'green' - level: 1 - warn: - name: 'warn' - color: 'magenta' - level: 1 - error: - name: 'error' - color: 'red' - level: 0 - - constructor: (@name, config={}) -> - self = @ - @config = {} - - _.extend @config, config - - if LoggerManager.loggers[@name]? - LoggerManager.loggers[@name].warn 'Duplicated instance' - return LoggerManager.loggers[@name] - - for type, typeConfig of @defaultTypes - do (type, typeConfig) -> - self[type] = (args...) -> - self._log.call self, - section: this.__section - type: type - level: typeConfig.level - method: typeConfig.name - arguments: args - - self[type+"_box"] = (args...) -> - self._log.call self, - section: this.__section - type: type - box: true - level: typeConfig.level - method: typeConfig.name - arguments: args - - if @config.methods? - for method, typeConfig of @config.methods - do (method, typeConfig) -> - if self[method]? - self.warn "Method", method, "already exists" - - if not self.defaultTypes[typeConfig.type]? - self.warn "Method type", typeConfig.type, "does not exist" - - self[method] = (args...) -> - self._log.call self, - section: this.__section - type: typeConfig.type - level: if typeConfig.level? then typeConfig.level else self.defaultTypes[typeConfig.type]?.level - method: method - arguments: args - - self[method+"_box"] = (args...) -> - self._log.call self, - section: this.__section - type: typeConfig.type - box: true - level: if typeConfig.level? then typeConfig.level else self.defaultTypes[typeConfig.type]?.level - method: method - arguments: args - - if @config.sections? - for section, name of @config.sections - do (section, name) -> - self[section] = {} - for type, typeConfig of self.defaultTypes - do (type, typeConfig) => - self[section][type] = => - self[type].apply {__section: name}, arguments - - self[section][type+"_box"] = => - self[type+"_box"].apply {__section: name}, arguments - - for method, typeConfig of self.config.methods - do (method, typeConfig) => - self[section][method] = => - self[method].apply {__section: name}, arguments - - self[section][method+"_box"] = => - self[method+"_box"].apply {__section: name}, arguments - - LoggerManager.register @ - return @ - - getPrefix: (options) -> - if options.section? - prefix = "#{@name} ➔ #{options.section}.#{options.method}" - else - prefix = "#{@name} ➔ #{options.method}" - - details = @_getCallerDetails() - - detailParts = [] - if details.package? and (LoggerManager.showPackage is true or options.type is 'error') - detailParts.push details.package - - if LoggerManager.showFileAndLine is true or options.type is 'error' - if details.file? and details.line? - detailParts.push "#{details.file}:#{details.line}" - else - if details.file? - detailParts.push details.file - if details.line? - detailParts.push details.line - - if @defaultTypes[options.type]? - prefix = prefix[@defaultTypes[options.type].color] - - if detailParts.length > 0 - prefix = "#{detailParts.join(' ')} #{prefix}" - - return prefix - - # @returns {Object: { line: Number, file: String }} - _getCallerDetails: -> - getStack = () -> - # We do NOT use Error.prepareStackTrace here (a V8 extension that gets us a - # pre-parsed stack) since it's impossible to compose it with the use of - # Error.prepareStackTrace used on the server for source maps. - err = new Error - stack = err.stack - return stack - - stack = getStack() - - if not stack - return {} - - lines = stack.split('\n') - - # looking for the first line outside the logging package (or an - # eval if we find that first) - line = undefined - for item, index in lines when index > 0 - line = item - if line.match(/^\s*at eval \(eval/) - return {file: "eval"} - - if not line.match(/packages\/rocketchat_logger(?:\/|\.js)/) - break - - details = {} - - # The format for FF is 'functionName@filePath:lineNumber' - # The format for V8 is 'functionName (packages/logging/logging.js:81)' or - # 'packages/logging/logging.js:81' - match = /(?:[@(]| at )([^(]+?):([0-9:]+)(?:\)|$)/.exec(line) - if not match - return details - # in case the matched block here is line:column - details.line = match[2].split(':')[0] - - # Possible format: https://foo.bar.com/scripts/file.js?random=foobar - # XXX: if you can write the following in better way, please do it - # XXX: what about evals? - details.file = match[1].split('/').slice(-1)[0].split('?')[0] - - packageMatch = match[1].match(/packages\/([^\.\/]+)(?:\/|\.)/) - if packageMatch? - details.package = packageMatch[1] - - return details - - makeABox: (message, title) -> - if not _.isArray(message) - message = message.split("\n") - - len = 0 - for line in message - len = Math.max(len, line.length) - - topLine = "+--" + s.pad('', len, '-') + "--+" - separator = "| " + s.pad('', len, '') + " |" - lines = [] - - lines.push topLine - if title? - lines.push "| " + s.lrpad(title, len) + " |" - lines.push topLine - - lines.push separator - - for line in message - lines.push "| " + s.rpad(line, len) + " |" - - lines.push separator - lines.push topLine - return lines - - - _log: (options) -> - if LoggerManager.enabled is false - LoggerManager.addToQueue @, arguments - return - - options.level ?= 1 - - if LoggerManager.logLevel < options.level - return - - prefix = @getPrefix(options) - - if options.box is true and _.isString(options.arguments[0]) - color = undefined - if @defaultTypes[options.type]? - color = @defaultTypes[options.type].color - - box = @makeABox options.arguments[0], options.arguments[1] - subPrefix = '➔' - if color? - subPrefix = subPrefix[color] - - console.log subPrefix, prefix - for line in box - if color? - console.log subPrefix, line[color] - else - console.log subPrefix, line - else - options.arguments.unshift prefix - console.log.apply console, options.arguments - - return - - -@SystemLogger = new Logger 'System', - methods: - startup: - type: 'success' - level: 0 - - -processString = (string, date) -> - if string[0] is '{' - try - return Log.format EJSON.parse(string), {color: true} - - try - return Log.format {message: string, time: date, level: 'info'}, {color: true} - - return string - -StdOut = new class extends EventEmitter - constructor: -> - @queue = [] - write = process.stdout.write - process.stdout.write = (string, encoding, fd) => - write.apply(process.stdout, arguments) - date = new Date - string = processString string, date - - item = - id: Random.id() - string: string - ts: date - - @queue.push item - - if RocketChat?.settings?.get('Log_View_Limit')? and @queue.length > RocketChat.settings.get('Log_View_Limit') - @queue.shift() - - @emit 'write', string, item - - -Meteor.publish 'stdout', -> - unless @userId - return @ready() - - if RocketChat.authz.hasPermission(@userId, 'view-logs') isnt true - return @ready() - - for item in StdOut.queue - @added 'stdout', item.id, - string: item.string - ts: item.ts - - @ready() - - StdOut.on 'write', (string, item) => - @added 'stdout', item.id, - string: item.string - ts: item.ts - - return diff --git a/packages/rocketchat-logger/server.js b/packages/rocketchat-logger/server.js new file mode 100644 index 000000000000..564751f6f7e1 --- /dev/null +++ b/packages/rocketchat-logger/server.js @@ -0,0 +1,373 @@ +/* globals EventEmitter LoggerManager SystemLogger Log*/ + +//TODO: change this global to import +LoggerManager = new class extends EventEmitter { // eslint-disable-line no-undef + constructor() { + super(); + this.enabled = false; + this.loggers = {}; + this.queue = []; + this.showPackage = false; + this.showFileAndLine = false; + this.logLevel = 0; + } + register(logger) { + if (!logger instanceof Logger) { + return; + } + this.loggers[logger.name] = logger; + this.emit('register', logger); + } + addToQueue(logger, args) { + this.queue.push({ + logger, args + }); + } + dispatchQueue() { + _.each(this.queue, (item) => item.logger._log.apply(item.logger, item.args)); + this.clearQueue(); + } + clearQueue() { + this.queue = []; + } + + disable() { + this.enabled = false; + } + + enable(dispatchQueue = false) { + this.enabled = true; + return (dispatchQueue === true) ? this.dispatchQueue() : this.clearQueue(); + } +}; + + + +const defaultTypes = { + debug: { + name: 'debug', + color: 'blue', + level: 2 + }, + log: { + name: 'info', + color: 'blue', + level: 1 + }, + info: { + name: 'info', + color: 'blue', + level: 1 + }, + success: { + name: 'info', + color: 'green', + level: 1 + }, + warn: { + name: 'warn', + color: 'magenta', + level: 1 + }, + error: { + name: 'error', + color: 'red', + level: 0 + } +}; + +class _Logger { + constructor(name, config = {}) { + self = this; + this.name = name; + + this.config = Object.assign({}, config); + if (LoggerManager.loggers && LoggerManager.loggers[this.name] != null) { + LoggerManager.loggers[this.name].warn('Duplicated instance'); + return LoggerManager.loggers[this.name]; + } + _.each(defaultTypes, (typeConfig, type) => { + this[type] = function(...args) { + return self._log.call(self, { + section: this.__section, + type, + level: typeConfig.level, + method: typeConfig.name, + 'arguments': args + }); + }; + + self[`${ type }_box`] = function(...args) { + return self._log.call(self, { + section: this.__section, + type, + box: true, + level: typeConfig.level, + method: typeConfig.name, + 'arguments': args + }); + }; + }); + if (this.config.methods) { + _.each(this.config.methods, (typeConfig, method) => { + if (this[method] != null) { + self.warn(`Method ${ method } already exists`); + } + if (defaultTypes[typeConfig.type] == null) { + self.warn(`Method type ${ typeConfig.type } does not exist`); + } + this[method] = function(...args) { + return self._log.call(self, { + section: this.__section, + type: typeConfig.type, + level: typeConfig.level != null ? typeConfig.level : defaultTypes[typeConfig.type] && defaultTypes[typeConfig.type].level, + method, + 'arguments': args + }); + }; + this[`${ method }_box`] = function(...args) { + return self._log.call(self, { + section: this.__section, + type: typeConfig.type, + box: true, + level: typeConfig.level != null ? typeConfig.level : defaultTypes[typeConfig.type] && defaultTypes[typeConfig.type].level, + method, + 'arguments': args + }); + }; + }); + } + if (this.config.sections) { + _.each(this.config.sections, (name, section) => { + this[section] = {}; + _.each(defaultTypes, (typeConfig, type) => { + self[section][type] = () => self[type].apply({__section: name}, arguments); + self[section][`${ type }_box`] = () => self[`${ type }_box`].apply({__section: name}, arguments); + }); + _.each(this.config.methods, (typeConfig, method) => { + self[section][method] = () => self[method].apply({__section: name}, arguments); + self[section][`${ method }_box`] = () => self[`${ method }_box`].apply({__section: name}, arguments); + }); + }); + } + + LoggerManager.register(this); + } + getPrefix(options) { + let prefix = `${ this.name } ➔ ${ options.method }`; + if (options.section) { + prefix = `${ this.name } ➔ ${ options.section }.${ options.method }`; + } + const details = this._getCallerDetails(); + const detailParts = []; + if (details['package'] && (LoggerManager.showPackage === true || options.type === 'error')) { + detailParts.push(details['package']); + } + if (LoggerManager.showFileAndLine === true || options.type === 'error') { + if ((details.file != null) && (details.line != null)) { + detailParts.push(`${ details.file }:${ details.line }`); + } else { + if (details.file != null) { + detailParts.push(details.file); + } + if (details.line != null) { + detailParts.push(details.line); + } + } + } + if (defaultTypes[options.type]) { + // format the message to a colored message + prefix = prefix[defaultTypes[options.type].color]; + } + if (detailParts.length > 0) { + prefix = `${ detailParts.join(' ') } ${ prefix }`; + } + return prefix; + } + _getCallerDetails() { + const getStack = () => { + // We do NOT use Error.prepareStackTrace here (a V8 extension that gets us a + // core-parsed stack) since it's impossible to compose it with the use of + // Error.prepareStackTrace used on the server for source maps. + const {stack} = new Error(); + return stack; + }; + const stack = getStack(); + if (!stack) { + return {}; + } + const lines = stack.split('\n').splice(1); + // looking for the first line outside the logging package (or an + // eval if we find that first) + let line = lines[0]; + for (let index = 0, len = lines.length; index < len, index++; line = lines[index]) { + if (line.match(/^\s*at eval \(eval/)) { + return {file: 'eval'}; + } + + if (!line.match(/packages\/rocketchat_logger(?:\/|\.js)/)) { + break; + } + } + + const details = {}; + // The format for FF is 'functionName@filePath:lineNumber' + // The format for V8 is 'functionName (packages/logging/logging.js:81)' or + // 'packages/logging/logging.js:81' + const match = /(?:[@(]| at )([^(]+?):([0-9:]+)(?:\)|$)/.exec(line); + if (!match) { + return details; + } + details.line = match[2].split(':')[0]; + // Possible format: https://foo.bar.com/scripts/file.js?random=foobar + // XXX: if you can write the following in better way, please do it + // XXX: what about evals? + details.file = match[1].split('/').slice(-1)[0].split('?')[0]; + const packageMatch = match[1].match(/packages\/([^\.\/]+)(?:\/|\.)/); + if (packageMatch) { + details['package'] = packageMatch[1]; + } + return details; + } + makeABox(message, title) { + if (!_.isArray(message)) { + message = message.split('\n'); + } + let len = 0; + + len = Math.max.apply(null, message.map(line => line.length)); + + const topLine = `+--${ s.pad('', len, '-') }--+`; + const separator = `| ${ s.pad('', len, '') } |`; + let lines = []; + + lines.push(topLine); + if (title) { + lines.push(`| ${ s.lrpad(title, len) } |`); + lines.push(topLine); + } + lines.push(separator); + + lines = [...lines, ...message.map(line => `| ${ s.rpad(line, len) } |`)]; + + lines.push(separator); + lines.push(topLine); + return lines; + } + + _log(options) { + if (LoggerManager.enabled === false) { + LoggerManager.addToQueue(this, arguments); + return; + } + if (options.level == null) { + options.level = 1; + } + + if (LoggerManager.logLevel < options.level) { + return; + } + + const prefix = this.getPrefix(options); + + if (options.box === true && _.isString(options.arguments[0])) { + let color = undefined; + if (defaultTypes[options.type]) { + color = defaultTypes[options.type].color; + } + + const box = this.makeABox(options.arguments[0], options.arguments[1]); + let subPrefix = '➔'; + if (color) { + subPrefix = subPrefix[color]; + } + + console.log(subPrefix, prefix); + box.forEach(line => { + console.log(subPrefix, color ? line[color]: line); + }); + + } else { + options.arguments.unshift(prefix); + console.log.apply(console, options.arguments); + } + } +} +// TODO: change this global to import +Logger = global.Logger = _Logger; +const processString = function(string, date) { + let obj; + try { + if (string[0] === '{') { + obj = EJSON.parse(string); + } else { + obj = { + message: string, + time: date, + level: 'info' + }; + } + return Log.format(obj, {color: true}); + } catch (error) { + return string; + } +}; +// TODO: change this global to import +SystemLogger = new Logger('System', { // eslint-disable-line no-undef + methods: { + startup: { + type: 'success', + level: 0 + } + } +}); + + +class StdOut extends EventEmitter { + constructor() { + super(); + const write = process.stdout.write; + this.queue = []; + process.stdout.write = (string) => { + write.apply(process.stdout, arguments); + const date = new Date; + string = processString(string, date); + const item = { + id: Random.id(), + string, + ts: date + }; + this.queue.push(item); + const limit = RocketChat.settings.get('Log_View_Limit'); + if (limit && this.queue.length > limit) { + this.queue.shift(); + } + this.emit('write', string, item); + }; + } +} + + +Meteor.publish('stdout', function() { + if (!this.userId || RocketChat.authz.hasPermission(this.userId, 'view-logs') !== true) { + return this.ready(); + } + + StdOut.queue.forEach(item => { + this.added('stdout', item.id, { + string: item.string, + ts: item.ts + }); + }); + + this.ready(); + StdOut.on('write', (string, item) => { + this.added('stdout', item.id, { + string: item.string, + ts: item.ts + }); + }); +}); + + +export { SystemLogger, StdOut, LoggerManager, processString, Logger };