refactor(@embark/core): refactor logger to provide correct origins

Also make a few more revisions:

Revise the "write logs" testing strategy such that it's not necessary for the
logger functions to take an optional callback.

Drop unused `tmp` package from `packages/core/logger` since it's not used in
the tests.

Strip colors before writing to the log file, use a two-space delimiter between
sections of each logged line in the log file, and collapse whitespace in the
message section of each line. These changes make the log file more amenable to
being processed with cli tools such as awk, cut, etc. It's also possible in a
text editor to replace `'  '` with `\t` and then load the file in a spreadsheet,
with each line-section in its own column.

Rearrange the sections of each logged line so that it's easier to read, and
only include the origin if the loglevel is debug or trace.
This commit is contained in:
Michael Bradley, Jr 2020-01-28 19:00:23 -06:00 committed by Michael Bradley
parent 60cbb9fa3e
commit 5b988ead7a
4 changed files with 155 additions and 101 deletions

View File

@ -49,7 +49,8 @@
"@babel/runtime-corejs3": "7.8.3", "@babel/runtime-corejs3": "7.8.3",
"colors": "1.4.0", "colors": "1.4.0",
"core-js": "3.6.4", "core-js": "3.6.4",
"date-and-time": "0.12.0" "date-and-time": "0.12.0",
"fs-extra": "8.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.7.7", "@babel/core": "7.7.7",
@ -59,8 +60,7 @@
"eslint": "6.8.0", "eslint": "6.8.0",
"jest": "24.9.0", "jest": "24.9.0",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"rimraf": "3.0.0", "rimraf": "3.0.0"
"tmp": "0.1.0"
}, },
"engines": { "engines": {
"node": ">=10.17.0", "node": ">=10.17.0",

View File

@ -1,11 +1,12 @@
const async = require('async'); const async = require('async');
require('colors'); require('colors');
const fs = require('fs'); const fs = require('fs-extra');
const date = require('date-and-time'); const date = require('date-and-time');
const { escapeHtml } = require('./utils'); const { escapeHtml } = require('./utils');
const util = require('util'); const util = require('util');
const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS'; const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS';
const DELIM = ' ';
export const LogLevels = { export const LogLevels = {
error: 'error', error: 'error',
@ -21,28 +22,45 @@ export class Logger {
this.logLevel = options.logLevel || 'info'; this.logLevel = options.logLevel || 'info';
this._logFunction = options.logFunction || console.log; this._logFunction = options.logFunction || console.log;
this.fs = options.fs || fs; this.fs = options.fs || fs;
this.logFunction = function(...args) {
const color = args[args.length - 1];
args.splice(args.length - 1, 1);
this._logFunction(...args.filter(arg => arg ?? false).map(arg => {
if (color) {
return typeof arg === 'object' ? util.inspect(arg, 2)[color] : arg[color];
}
return typeof arg === 'object' ? util.inspect(arg, 2) : arg;
}));
};
this.logFile = options.logFile;
this.writeToFile = async.cargo((tasks, callback) => { this.logFunction = function(args, color) {
args = Array.isArray(args) ? args : [args];
this._logFunction(...(args.filter(arg => arg ?? false).map(arg => {
if (typeof arg === 'object') arg = util.inspect(arg, 2);
return color ? arg[color] : arg;
})));
};
this.logFile = options.logFile;
if (this.logFile) {
this.fs.ensureFileSync(this.logFile);
}
const isDebugOrTrace = ['debug', 'trace'].includes(this.logLevel);
this.isDebugOrTrace = isDebugOrTrace;
const noop = () => {};
this.writeToFile = async.cargo((tasks, callback = noop) => {
if (!this.logFile) { if (!this.logFile) {
return callback(); return callback();
} }
let logs = ''; let logs = '';
let origin = "[" + ((new Error().stack).split("at ")[3]).trim() + "]";
tasks.forEach(task => { tasks.forEach(task => {
logs += `[${date.format(new Date(), DATE_FORMAT)}] ${task.prefix} ${task.args}\n`; let message = [].concat(task.args).join(' ').trim();
if (!message) return;
const dts = `[${date.format(new Date(), DATE_FORMAT)}]`;
message = message.replace(/\s+/g, ' ');
let origin = '';
if (isDebugOrTrace) origin = `${DELIM}${task.origin.match(/^at\s+.*(\(.*\))/)[1] || '(unknown)'}`;
const prefix = task.prefix;
logs += `${dts}${DELIM}${prefix}${DELIM}${message}${origin}\n`;
}); });
this.fs.appendFile(this.logFile, `\n${origin} ${logs}`, err => {
if (!logs) {
callback();
}
this.fs.appendFile(this.logFile, logs.stripColors, err => {
if (err) { if (err) {
this.logFunction(`There was an error writing to the log file: ${err}`, 'red'); this.logFunction(`There was an error writing to the log file: ${err}`, 'red');
return callback(err); return callback(err);
@ -71,15 +89,18 @@ export class Logger {
return; return;
} }
let callback = () => {};
if (typeof args[args.length - 1] === 'function') {
callback = args[args.length - 1];
args.splice(args.length - 1, 1);
}
this.events.emit("log", "error", args); this.events.emit("log", "error", args);
this.logFunction(...args, 'red'); this.logFunction(args, 'red');
this.writeToFile.push({ prefix: "[error]: ", args }, callback);
let origin;
if (this.isDebugOrTrace) {
try {
const stack = new Error().stack;
origin = stack.split('\n')[2].trim();
// eslint-disable-next-line no-empty
} catch (e) {}
}
this.writeToFile.push({ args, origin, prefix: "[error]" });
} }
warn(...args) { warn(...args) {
@ -87,15 +108,18 @@ export class Logger {
return; return;
} }
let callback = () => {};
if (typeof args[args.length - 1] === 'function') {
callback = args[args.length - 1];
args.splice(args.length - 1, 1);
}
this.events.emit("log", "warn", args); this.events.emit("log", "warn", args);
this.logFunction(...args, 'yellow'); this.logFunction(args, 'yellow');
this.writeToFile.push({ prefix: "[warning]: ", args }, callback);
let origin;
if (this.isDebugOrTrace) {
try {
const stack = new Error().stack;
origin = stack.split('\n')[2].trim();
// eslint-disable-next-line no-empty
} catch (e) {}
}
this.writeToFile.push({ args, origin, prefix: "[warn]" });
} }
info(...args) { info(...args) {
@ -103,15 +127,18 @@ export class Logger {
return; return;
} }
let callback = () => {};
if (typeof args[args.length - 1] === 'function') {
callback = args[args.length - 1];
args.splice(args.length - 1, 1);
}
this.events.emit("log", "info", args); this.events.emit("log", "info", args);
this.logFunction(...args, 'green'); this.logFunction(args, 'green');
this.writeToFile.push({ prefix: "[info]: ", args }, callback);
let origin;
if (this.isDebugOrTrace) {
try {
const stack = new Error().stack;
origin = stack.split('\n')[2].trim();
// eslint-disable-next-line no-empty
} catch (e) {}
}
this.writeToFile.push({ args, origin, prefix: "[info]" });
} }
consoleOnly(...args) { consoleOnly(...args) {
@ -119,14 +146,17 @@ export class Logger {
return; return;
} }
let callback = () => {}; this.logFunction(args, 'green');
if (typeof args[args.length - 1] === 'function') {
callback = args[args.length - 1];
args.splice(args.length - 1, 1);
}
this.logFunction(...args, 'green'); let origin;
this.writeToFile.push({prefix: "[consoleOnly]: ", args }, callback); if (this.isDebugOrTrace) {
try {
const stack = new Error().stack;
origin = stack.split('\n')[2].trim();
// eslint-disable-next-line no-empty
} catch (e) {}
}
this.writeToFile.push({ args, origin, prefix: "[consoleOnly]" });
} }
debug(...args) { debug(...args) {
@ -134,15 +164,18 @@ export class Logger {
return; return;
} }
let callback = () => {};
if (typeof args[args.length - 1] === 'function') {
callback = args[args.length - 1];
args.splice(args.length - 1, 1);
}
this.events.emit("log", "debug", args); this.events.emit("log", "debug", args);
this.logFunction(...args, null); this.logFunction(args, null);
this.writeToFile.push({ prefix: "[debug]: ", args }, callback);
let origin;
if (this.isDebugOrTrace) {
try {
const stack = new Error().stack;
origin = stack.split('\n')[2].trim();
// eslint-disable-next-line no-empty
} catch (e) {}
}
this.writeToFile.push({ args, origin, prefix: "[debug]" });
} }
trace(...args) { trace(...args) {
@ -150,32 +183,37 @@ export class Logger {
return; return;
} }
let callback = () => {};
if (typeof args[args.length - 1] === 'function') {
callback = args[args.length - 1];
args.splice(args.length - 1, 1);
}
this.events.emit("log", "trace", args); this.events.emit("log", "trace", args);
this.logFunction(...args, null); this.logFunction(args, null);
this.writeToFile.push({ prefix: "[trace]: ", args }, callback);
let origin;
if (this.isDebugOrTrace) {
try {
const stack = new Error().stack;
origin = stack.split('\n')[2].trim();
// eslint-disable-next-line no-empty
} catch (e) {}
}
this.writeToFile.push({ args, origin, prefix: "[trace]" });
} }
dir(...args) { dir(obj) {
const txt = args[0]; if (!obj || !(this.shouldLog('info'))) {
if (!txt || !(this.shouldLog('info'))) {
return; return;
} }
let callback = () => {}; this.events.emit("log", "dir", obj);
if (typeof args[args.length - 1] === 'function') { this.logFunction(obj, null);
callback = args[args.length - 1];
args.splice(args.length - 1, 1);
}
this.events.emit("log", "dir", txt); let origin;
this.logFunction(txt, null); if (this.isDebugOrTrace) {
this.writeToFile({ prefix: "[dir]: ", args }, callback); try {
const stack = new Error().stack;
origin = stack.split('\n')[2].trim();
// eslint-disable-next-line no-empty
} catch (e) {}
}
this.writeToFile({ args: obj, origin, prefix: "[dir]" });
} }
shouldLog(level) { shouldLog(level) {
@ -183,4 +221,3 @@ export class Logger {
return (logLevels.indexOf(level) <= logLevels.indexOf(this.logLevel)); return (logLevels.indexOf(level) <= logLevels.indexOf(this.logLevel));
} }
} }

View File

@ -2,35 +2,49 @@ import assert from 'assert';
import sinon from 'sinon'; import sinon from 'sinon';
import * as fs from 'fs'; import * as fs from 'fs';
import { fakeEmbark } from 'embark-testing'; import { fakeEmbark } from 'embark-testing';
import { Logger } from '../src/';
import tmp from 'tmp';
describe('core/logger', () => { describe('core/logger', () => {
const { embark, plugins } = fakeEmbark(); const { embark, plugins } = fakeEmbark();
let logger, testLogFn, logFile; let logger, testLogFn, resolve;
const logFile = 'embark.log';
const fsMock = { const fsMock = {
logs: {}, logs: {},
appendFile: (file, content, callback) => { appendFile: (file, content, callback) => {
fsMock.logs[file].push(content);
if (resolve) resolve();
callback();
},
ensureFileSync: (file) => {
if (!fsMock.logs[file]) { if (!fsMock.logs[file]) {
fsMock.logs[file] = []; fsMock.logs[file] = [];
} }
fsMock.logs[file].push(content);
callback();
} }
}; };
beforeEach(() => { let Logger;
logFile = tmp.fileSync(); beforeAll(() => {
fsMock[logFile.name] = []; process.env.FORCE_COLOR = '1';
// load the module only after the environment variable is set, otherwise
// the colors package will already have been loaded and in CI the result
// will be that colors are disabled and tests below will fail
({Logger} = require('../src'));
});
afterAll(() => {
delete process.env.FORCE_COLOR;
});
beforeEach(() => {
fsMock.logs[logFile] = [];
resolve = null;
testLogFn = sinon.fake(); testLogFn = sinon.fake();
logger = new Logger({ logger = new Logger({
events: embark.events, events: embark.events,
logFile,
logFunction: testLogFn, logFunction: testLogFn,
logFile: logFile.name,
fs: fsMock fs: fsMock
}); });
}); });
@ -40,37 +54,40 @@ describe('core/logger', () => {
sinon.restore(); sinon.restore();
}); });
test('it should use custom log function for logging', () => { test('it should use custom log function for logging', async () => {
const promise = new Promise(res => { resolve = res; });
logger.info('Hello world'); logger.info('Hello world');
assert(testLogFn.calledOnce); assert(testLogFn.calledOnce);
await promise;
}); });
test('it should inject color encoding based on log method', () => { test('it should inject color encoding based on log method', async () => {
logger.info('Hello world'); logger.info('Hello world');
assert(testLogFn.calledWith('\u001b[32mHello world\u001b[39m')); assert(testLogFn.calledWith('\u001b[32mHello world\u001b[39m'));
logger.warn('Hello world'); logger.warn('Hello world');
assert(testLogFn.calledWith('\u001b[33mHello world\u001b[39m')); assert(testLogFn.calledWith('\u001b[33mHello world\u001b[39m'));
const promise = new Promise(res => { resolve = res; });
logger.error('Hello world'); logger.error('Hello world');
assert(testLogFn.calledWith('\u001b[31mHello world\u001b[39m')); assert(testLogFn.calledWith('\u001b[31mHello world\u001b[39m'));
await promise;
}); });
test('it should write logs to log file', (done) => { test('it should write logs to log file', async () => {
const stats = fs.statSync(logFile.name); const promise = new Promise(res => { resolve = res; });
logger.info('Some test log', () => { logger.info('Some test log');
fsMock.logs[logFile.name].forEach(entry => { await promise;
if (entry.indexOf('[info]: Some test log') > -1) { assert(fsMock.logs[logFile].some(
assert.ok(true); entry => entry.includes('[info] Some test log')
} ));
});
done();
});
}); });
test('it should not log if log method level is higher than configured log level', () => { test('it should not log if log method level is higher than configured log level', async () => {
logger.trace('Hello world'); logger.trace('Hello world');
// default log level is `info` which is lower than `trace` // default log level is `info` which is lower than `trace`
assert.ok(!testLogFn.calledOnce); assert.ok(!testLogFn.calledOnce);
const promise = new Promise(res => { resolve = res; });
logger.warn('Test'); logger.warn('Test');
assert.ok(testLogFn.calledOnce); assert.ok(testLogFn.calledOnce);
await promise;
}); });
}); });

View File

@ -22286,7 +22286,7 @@ tmp@0.0.33, tmp@^0.0.33:
dependencies: dependencies:
os-tmpdir "~1.0.2" os-tmpdir "~1.0.2"
tmp@0.1.0, tmp@^0.1.0: tmp@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==