Skip to content

Commit 23c1284

Browse files
committed
Merge pull request #243 from ParsePlatform/peterjs.logs
Logs support.
2 parents 9dfc013 + dc4859f commit 23c1284

File tree

7 files changed

+449
-2
lines changed

7 files changed

+449
-2
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"node-gcm": "^0.14.0",
2424
"parse": "^1.7.0",
2525
"randomstring": "^1.1.3",
26-
"request": "^2.65.0"
26+
"request": "^2.65.0",
27+
"winston": "^2.1.1"
2728
},
2829
"devDependencies": {
2930
"babel-cli": "^6.5.1",

spec/FileLoggerAdapter.spec.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
2+
var Parse = require('parse/node').Parse;
3+
var request = require('request');
4+
var fs = require('fs');
5+
6+
var LOGS_FOLDER = './test_logs/';
7+
8+
var deleteFolderRecursive = function(path) {
9+
if( fs.existsSync(path) ) {
10+
fs.readdirSync(path).forEach(function(file,index){
11+
var curPath = path + "/" + file;
12+
if(fs.lstatSync(curPath).isDirectory()) { // recurse
13+
deleteFolderRecursive(curPath);
14+
} else { // delete file
15+
fs.unlinkSync(curPath);
16+
}
17+
});
18+
fs.rmdirSync(path);
19+
}
20+
};
21+
22+
describe('info logs', () => {
23+
24+
afterEach((done) => {
25+
deleteFolderRecursive(LOGS_FOLDER);
26+
done();
27+
});
28+
29+
it("Verify INFO logs", (done) => {
30+
var fileLoggerAdapter = new FileLoggerAdapter({
31+
logsFolder: LOGS_FOLDER
32+
});
33+
fileLoggerAdapter.info('testing info logs', () => {
34+
fileLoggerAdapter.query({
35+
size: 1,
36+
level: 'info'
37+
}, (results) => {
38+
expect(results[0].message).toEqual('testing info logs');
39+
done();
40+
});
41+
});
42+
});
43+
});
44+
45+
describe('error logs', () => {
46+
47+
afterEach((done) => {
48+
deleteFolderRecursive(LOGS_FOLDER);
49+
done();
50+
});
51+
52+
it("Verify ERROR logs", (done) => {
53+
var fileLoggerAdapter = new FileLoggerAdapter();
54+
fileLoggerAdapter.error('testing error logs', () => {
55+
fileLoggerAdapter.query({
56+
size: 1,
57+
level: 'error'
58+
}, (results) => {
59+
expect(results[0].message).toEqual('testing error logs');
60+
done();
61+
});
62+
});
63+
});
64+
});

spec/LoggerController.spec.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
2+
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
3+
4+
describe('LoggerController', () => {
5+
it('can check valid master key of request', (done) => {
6+
// Make mock request
7+
var request = {
8+
auth: {
9+
isMaster: true
10+
},
11+
query: {}
12+
};
13+
14+
var loggerController = new LoggerController(new FileLoggerAdapter());
15+
16+
expect(() => {
17+
loggerController.handleGET(request);
18+
}).not.toThrow();
19+
done();
20+
});
21+
22+
it('can check invalid construction of controller', (done) => {
23+
// Make mock request
24+
var request = {
25+
auth: {
26+
isMaster: true
27+
},
28+
query: {}
29+
};
30+
31+
var loggerController = new LoggerController();
32+
33+
expect(() => {
34+
loggerController.handleGET(request);
35+
}).toThrow();
36+
done();
37+
});
38+
39+
it('can check invalid master key of request', (done) => {
40+
// Make mock request
41+
var request = {
42+
auth: {
43+
isMaster: false
44+
},
45+
query: {}
46+
};
47+
48+
var loggerController = new LoggerController(new FileLoggerAdapter());
49+
50+
expect(() => {
51+
loggerController.handleGET(request);
52+
}).toThrow();
53+
done();
54+
});
55+
});
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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;

src/Adapters/Logger/LoggerAdapter.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Logger Adapter
2+
//
3+
// Allows you to change the logger mechanism
4+
//
5+
// Adapter classes must implement the following functions:
6+
// * info(obj1 [, obj2, .., objN])
7+
// * error(obj1 [, obj2, .., objN])
8+
// * query(options, callback)
9+
// Default is FileLoggerAdapter.js
10+
11+
export class LoggerAdapter {
12+
info() {}
13+
error() {}
14+
query(options, callback) {}
15+
}
16+
17+
export default LoggerAdapter;

0 commit comments

Comments
 (0)