You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			255 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			255 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			JavaScript
		
	
| // NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
 | |
| /* eslint-disable more/no-then */
 | |
| 
 | |
| const path = require('path');
 | |
| const fs = require('fs');
 | |
| 
 | |
| 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 rimraf = require('rimraf');
 | |
| 
 | |
| const {
 | |
|   app,
 | |
|   ipcMain: ipc,
 | |
| } = electron;
 | |
| const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
 | |
| let logger;
 | |
| 
 | |
| 
 | |
| module.exports = {
 | |
|   initialize,
 | |
|   getLogger,
 | |
|   // for tests only:
 | |
|   isLineAfterDate,
 | |
|   eliminateOutOfDateFiles,
 | |
|   eliminateOldEntries,
 | |
|   fetchLog,
 | |
|   fetch,
 | |
| };
 | |
| 
 | |
| function initialize() {
 | |
|   if (logger) {
 | |
|     throw new Error('Already called initialize!');
 | |
|   }
 | |
| 
 | |
|   const basePath = app.getPath('userData');
 | |
|   const logPath = path.join(basePath, 'logs');
 | |
|   mkdirp.sync(logPath);
 | |
| 
 | |
|   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,
 | |
|       }],
 | |
|     });
 | |
| 
 | |
|     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('delete-all-logs', async (event) => {
 | |
|       try {
 | |
|         await deleteAllLogs(logPath);
 | |
|       } catch (error) {
 | |
|         logger.error(`Problem deleting all logs: ${error.stack}`);
 | |
|       }
 | |
| 
 | |
|       event.sender.send('delete-all-logs-complete');
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| async function deleteAllLogs(logPath) {
 | |
|   return new Promise((resolve, reject) => {
 | |
|     rimraf(logPath, {
 | |
|       disableGlob: true,
 | |
|     }, (error) => {
 | |
|       if (error) {
 | |
|         return reject(error);
 | |
|       }
 | |
| 
 | |
|       return resolve();
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| 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!');
 | |
|   }
 | |
| 
 | |
|   return logger;
 | |
| }
 | |
| 
 | |
| function fetchLog(logFile) {
 | |
|   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((line) => {
 | |
|         try {
 | |
|           return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
 | |
|         } catch (e) {
 | |
|           return null;
 | |
|         }
 | |
|       }));
 | |
| 
 | |
|       return resolve(data);
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| function fetch(logPath) {
 | |
|   const files = fs.readdirSync(logPath);
 | |
|   const paths = files.map(file => path.join(logPath, file));
 | |
| 
 | |
|   // creating a manual log entry for the final log result
 | |
|   const now = new Date();
 | |
|   const fileListEntry = {
 | |
|     level: 30, // INFO
 | |
|     time: now.toJSON(),
 | |
|     msg: `Loaded this list of log files from logPath: ${files.join(', ')}`,
 | |
|   };
 | |
| 
 | |
|   return Promise.all(paths.map(fetchLog)).then((results) => {
 | |
|     const data = _.flatten(results);
 | |
| 
 | |
|     data.push(fileListEntry);
 | |
| 
 | |
|     return _.sortBy(data, 'time');
 | |
|   });
 | |
| }
 | |
| 
 | |
| 
 | |
| function logAtLevel(level, ...args) {
 | |
|   if (logger) {
 | |
|     // To avoid [Object object] in our log since console.log handles non-strings smoothly
 | |
|     const str = args.map((item) => {
 | |
|       if (typeof item !== 'string') {
 | |
|         try {
 | |
|           return JSON.stringify(item);
 | |
|         } catch (e) {
 | |
|           return item;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return item;
 | |
|     });
 | |
|     logger[level](str.join(' '));
 | |
|   } else {
 | |
|     console._log(...args);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // 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');
 | |
| }
 |