signal-desktop/app/logging.js
Scott Nonnenberg ffbcb4ecb5 Load debug log dialog immediately, then populate log data (#1540)
An immediate response to the user request to see the log, and then we
show the real data as soon as we've loaded it from disk.

Changes:
  - the IPC exchange to get the log data is now async
  - the API to fetch the log on the client side now returns a Promise
  - in the main process, the only disk access done synchronoously is
    reading the contents of the log directory. The JSON parsing of the
    resultant log data is now split up into three chunks.
  - We only send three keys from each log item to the renderer process:
    msg, time, level. Previously we sent the entire log entry with extra
    keys: hostname, pid, name.

FREEBIE
2017-10-04 14:40:35 -07:00

139 lines
3 KiB
JavaScript

const path = require('path');
const fs = require('fs');
const electron = require('electron')
const bunyan = require('bunyan');
const mkdirp = require('mkdirp');
const _ = require('lodash');
const app = electron.app;
const ipc = electron.ipcMain;
const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
let logger;
function dropFirst(args) {
return Array.prototype.slice.call(args, 1);
}
function initialize() {
if (logger) {
throw new Error('Already called initialize!');
}
const basePath = app.getPath('userData');
const logPath = path.join(basePath, 'logs');
mkdirp.sync(logPath);
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
}]
});
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);
});
});
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 getLogger() {
if (!logger) {
throw new Error('Logger hasn\'t been initialized yet!');
}
return logger;
}
function fetchLog(logFile) {
return new Promise(function(resolve, reject) {
fs.readFile(logFile, { encoding: 'utf8' }, function(err, text) {
if (err) {
return reject(err);
}
const lines = _.compact(text.split('\n'));
const data = _.compact(lines.map(function(line) {
try {
return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
}
catch (e) {}
}));
return resolve(data);
});
});
}
function fetch(logPath) {
const files = fs.readdirSync(logPath);
const paths = files.map(function(file) {
return path.join(logPath, file)
});
return Promise.all(paths.map(fetchLog)).then(function(results) {
const data = _.flatten(results);
return _.sortBy(data, 'time');
});
}
function logAtLevel() {
const level = arguments[0];
const args = Array.prototype.slice.call(arguments, 1);
if (logger) {
// To avoid [Object object] in our log since console.log handles non-strings smoothly
const str = args.map(function(item) {
if (typeof item !== 'string') {
try {
return JSON.stringify(item);
}
catch (e) {
return item;
}
}
return item;
});
logger[level](str.join(' '));
} else {
console._log.apply(console, consoleArgs);
}
}
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,
};