Clean logs on start - and eslint/mocha with code coverage (#1945)

* Clean logs on startup; install server-side testing/linting

* Add eslint config, make all of app/ conform to its demands

* Add Node.js testing and linting to CI

* Lock project to Node.js 7.9.0, used by Electron 1.7.10

* New eslint error: trailing commas in function argumensts

Node 7.9.0 doesn't like trailing commas, but Electron does

* Move electron to devDependency, tell eslint it's built-in
This commit is contained in:
Scott Nonnenberg 2018-01-08 13:19:25 -08:00 committed by GitHub
parent 6464d0a5fa
commit 64fe9dbfb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1782 additions and 316 deletions

View file

@ -1,22 +1,31 @@
const path = require('path');
const fs = require('fs');
const electron = require('electron')
const electron = require('electron');
const bunyan = require('bunyan');
const mkdirp = require('mkdirp');
const _ = require('lodash');
const readFirstLine = require('firstline');
const readLastLines = require('read-last-lines').read;
const app = electron.app;
const ipc = electron.ipcMain;
const {
app,
ipcMain: ipc,
} = electron;
const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
let logger;
function dropFirst(args) {
return Array.prototype.slice.call(args, 1);
}
module.exports = {
initialize,
getLogger,
// for tests only:
isLineAfterDate,
eliminateOutOfDateFiles,
eliminateOldEntries,
fetchLog,
fetch,
};
function initialize() {
if (logger) {
@ -27,38 +36,114 @@ function initialize() {
const logPath = path.join(basePath, 'logs');
mkdirp.sync(logPath);
const logFile = path.join(logPath, 'log.log');
return cleanupLogs(logPath).then(() => {
const logFile = path.join(logPath, 'log.log');
logger = bunyan.createLogger({
name: 'log',
streams: [{
level: 'debug',
stream: process.stdout
}, {
type: 'rotating-file',
path: logFile,
period: '1d',
count: 3
}]
});
logger = bunyan.createLogger({
name: 'log',
streams: [{
level: 'debug',
stream: process.stdout,
}, {
type: 'rotating-file',
path: logFile,
period: '1d',
count: 3,
}],
});
LEVELS.forEach(function(level) {
ipc.on('log-' + level, function() {
// first parameter is the event, rest are provided arguments
var args = dropFirst(arguments);
logger[level].apply(logger, args);
LEVELS.forEach((level) => {
ipc.on(`log-${level}`, (first, ...rest) => {
logger[level](...rest);
});
});
ipc.on('fetch-log', (event) => {
fetch(logPath).then((data) => {
event.sender.send('fetched-log', data);
}, (error) => {
logger.error(`Problem loading log from disk: ${error.stack}`);
});
});
});
}
ipc.on('fetch-log', function(event) {
fetch(logPath).then(function(data) {
event.sender.send('fetched-log', data);
}, function(error) {
logger.error('Problem loading log from disk: ' + error.stack);
});
function cleanupLogs(logPath) {
const now = new Date();
const earliestDate = new Date(Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate() - 3
));
return eliminateOutOfDateFiles(logPath, earliestDate).then((remaining) => {
const files = _.filter(remaining, file => !file.start && file.end);
if (!files.length) {
return null;
}
return eliminateOldEntries(files, earliestDate);
});
}
function isLineAfterDate(line, date) {
if (!line) {
return false;
}
try {
const data = JSON.parse(line);
return (new Date(data.time)).getTime() > date.getTime();
} catch (e) {
console.log('error parsing log line', e.stack, line);
return false;
}
}
function eliminateOutOfDateFiles(logPath, date) {
const files = fs.readdirSync(logPath);
const paths = files.map(file => path.join(logPath, file));
return Promise.all(_.map(
paths,
target => Promise.all([
readFirstLine(target),
readLastLines(target, 2),
]).then((results) => {
const start = results[0];
const end = results[1].split('\n');
const file = {
path: target,
start: isLineAfterDate(start, date),
end: isLineAfterDate(end[end.length - 1], date)
|| isLineAfterDate(end[end.length - 2], date),
};
if (!file.start && !file.end) {
fs.unlinkSync(file.path);
}
return file;
})
));
}
function eliminateOldEntries(files, date) {
const earliest = date.getTime();
return Promise.all(_.map(
files,
file => fetchLog(file.path).then((lines) => {
const recent = _.filter(lines, line => (new Date(line.time)).getTime() >= earliest);
const text = _.map(recent, line => JSON.stringify(line)).join('\n');
return fs.writeFileSync(file.path, `${text}\n`);
})
));
}
function getLogger() {
if (!logger) {
throw new Error('Logger hasn\'t been initialized yet!');
@ -68,18 +153,19 @@ function getLogger() {
}
function fetchLog(logFile) {
return new Promise(function(resolve, reject) {
fs.readFile(logFile, { encoding: 'utf8' }, function(err, text) {
return new Promise((resolve, reject) => {
fs.readFile(logFile, { encoding: 'utf8' }, (err, text) => {
if (err) {
return reject(err);
}
const lines = _.compact(text.split('\n'));
const data = _.compact(lines.map(function(line) {
const data = _.compact(lines.map((line) => {
try {
return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
} catch (e) {
return null;
}
catch (e) {}
}));
return resolve(data);
@ -89,19 +175,17 @@ function fetchLog(logFile) {
function fetch(logPath) {
const files = fs.readdirSync(logPath);
const paths = files.map(function(file) {
return path.join(logPath, file)
});
const paths = files.map(file => path.join(logPath, file));
// creating a manual log entry for the final log result
var now = new Date();
const now = new Date();
const fileListEntry = {
level: 30, // INFO
time: now.toJSON(),
msg: 'Loaded this list of log files from logPath: ' + files.join(', '),
msg: `Loaded this list of log files from logPath: ${files.join(', ')}`,
};
return Promise.all(paths.map(fetchLog)).then(function(results) {
return Promise.all(paths.map(fetchLog)).then((results) => {
const data = _.flatten(results);
data.push(fileListEntry);
@ -111,18 +195,14 @@ function fetch(logPath) {
}
function logAtLevel() {
const level = arguments[0];
const args = Array.prototype.slice.call(arguments, 1);
function logAtLevel(level, ...args) {
if (logger) {
// To avoid [Object object] in our log since console.log handles non-strings smoothly
const str = args.map(function(item) {
const str = args.map((item) => {
if (typeof item !== 'string') {
try {
return JSON.stringify(item);
}
catch (e) {
} catch (e) {
return item;
}
}
@ -131,20 +211,16 @@ function logAtLevel() {
});
logger[level](str.join(' '));
} else {
console._log.apply(console, consoleArgs);
console._log(...args);
}
}
console._log = console.log;
console.log = _.partial(logAtLevel, 'info');
console._error = console.error;
console.error = _.partial(logAtLevel, 'error');
console._warn = console.warn;
console.warn = _.partial(logAtLevel, 'warn');
module.exports = {
initialize,
getLogger,
};
// This blows up using mocha --watch, so we ensure it is run just once
if (!console._log) {
console._log = console.log;
console.log = _.partial(logAtLevel, 'info');
console._error = console.error;
console.error = _.partial(logAtLevel, 'error');
console._warn = console.warn;
console.warn = _.partial(logAtLevel, 'warn');
}