The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/app/logger/server/server.js

321 lines
8.0 KiB

import { EventEmitter } from 'events';
import _ from 'underscore';
import s from 'underscore.string';
export const LoggerManager = new class extends EventEmitter {
constructor() {
super();
this.enabled = false;
this.loggers = {};
this.queue = [];
this.showPackage = false;
this.showFileAndLine = false;
this.logLevel = 0;
}
register(logger) {
// eslint-disable-next-line no-use-before-define
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,
},
deprecation: {
name: 'warn',
color: 'magenta',
level: 0,
},
};
export class Logger {
constructor(name, config = {}) {
const 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] = (...args) => this[type].apply({ __section: name }, args);
self[section][`${ type }_box`] = (...args) => this[`${ type }_box`].apply({ __section: name }, args);
});
_.each(this.config.methods, (typeConfig, method) => {
self[section][method] = (...args) => self[method].apply({ __section: name }, args);
self[section][`${ method }_box`] = (...args) => self[`${ method }_box`].apply({ __section: name }, args);
});
});
}
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, ...args) {
if (LoggerManager.enabled === false) {
LoggerManager.addToQueue(this, [options, ...args]);
return;
}
if (options.level == null) {
options.level = 1;
}
if (LoggerManager.logLevel < options.level) {
return;
}
// Deferred logging
if (typeof options.arguments[0] === 'function') {
options.arguments[0] = options.arguments[0]();
}
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);
}
}
}
export const SystemLogger = new Logger('System', {
methods: {
startup: {
type: 'success',
level: 0,
},
},
});