Skip to content

Commit b530feb

Browse files
spenthilRafael Santos
authored and
Rafael Santos
committed
Log objects rather than JSON strings and option for single line logs (parse-community#2028)
* Log objects rather than JSON strings and option for single line logs This reverts commit fcd914b. * Better password stripping tests
1 parent 490962f commit b530feb

8 files changed

+112
-51
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@ app.listen(1337, function() {
145145

146146
For a full list of available options, run `parse-server --help`.
147147

148+
## Logging
149+
150+
Parse Server will, by default, will log:
151+
* to the console
152+
* daily rotating files as new line delimited JSON
153+
154+
Logs are also be viewable in Parse Dashboard.
155+
156+
**Want to log each request and response?** Set the `VERBOSE` environment variable when starting `parse-server`. Usage :- `VERBOSE='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
157+
158+
**Want logs to be in placed in other folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
159+
160+
**Want new line delimited JSON error logs (for consumption by CloudWatch, Google Cloud Logging, etc.)?** Pass the `JSON_LOGS` environment variable when starting `parse-server`. Usage :- `JSON_LOGS='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
161+
148162
# Documentation
149163

150164
The full documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki). The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.

spec/FileLoggerAdapter.spec.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ describe('verbose logs', () => {
5959
level: 'verbose'
6060
});
6161
}).then((results) => {
62-
expect(results[1].message.includes('"password": "********"')).toEqual(true);
62+
let logString = JSON.stringify(results);
63+
expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0);
64+
expect(logString.match(/moon-y/g)).toBe(null);
65+
6366
var headers = {
6467
'X-Parse-Application-Id': 'test',
6568
'X-Parse-REST-API-Key': 'rest'
@@ -74,11 +77,16 @@ describe('verbose logs', () => {
7477
size: 100,
7578
level: 'verbose'
7679
}).then((results) => {
77-
expect(results[1].message.includes('password=********')).toEqual(true);
80+
let logString = JSON.stringify(results);
81+
expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0);
82+
expect(logString.match(/moon-y/g)).toBe(null);
7883
done();
7984
});
8085
});
81-
});
86+
}).catch((err) => {
87+
fail(JSON.stringify(err));
88+
done();
89+
})
8290
});
8391

8492
it("should not mask information in non _User class", (done) => {
@@ -92,7 +100,7 @@ describe('verbose logs', () => {
92100
level: 'verbose'
93101
});
94102
}).then((results) => {
95-
expect(results[1].message.includes('"password": "pw"')).toEqual(true);
103+
expect(results[1].body.password).toEqual("pw");
96104
done();
97105
});
98106
});

src/Config.js

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class Config {
2222
}
2323

2424
this.applicationId = applicationId;
25+
this.jsonLogs = cacheInfo.jsonLogs;
2526
this.masterKey = cacheInfo.masterKey;
2627
this.clientKey = cacheInfo.clientKey;
2728
this.javascriptKey = cacheInfo.javascriptKey;

src/ParseServer.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Defau
6969
// and delete
7070
// "loggerAdapter": a class like FileLoggerAdapter providing info, error,
7171
// and query
72+
// "jsonLogs": log as structured JSON objects
7273
// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us
7374
// what database this Parse API connects to.
7475
// "cloud": relative location to cloud code to require, or a function
@@ -98,6 +99,7 @@ class ParseServer {
9899
filesAdapter,
99100
push,
100101
loggerAdapter,
102+
jsonLogs,
101103
logsFolder,
102104
databaseURI,
103105
databaseOptions,
@@ -155,9 +157,7 @@ class ParseServer {
155157
}
156158

157159
if (logsFolder) {
158-
configureLogger({
159-
logsFolder
160-
})
160+
configureLogger({logsFolder, jsonLogs});
161161
}
162162

163163
if (cloud) {
@@ -172,7 +172,7 @@ class ParseServer {
172172
}
173173

174174
if (verbose || process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER) {
175-
configureLogger({level: 'silly'});
175+
configureLogger({level: 'silly', jsonLogs});
176176
}
177177

178178
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
@@ -215,6 +215,7 @@ class ParseServer {
215215
})
216216

217217
AppCache.put(appId, {
218+
appId,
218219
masterKey: masterKey,
219220
serverURL: serverURL,
220221
collectionPrefix: collectionPrefix,
@@ -242,6 +243,7 @@ class ParseServer {
242243
liveQueryController: liveQueryController,
243244
sessionLength: Number(sessionLength),
244245
expireInactiveSessions: expireInactiveSessions,
246+
jsonLogs,
245247
revokeSessionOnPasswordReset,
246248
databaseController,
247249
});
@@ -265,7 +267,7 @@ class ParseServer {
265267
return ParseServer.app(this.config);
266268
}
267269

268-
static app({maxUploadSize = '20mb'}) {
270+
static app({maxUploadSize = '20mb', appId}) {
269271
// This app serves the Parse API directly.
270272
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
271273
var api = express();
@@ -312,7 +314,7 @@ class ParseServer {
312314
return memo.concat(router.routes);
313315
}, []);
314316

315-
let appRouter = new PromiseRouter(routes);
317+
let appRouter = new PromiseRouter(routes, appId);
316318

317319
batch.mountOnto(appRouter);
318320

src/PromiseRouter.js

+34-18
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
// themselves use our routing information, without disturbing express
66
// components that external developers may be modifying.
77

8-
import express from 'express';
9-
import url from 'url';
10-
import log from './logger';
8+
import AppCache from './cache';
9+
import express from 'express';
10+
import url from 'url';
11+
import log from './logger';
12+
import {inspect} from 'util';
1113

1214
export default class PromiseRouter {
1315
// Each entry should be an object with:
@@ -19,8 +21,9 @@ export default class PromiseRouter {
1921
// status: optional. the http status code. defaults to 200
2022
// response: a json object with the content of the response
2123
// location: optional. a location header
22-
constructor(routes = []) {
24+
constructor(routes = [], appId) {
2325
this.routes = routes;
26+
this.appId = appId;
2427
this.mountRoutes();
2528
}
2629

@@ -107,16 +110,16 @@ export default class PromiseRouter {
107110
for (var route of this.routes) {
108111
switch(route.method) {
109112
case 'POST':
110-
expressApp.post(route.path, makeExpressHandler(route.handler));
113+
expressApp.post(route.path, makeExpressHandler(this.appId, route.handler));
111114
break;
112115
case 'GET':
113-
expressApp.get(route.path, makeExpressHandler(route.handler));
116+
expressApp.get(route.path, makeExpressHandler(this.appId, route.handler));
114117
break;
115118
case 'PUT':
116-
expressApp.put(route.path, makeExpressHandler(route.handler));
119+
expressApp.put(route.path, makeExpressHandler(this.appId, route.handler));
117120
break;
118121
case 'DELETE':
119-
expressApp.delete(route.path, makeExpressHandler(route.handler));
122+
expressApp.delete(route.path, makeExpressHandler(this.appId, route.handler));
120123
break;
121124
default:
122125
throw 'unexpected code branch';
@@ -129,16 +132,16 @@ export default class PromiseRouter {
129132
for (var route of this.routes) {
130133
switch(route.method) {
131134
case 'POST':
132-
expressApp.post(route.path, makeExpressHandler(route.handler));
135+
expressApp.post(route.path, makeExpressHandler(this.appId, route.handler));
133136
break;
134137
case 'GET':
135-
expressApp.get(route.path, makeExpressHandler(route.handler));
138+
expressApp.get(route.path, makeExpressHandler(this.appId, route.handler));
136139
break;
137140
case 'PUT':
138-
expressApp.put(route.path, makeExpressHandler(route.handler));
141+
expressApp.put(route.path, makeExpressHandler(this.appId, route.handler));
139142
break;
140143
case 'DELETE':
141-
expressApp.delete(route.path, makeExpressHandler(route.handler));
144+
expressApp.delete(route.path, makeExpressHandler(this.appId, route.handler));
142145
break;
143146
default:
144147
throw 'unexpected code branch';
@@ -152,17 +155,30 @@ export default class PromiseRouter {
152155
// handler.
153156
// Express handlers should never throw; if a promise handler throws we
154157
// just treat it like it resolved to an error.
155-
function makeExpressHandler(promiseHandler) {
158+
function makeExpressHandler(appId, promiseHandler) {
159+
let config = AppCache.get(appId);
156160
return function(req, res, next) {
157161
try {
158-
log.verbose(req.method, maskSensitiveUrl(req), req.headers,
159-
JSON.stringify(maskSensitiveBody(req), null, 2));
162+
let url = maskSensitiveUrl(req);
163+
let body = maskSensitiveBody(req);
164+
let stringifiedBody = JSON.stringify(body, null, 2);
165+
log.verbose(`REQUEST for [${req.method}] ${url}: ${stringifiedBody}`, {
166+
method: req.method,
167+
url: url,
168+
headers: req.headers,
169+
body: body
170+
});
160171
promiseHandler(req).then((result) => {
161172
if (!result.response && !result.location && !result.text) {
162173
log.error('the handler did not include a "response" or a "location" field');
163174
throw 'control should not get here';
164175
}
165-
log.verbose(JSON.stringify(result, null, 2));
176+
177+
let stringifiedResponse = JSON.stringify(result, null, 2);
178+
log.verbose(
179+
`RESPONSE from [${req.method}] ${url}: ${stringifiedResponse}`,
180+
{result: result}
181+
);
166182

167183
var status = result.status || 200;
168184
res.status(status);
@@ -186,11 +202,11 @@ function makeExpressHandler(promiseHandler) {
186202
}
187203
res.json(result.response);
188204
}, (e) => {
189-
log.verbose('error:', e);
205+
log.error(`Error generating response. ${inspect(e)}`, {error: e});
190206
next(e);
191207
});
192208
} catch (e) {
193-
log.verbose('exception:', e);
209+
log.error(`Error handling request: ${inspect(e)}`, {error: e});
194210
next(e);
195211
}
196212
}

src/cli/cli-definitions.js

+4
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ export default {
184184
env: "VERBOSE",
185185
help: "Set the logging to verbose"
186186
},
187+
"jsonLogs": {
188+
env: "JSON_LOGS",
189+
help: "Log as structured JSON objects"
190+
},
187191
"revokeSessionOnPasswordReset": {
188192
env: "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET",
189193
help: "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.",

src/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ParseServer from './ParseServer';
2+
import logger from './logger';
23
import S3Adapter from 'parse-server-s3-adapter'
34
import FileSystemAdapter from 'parse-server-fs-adapter'
45
import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter'
@@ -16,4 +17,4 @@ _ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer;
1617
let GCSAdapter = useExternal('GCSAdapter', 'parse-server-gcs-adapter');
1718

1819
export default ParseServer;
19-
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, TestUtils, _ParseServer as ParseServer };
20+
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, TestUtils, logger, _ParseServer as ParseServer };

src/logger.js

+37-22
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,45 @@ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
1010
}
1111

1212
LOGS_FOLDER = process.env.PARSE_SERVER_LOGS_FOLDER || LOGS_FOLDER;
13+
const JSON_LOGS = process.env.JSON_LOGS || false;
1314

1415
let currentLogsFolder = LOGS_FOLDER;
1516

16-
function generateTransports(level) {
17+
function generateTransports(level, options = {}) {
1718
let transports = [
18-
new (DailyRotateFile)({
19-
filename: 'parse-server.info',
20-
dirname: currentLogsFolder,
21-
name: 'parse-server',
22-
level: level
23-
}),
24-
new (DailyRotateFile)({
25-
filename: 'parse-server.err',
26-
dirname: currentLogsFolder,
27-
name: 'parse-server-error',
28-
level: 'error'
29-
})
30-
]
19+
new (DailyRotateFile)(
20+
Object.assign({
21+
filename: 'parse-server.info',
22+
dirname: currentLogsFolder,
23+
name: 'parse-server',
24+
level: level
25+
}, options)
26+
),
27+
new (DailyRotateFile)(
28+
Object.assign({
29+
filename: 'parse-server.err',
30+
dirname: currentLogsFolder,
31+
name: 'parse-server-error',
32+
level: 'error'
33+
}
34+
), options)
35+
];
3136
if (!process.env.TESTING || process.env.VERBOSE) {
32-
transports = [new (winston.transports.Console)({
33-
colorize: true,
34-
level:level
35-
})].concat(transports);
37+
transports = [
38+
new (winston.transports.Console)(
39+
Object.assign({
40+
colorize: true,
41+
level: level
42+
}, options)
43+
)
44+
].concat(transports);
3645
}
3746
return transports;
3847
}
3948

4049
const logger = new winston.Logger();
4150

42-
export function configureLogger({logsFolder, level = winston.level}) {
51+
export function configureLogger({ logsFolder, jsonLogs, level = winston.level }) {
4352
winston.level = level;
4453
logsFolder = logsFolder || currentLogsFolder;
4554

@@ -53,16 +62,22 @@ export function configureLogger({logsFolder, level = winston.level}) {
5362
}
5463
currentLogsFolder = logsFolder;
5564

65+
const options = {};
66+
if (jsonLogs) {
67+
options.json = true;
68+
options.stringify = true;
69+
}
70+
const transports = generateTransports(level, options);
5671
logger.configure({
57-
transports: generateTransports(level)
72+
transports: transports
5873
})
5974
}
6075

61-
configureLogger({logsFolder: LOGS_FOLDER});
76+
configureLogger({ logsFolder: LOGS_FOLDER, jsonLogs: JSON_LOGS });
6277

6378
export function addGroup(groupName) {
6479
let level = winston.level;
65-
let transports = generateTransports().concat(new (DailyRotateFile)({
80+
let transports = generateTransports().concat(new (DailyRotateFile)({
6681
filename: groupName,
6782
dirname: currentLogsFolder,
6883
name: groupName,

0 commit comments

Comments
 (0)