Skip to content

Commit 5f67cae

Browse files
Arthur Cinaderflovilmart
authored andcommitted
Make parse-server cloud code logging closer parse.com legacy (#2550)
* Make parse-server cloud code logging much to parse.com legacy. (fixes #2501) 1. More closely mimic the wording. Include the user id. 2. Truncate input and result at 1k char. 3. Use more sensible metadata that would makes sense to index. The guideline I used was: if it makes sense to filter on, put it in metadata. If it makes sense to "free text" search on, then put it in the message. - file and console output, logging an object does not do what on might expect. For example, logging a function's "params": ``` expected: info: Ran cloud function aFunction for user qWHLVEsbEe with: Input: {"foo":"bar","bar":"baz"} Result: "it worked!" functionName=aFunction, params= { foo: "bar", "bar": baz }, user=qWHLVEsbEe what you actually get: info: Ran cloud function aFunction for user qWHLVEsbEe with: Input: {"foo":"bar","bar":"baz"} Result: "it worked!" functionName=aFunction, foo=bar, bar=baz, user=qWHLVEsbEe ``` - logging highly variable metadata is pretty useless for indexing when logs are sent to a logging repository like elastic search. In that use case, you want to index stuff you expect to filter on like user, hook type. - finally, putting the same input and result data in both the metadata and the message makes each message much larger with no additional value (that I know of anyway :). 4. Change some of the naming of functions in trigger.js to make future work easier. I was confused about why there were three logging functions in trigger and it took me awhile to get that before hooks and after hooks are logged differently. I just changed the names to make it obvious at first glance. 5. Add some try/catches to help any future futzers see syntax errors, etc instead of just hanging. Some log examples from unit test output: ``` info: Ran cloud function loggerTest for user YUD2os1i5B with: Input: {} Result: {} functionName=loggerTest, user=YUD2os1i5B info: beforeSave triggered for MyObject for user nssehQ3wtz: Input: {} Result: {} className=MyObject, triggerType=beforeSave, user=nssehQ3wtz info: afterSave triggered for MyObject for user XdznQgTD0p: Input: {"createdAt":"2016-08-19T01:11:31.249Z","updatedAt":"2016-08-19T01:11:31.249Z","objectId":"POoOOLL89U"} className=MyObject, triggerType=afterSave, user=XdznQgTD0p error: beforeSave failed for MyObject for user 7JHqCZgnhf: Input: {} Error: {"code":141,"message":"uh oh!"} className=MyObject, triggerType=beforeSave, code=141, message=uh oh!, user=7JHqCZgnhf info: Ran cloud function aFunction for user YR3nOoT3r9 with: Input: {"foo":"bar"} Result: "it worked!" functionName=aFunction, user=YR3nOoT3r9 error: Failed running cloud function aFunction for user Xm6NpOyuMC with: Input: {"foo":"bar"} Error: {"code":141,"message":"it failed!"} functionName=aFunction, code=141, message=it failed!, user=Xm6NpOyuMC info: Ran cloud function aFunction for user CK1lvkmaLg with: Input: {"longString":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lobortis semper diam, ac euismod diam pharetra sed. Etiam eget efficitur neque. Proin nec diam mi. Sed ut purus dolor. Nulla nulla nibh, ornare vitae ornare et, scelerisque rutrum eros. Mauris venenatis tincidunt turpis a mollis. Donec gravida eget enim in luctus.\n\nSed porttitor commodo orci, ut pretium eros convallis eget. Curabitur pretium velit in odio dictum luctus. Vivamus ac tristique arcu, a semper tellus. Morbi euismod purus dapibus vestibulum sagittis. Nunc dapibus vehicula leo at scelerisque. Donec porta mauris quis nulla imperdiet consectetur. Curabitur sagittis eleifend arcu eget elementum. Aenean interdum tincidunt ornare. Pellentesque sit amet interdum tortor. Pellentesque blandit nisl eget euismod consequat. Etiam feugiat felis sit amet porta pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nNulla faucibus sem ipsum, at rhoncus diam pulvinar at. Vivamus consectetur, diam... (truncated) Result: {"longString":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lobortis semper diam, ac euismod diam pharetra sed. Etiam eget efficitur neque. Proin nec diam mi. Sed ut purus dolor. Nulla nulla nibh, ornare vitae ornare et, scelerisque rutrum eros. Mauris venenatis tincidunt turpis a mollis. Donec gravida eget enim in luctus.\n\nSed porttitor commodo orci, ut pretium eros convallis eget. Curabitur pretium velit in odio dictum luctus. Vivamus ac tristique arcu, a semper tellus. Morbi euismod purus dapibus vestibulum sagittis. Nunc dapibus vehicula leo at scelerisque. Donec porta mauris quis nulla imperdiet consectetur. Curabitur sagittis eleifend arcu eget elementum. Aenean interdum tincidunt ornare. Pellentesque sit amet interdum tortor. Pellentesque blandit nisl eget euismod consequat. Etiam feugiat felis sit amet porta pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nNulla faucibus sem ipsum, at rhoncus diam pulvinar at. Vivamus consectetur, diam... (truncated) functionName=aFunction, user=CK1lvkmaLg ``` * Implement PR comments: - add back params to metadata and add back to the test - use screaming snake case for conts * fix typo
1 parent 277df54 commit 5f67cae

File tree

6 files changed

+247
-50
lines changed

6 files changed

+247
-50
lines changed

spec/CloudCode.spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@ describe('Cloud Code', () => {
5959
})
6060
});
6161

62+
it('returns an error', (done) => {
63+
Parse.Cloud.define('cloudCodeWithError', (req, res) => {
64+
foo.bar();
65+
res.success('I better throw an error.');
66+
});
67+
68+
Parse.Cloud.run('cloudCodeWithError')
69+
.then(
70+
a => done.fail('should not succeed'),
71+
e => {
72+
expect(e).toEqual(new Parse.Error(1, undefined));
73+
done();
74+
});
75+
});
76+
6277
it('beforeSave rejection with custom error code', function(done) {
6378
Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function (req, res) {
6479
res.error(999, 'Nope');

spec/CloudCodeLogger.spec.js

Lines changed: 156 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,48 @@
22
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
33
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
44

5+
const fs = require('fs');
6+
const loremFile = __dirname + '/support/lorem.txt';
7+
58
describe("Cloud Code Logger", () => {
6-
it("should expose log to functions", (done) => {
9+
let user;
10+
11+
beforeEach(done => {
12+
Parse.User.enableUnsafeCurrentUser();
13+
return reconfigureServer({
14+
// useful to flip to false for fine tuning :).
15+
silent: true,
16+
}).then(() => {
17+
return Parse.User.signUp('tester', 'abc')
18+
.then(loggedInUser => user = loggedInUser)
19+
.then(() => Parse.User.logIn(user.get('username'), 'abc'))
20+
.then(() => done())
21+
});
22+
});
23+
24+
// Note that helpers takes care of logout.
25+
// see helpers.js:afterEach
26+
27+
it("should expose log to functions", done => {
728
var logController = new LoggerController(new WinstonLoggerAdapter());
829

930
Parse.Cloud.define("loggerTest", (req, res) => {
10-
req.log.info('logTest', 'info log', {info: 'some log' });
11-
req.log.error('logTest','error log', {error: 'there was an error'});
31+
req.log.info('logTest', 'info log', { info: 'some log' });
32+
req.log.error('logTest', 'error log', { error: 'there was an error' });
1233
res.success({});
1334
});
1435

1536
Parse.Cloud.run('loggerTest').then(() => {
16-
return logController.getLogs({from: Date.now() - 500, size: 1000});
37+
return logController.getLogs({ from: Date.now() - 500, size: 1000 });
1738
}).then((res) => {
1839
expect(res.length).not.toBe(0);
19-
let lastLogs = res.slice(0, 3);
40+
let lastLogs = res.slice(0, 3);
2041
let cloudFunctionMessage = lastLogs[0];
2142
let errorMessage = lastLogs[1];
2243
let infoMessage = lastLogs[2];
2344
expect(cloudFunctionMessage.level).toBe('info');
2445
expect(cloudFunctionMessage.params).toEqual({});
25-
expect(cloudFunctionMessage.message).toEqual('Ran cloud function loggerTest with:\nInput: {}\nResult: {}');
46+
expect(cloudFunctionMessage.message).toMatch(/Ran cloud function loggerTest for user [^ ]* with:\n Input: {}\n Result: {}/);
2647
expect(cloudFunctionMessage.functionName).toEqual('loggerTest');
2748
expect(errorMessage.level).toBe('error');
2849
expect(errorMessage.error).toBe('there was an error');
@@ -38,23 +59,24 @@ describe("Cloud Code Logger", () => {
3859
var logController = new LoggerController(new WinstonLoggerAdapter());
3960

4061
Parse.Cloud.beforeSave("MyObject", (req, res) => {
41-
req.log.info('beforeSave MyObject', 'info log', {info: 'some log' });
42-
req.log.error('beforeSave MyObject','error log', {error: 'there was an error'});
62+
req.log.info('beforeSave MyObject', 'info log', { info: 'some log' });
63+
req.log.error('beforeSave MyObject', 'error log', { error: 'there was an error' });
4364
res.success({});
4465
});
4566

4667
let obj = new Parse.Object('MyObject');
4768
obj.save().then(() => {
48-
return logController.getLogs({from: Date.now() - 500, size: 1000})
69+
return logController.getLogs({ from: Date.now() - 500, size: 1000 })
4970
}).then((res) => {
5071
expect(res.length).not.toBe(0);
51-
let lastLogs = res.slice(0, 3);
72+
let lastLogs = res.slice(0, 3);
5273
let cloudTriggerMessage = lastLogs[0];
5374
let errorMessage = lastLogs[1];
5475
let infoMessage = lastLogs[2];
5576
expect(cloudTriggerMessage.level).toBe('info');
56-
expect(cloudTriggerMessage.input).toEqual({});
57-
expect(cloudTriggerMessage.message).toEqual('beforeSave triggered for MyObject\nInput: {}\nResult: {}');
77+
expect(cloudTriggerMessage.triggerType).toEqual('beforeSave');
78+
expect(cloudTriggerMessage.message).toMatch(/beforeSave triggered for MyObject for user [^ ]*\n Input: {}\n Result: {}/);
79+
expect(cloudTriggerMessage.user).toBe(user.id);
5880
expect(errorMessage.level).toBe('error');
5981
expect(errorMessage.error).toBe('there was an error');
6082
expect(errorMessage.message).toBe('beforeSave MyObject error log');
@@ -64,4 +86,126 @@ describe("Cloud Code Logger", () => {
6486
done();
6587
});
6688
});
89+
90+
it('should truncate really long lines when asked to', () => {
91+
const logController = new LoggerController(new WinstonLoggerAdapter());
92+
const longString = fs.readFileSync(loremFile, 'utf8');
93+
const truncatedString = logController.truncateLogMessage(longString);
94+
expect(truncatedString.length).toBe(1015); // truncate length + the string '... (truncated)'
95+
});
96+
97+
it('should truncate input and result of long lines', done => {
98+
const logController = new LoggerController(new WinstonLoggerAdapter());
99+
const longString = fs.readFileSync(loremFile, 'utf8');
100+
Parse.Cloud.define('aFunction', (req, res) => {
101+
res.success(req.params);
102+
});
103+
104+
Parse.Cloud.run('aFunction', { longString })
105+
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
106+
.then(logs => {
107+
const log = logs[0];
108+
expect(log.level).toEqual('info');
109+
expect(log.message).toMatch(
110+
/Ran cloud function aFunction for user [^ ]* with:\n Input: {.*?\(truncated\)$/m);
111+
done();
112+
})
113+
.then(null, e => done.fail(e));
114+
});
115+
116+
it('should log an afterSave', done => {
117+
const logController = new LoggerController(new WinstonLoggerAdapter());
118+
Parse.Cloud.afterSave("MyObject", (req) => { });
119+
new Parse.Object('MyObject')
120+
.save()
121+
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
122+
.then((logs) => {
123+
const log = logs[0];
124+
expect(log.triggerType).toEqual('afterSave');
125+
done();
126+
})
127+
// catch errors - not that the error is actually useful :(
128+
.then(null, e => done.fail(e));
129+
});
130+
131+
it('should log a denied beforeSave', done => {
132+
const logController = new LoggerController(new WinstonLoggerAdapter());
133+
Parse.Cloud.beforeSave("MyObject", (req, res) => {
134+
res.error('uh oh!');
135+
});
136+
137+
new Parse.Object('MyObject')
138+
.save()
139+
.then(
140+
() => done.fail('this is not supposed to succeed'),
141+
e => logController.getLogs({ from: Date.now() - 500, size: 1000 })
142+
)
143+
.then(logs => {
144+
const log = logs[1]; // 0 is the 'uh oh!' from rejection...
145+
expect(log.level).toEqual('error');
146+
expect(log.error).toEqual({ code: 141, message: 'uh oh!' });
147+
done()
148+
});
149+
});
150+
151+
it('should log cloud function success', done => {
152+
const logController = new LoggerController(new WinstonLoggerAdapter());
153+
154+
Parse.Cloud.define('aFunction', (req, res) => {
155+
res.success('it worked!');
156+
});
157+
158+
Parse.Cloud.run('aFunction', { foo: 'bar' })
159+
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
160+
.then(logs => {
161+
const log = logs[0];
162+
expect(log.level).toEqual('info');
163+
expect(log.message).toMatch(
164+
/Ran cloud function aFunction for user [^ ]* with:\n Input: {"foo":"bar"}\n Result: "it worked!/);
165+
done();
166+
});
167+
});
168+
169+
it('should log cloud function failure', done => {
170+
const logController = new LoggerController(new WinstonLoggerAdapter());
171+
172+
Parse.Cloud.define('aFunction', (req, res) => {
173+
res.error('it failed!');
174+
});
175+
176+
Parse.Cloud.run('aFunction', { foo: 'bar' })
177+
.then(null, () => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
178+
.then(logs => {
179+
const log = logs[1];
180+
expect(log.level).toEqual('error');
181+
expect(log.message).toMatch(
182+
/Failed running cloud function aFunction for user [^ ]* with:\n Input: {"foo":"bar"}\n Error: {"code":141,"message":"it failed!"}/);
183+
done();
184+
});
185+
});
186+
187+
xit('should log a changed beforeSave indicating a change', done => {
188+
const logController = new LoggerController(new WinstonLoggerAdapter());
189+
190+
Parse.Cloud.beforeSave("MyObject", (req, res) => {
191+
const myObj = req.object;
192+
myObj.set('aChange', true);
193+
res.success(myObj);
194+
});
195+
196+
new Parse.Object('MyObject')
197+
.save()
198+
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
199+
.then(logs => {
200+
// expect the log to indicate that it has changed
201+
/*
202+
Here's what it looks like on parse.com...
203+
204+
Input: {"original":{"clientVersion":"1","createdAt":"2016-06-02T05:29:08.694Z","image":{"__type":"File","name":"tfss-xxxxxxxx.png","url":"http://files.parsetfss.com/xxxxxxxx.png"},"lastScanDate":{"__type":"Date","iso":"2016-06-02T05:28:58.135Z"},"localIdentifier":"XXXXX","objectId":"OFHMX7ZUcI","status":... (truncated)
205+
Result: Update changed to {"object":{"__type":"Pointer","className":"Emoticode","objectId":"ksrq7z3Ehc"},"imageThumb":{"__type":"File","name":"tfss-xxxxxxx.png","url":"http://files.parsetfss.com/xxxxx.png"},"status":"success"}
206+
*/
207+
done();
208+
})
209+
.then(null, e => done.fail(JSON.stringify(e)));
210+
}).pend('needs more work.....');
67211
});

spec/support/lorem.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lobortis semper diam, ac euismod diam pharetra sed. Etiam eget efficitur neque. Proin nec diam mi. Sed ut purus dolor. Nulla nulla nibh, ornare vitae ornare et, scelerisque rutrum eros. Mauris venenatis tincidunt turpis a mollis. Donec gravida eget enim in luctus.
2+
3+
Sed porttitor commodo orci, ut pretium eros convallis eget. Curabitur pretium velit in odio dictum luctus. Vivamus ac tristique arcu, a semper tellus. Morbi euismod purus dapibus vestibulum sagittis. Nunc dapibus vehicula leo at scelerisque. Donec porta mauris quis nulla imperdiet consectetur. Curabitur sagittis eleifend arcu eget elementum. Aenean interdum tincidunt ornare. Pellentesque sit amet interdum tortor. Pellentesque blandit nisl eget euismod consequat. Etiam feugiat felis sit amet porta pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4+
5+
Nulla faucibus sem ipsum, at rhoncus diam pulvinar at. Vivamus consectetur, diam at aliquet vestibulum, sem purus elementum nulla, eget tincidunt nullam.

src/Controllers/LoggerController.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import AdaptableController from './AdaptableController';
44
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
55

66
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
7+
const LOG_STRING_TRUNCATE_LENGTH = 1000;
8+
const truncationMarker = '... (truncated)';
79

810
export const LogLevel = {
911
INFO: 'info',
@@ -16,7 +18,7 @@ export const LogOrder = {
1618
}
1719

1820
export class LoggerController extends AdaptableController {
19-
21+
2022
log(level, args) {
2123
args = [].concat(level, [...args]);
2224
this.adapter.log.apply(this.adapter, args);
@@ -25,7 +27,7 @@ export class LoggerController extends AdaptableController {
2527
info() {
2628
return this.log('info', arguments);
2729
}
28-
30+
2931
error() {
3032
return this.log('error', arguments);
3133
}
@@ -59,6 +61,15 @@ export class LoggerController extends AdaptableController {
5961
return null;
6062
}
6163

64+
truncateLogMessage(string) {
65+
if (string && string.length > LOG_STRING_TRUNCATE_LENGTH) {
66+
const truncated = string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker;
67+
return truncated;
68+
}
69+
70+
return string;
71+
}
72+
6273
static parseOptions(options = {}) {
6374
let from = LoggerController.validDateTime(options.from) ||
6475
new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);

src/Routers/FunctionsRouter.js

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,35 @@ export class FunctionsRouter extends PromiseRouter {
7878
}
7979

8080
return new Promise(function (resolve, reject) {
81-
var response = FunctionsRouter.createResponseObject((result) => {
82-
logger.info(`Ran cloud function ${req.params.functionName} with:\nInput: ${JSON.stringify(params)}\nResult: ${JSON.stringify(result.response.result)}`, {
83-
functionName: req.params.functionName,
84-
params,
85-
result: result.response.result
86-
});
87-
resolve(result);
88-
}, (error) => {
89-
logger.error(`Failed running cloud function ${req.params.functionName} with:\nInput: ${JSON.stringify(params)}\Error: ${JSON.stringify(error)}`, {
90-
functionName: req.params.functionName,
91-
params,
92-
error
93-
});
94-
reject(error);
81+
const userString = (req.auth && req.auth.user) ? req.auth.user.id : undefined;
82+
const cleanInput = logger.truncateLogMessage(JSON.stringify(params));
83+
var response = FunctionsRouter.createResponseObject((result) => {
84+
try {
85+
const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result));
86+
logger.info(`Ran cloud function ${req.params.functionName} for user ${userString} `
87+
+ `with:\n Input: ${cleanInput }\n Result: ${cleanResult }`, {
88+
functionName: req.params.functionName,
89+
params,
90+
user: userString,
91+
});
92+
resolve(result);
93+
} catch (e) {
94+
reject(e);
95+
}
96+
}, (error) => {
97+
try {
98+
logger.error(`Failed running cloud function ${req.params.functionName} for `
99+
+ `user ${userString} with:\n Input: ${cleanInput}\n Error: `
100+
+ JSON.stringify(error), {
101+
functionName: req.params.functionName,
102+
error,
103+
params,
104+
user: userString
105+
});
106+
reject(error);
107+
} catch (e) {
108+
reject(e);
109+
}
95110
});
96111
// Force the keys before the function calls.
97112
Parse.applicationId = req.config.applicationId;

0 commit comments

Comments
 (0)