|
| 1 | +// Logger |
| 2 | +// |
| 3 | +// Wrapper around Winston logging library with custom query |
| 4 | +// |
| 5 | +// expected log entry to be in the shape of: |
| 6 | +// {"level":"info","message":"{ '0': 'Your Message' }","timestamp":"2016-02-04T05:59:27.412Z"} |
| 7 | +// |
| 8 | +import { LoggerAdapter } from './LoggerAdapter'; |
| 9 | +import winston from 'winston'; |
| 10 | +import fs from 'fs'; |
| 11 | +import { Parse } from 'parse/node'; |
| 12 | + |
| 13 | +const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; |
| 14 | +const CACHE_TIME = 1000 * 60; |
| 15 | + |
| 16 | +let LOGS_FOLDER = './logs/'; |
| 17 | + |
| 18 | +if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { |
| 19 | + LOGS_FOLDER = './test_logs/' |
| 20 | +} |
| 21 | + |
| 22 | +let currentDate = new Date(); |
| 23 | + |
| 24 | +let simpleCache = { |
| 25 | + timestamp: null, |
| 26 | + from: null, |
| 27 | + until: null, |
| 28 | + order: null, |
| 29 | + data: [], |
| 30 | + level: 'info', |
| 31 | +}; |
| 32 | + |
| 33 | +// returns Date object rounded to nearest day |
| 34 | +let _getNearestDay = (date) => { |
| 35 | + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); |
| 36 | +} |
| 37 | + |
| 38 | +// returns Date object of previous day |
| 39 | +let _getPrevDay = (date) => { |
| 40 | + return new Date(date - MILLISECONDS_IN_A_DAY); |
| 41 | +} |
| 42 | + |
| 43 | +// returns the iso formatted file name |
| 44 | +let _getFileName = () => { |
| 45 | + return _getNearestDay(currentDate).toISOString() |
| 46 | +} |
| 47 | + |
| 48 | +// check for valid cache when both from and util match. |
| 49 | +// cache valid for up to 1 minute |
| 50 | +let _hasValidCache = (from, until, level) => { |
| 51 | + if (String(from) === String(simpleCache.from) && |
| 52 | + String(until) === String(simpleCache.until) && |
| 53 | + new Date() - simpleCache.timestamp < CACHE_TIME && |
| 54 | + level === simpleCache.level) { |
| 55 | + return true; |
| 56 | + } |
| 57 | + return false; |
| 58 | +} |
| 59 | + |
| 60 | +// renews transports to current date |
| 61 | +let _renewTransports = ({infoLogger, errorLogger, logsFolder}) => { |
| 62 | + if (infoLogger) { |
| 63 | + infoLogger.add(winston.transports.File, { |
| 64 | + filename: logsFolder + _getFileName() + '.info', |
| 65 | + name: 'info-file', |
| 66 | + level: 'info' |
| 67 | + }); |
| 68 | + } |
| 69 | + if (errorLogger) { |
| 70 | + errorLogger.add(winston.transports.File, { |
| 71 | + filename: logsFolder + _getFileName() + '.error', |
| 72 | + name: 'error-file', |
| 73 | + level: 'error' |
| 74 | + }); |
| 75 | + } |
| 76 | +}; |
| 77 | + |
| 78 | +// check that log entry has valid time stamp based on query |
| 79 | +let _isValidLogEntry = (from, until, entry) => { |
| 80 | + var _entry = JSON.parse(entry), |
| 81 | + timestamp = new Date(_entry.timestamp); |
| 82 | + return timestamp >= from && timestamp <= until |
| 83 | + ? true |
| 84 | + : false |
| 85 | +}; |
| 86 | + |
| 87 | +// ensure that file name is up to date |
| 88 | +let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => { |
| 89 | + if (_getNearestDay(currentDate) !== _getNearestDay(new Date())) { |
| 90 | + currentDate = new Date(); |
| 91 | + if (infoLogger) { |
| 92 | + infoLogger.remove('info-file'); |
| 93 | + } |
| 94 | + if (errorLogger) { |
| 95 | + errorLogger.remove('error-file'); |
| 96 | + } |
| 97 | + _renewTransports({infoLogger, errorLogger, logsFolder}); |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +export class FileLoggerAdapter extends LoggerAdapter { |
| 102 | + constructor(options = {}) { |
| 103 | + super(); |
| 104 | + |
| 105 | + this._logsFolder = options.logsFolder || LOGS_FOLDER; |
| 106 | + |
| 107 | + // check logs folder exists |
| 108 | + if (!fs.existsSync(this._logsFolder)) { |
| 109 | + fs.mkdirSync(this._logsFolder); |
| 110 | + } |
| 111 | + |
| 112 | + this._errorLogger = new (winston.Logger)({ |
| 113 | + exitOnError: false, |
| 114 | + transports: [ |
| 115 | + new (winston.transports.File)({ |
| 116 | + filename: this._logsFolder + _getFileName() + '.error', |
| 117 | + name: 'error-file', |
| 118 | + level: 'error' |
| 119 | + }) |
| 120 | + ] |
| 121 | + }); |
| 122 | + |
| 123 | + this._infoLogger = new (winston.Logger)({ |
| 124 | + exitOnError: false, |
| 125 | + transports: [ |
| 126 | + new (winston.transports.File)({ |
| 127 | + filename: this._logsFolder + _getFileName() + '.info', |
| 128 | + name: 'info-file', |
| 129 | + level: 'info' |
| 130 | + }) |
| 131 | + ] |
| 132 | + }); |
| 133 | + } |
| 134 | + |
| 135 | + info() { |
| 136 | + _verifyTransports({infoLogger: this._infoLogger, logsFolder: this._logsFolder}); |
| 137 | + return this._infoLogger.info.apply(undefined, arguments); |
| 138 | + } |
| 139 | + |
| 140 | + error() { |
| 141 | + _verifyTransports({errorLogger: this._errorLogger, logsFolder: this._logsFolder}); |
| 142 | + return this._errorLogger.error.apply(undefined, arguments); |
| 143 | + } |
| 144 | + |
| 145 | + // custom query as winston is currently limited |
| 146 | + query(options, callback) { |
| 147 | + if (!options) { |
| 148 | + options = {}; |
| 149 | + } |
| 150 | + // defaults to 7 days prior |
| 151 | + let from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY)); |
| 152 | + let until = options.until || new Date(); |
| 153 | + let size = options.size || 10; |
| 154 | + let order = options.order || 'desc'; |
| 155 | + let level = options.level || 'info'; |
| 156 | + let roundedUntil = _getNearestDay(until); |
| 157 | + let roundedFrom = _getNearestDay(from); |
| 158 | + |
| 159 | + if (_hasValidCache(roundedFrom, roundedUntil, level)) { |
| 160 | + let logs = []; |
| 161 | + if (order !== simpleCache.order) { |
| 162 | + // reverse order of data |
| 163 | + simpleCache.data.forEach((entry) => { |
| 164 | + logs.unshift(entry); |
| 165 | + }); |
| 166 | + } else { |
| 167 | + logs = simpleCache.data; |
| 168 | + } |
| 169 | + callback(logs.slice(0, size)); |
| 170 | + return; |
| 171 | + } |
| 172 | + |
| 173 | + let curDate = roundedUntil; |
| 174 | + let curSize = 0; |
| 175 | + let method = order === 'desc' ? 'push' : 'unshift'; |
| 176 | + let files = []; |
| 177 | + let promises = []; |
| 178 | + |
| 179 | + // current a batch call, all files with valid dates are read |
| 180 | + while (curDate >= from) { |
| 181 | + files[method](this._logsFolder + curDate.toISOString() + '.' + level); |
| 182 | + curDate = _getPrevDay(curDate); |
| 183 | + } |
| 184 | + |
| 185 | + // read each file and split based on newline char. |
| 186 | + // limitation is message cannot contain newline |
| 187 | + // TODO: strip out delimiter from logged message |
| 188 | + files.forEach(function(file, i) { |
| 189 | + let promise = new Parse.Promise(); |
| 190 | + fs.readFile(file, 'utf8', function(err, data) { |
| 191 | + if (err) { |
| 192 | + promise.resolve([]); |
| 193 | + } else { |
| 194 | + let results = data.split('\n').filter((value) => { |
| 195 | + return value.trim() !== ''; |
| 196 | + }); |
| 197 | + promise.resolve(results); |
| 198 | + } |
| 199 | + }); |
| 200 | + promises[method](promise); |
| 201 | + }); |
| 202 | + |
| 203 | + Parse.Promise.when(promises).then((results) => { |
| 204 | + let logs = []; |
| 205 | + results.forEach(function(logEntries, i) { |
| 206 | + logEntries.forEach(function(entry) { |
| 207 | + if (_isValidLogEntry(from, until, entry)) { |
| 208 | + logs[method](JSON.parse(entry)); |
| 209 | + } |
| 210 | + }); |
| 211 | + }); |
| 212 | + simpleCache = { |
| 213 | + timestamp: new Date(), |
| 214 | + from: roundedFrom, |
| 215 | + until: roundedUntil, |
| 216 | + data: logs, |
| 217 | + order, |
| 218 | + level, |
| 219 | + }; |
| 220 | + callback(logs.slice(0, size)); |
| 221 | + }); |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +export default FileLoggerAdapter; |
0 commit comments