Skip to content

feat: Add option to change the log level of the logs emitted by triggers #8328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a391ebd
feat: add option to change the log level of the logs emitted by triggers
alljinx Nov 21, 2022
b0102fd
feat: add option to change the log level of the logs emitted by triggers
alljinx Nov 22, 2022
c792e51
Update src/Options/index.js
alljinx Nov 23, 2022
6a4b83b
Multiples fixes suggested by mtrezza, including renaming logLevels op…
alljinx Nov 23, 2022
2c167d3
Refactoring test.
alljinx Nov 23, 2022
30e21eb
Update src/Options/index.js
alljinx Nov 23, 2022
d999740
Update src/Options/index.js
alljinx Nov 23, 2022
3bada12
Update src/Options/index.js
alljinx Nov 23, 2022
1b61a2e
Minor correction on doc and regeneration.
alljinx Nov 23, 2022
6b4b787
prettier && lint-fix
alljinx Nov 23, 2022
dfae832
Update spec/CloudCodeLogger.spec.js
mtrezza Nov 23, 2022
4f466a6
Update spec/CloudCodeLogger.spec.js
mtrezza Nov 23, 2022
d0bed5d
Update spec/CloudCodeLogger.spec.js
mtrezza Nov 23, 2022
5fa9f72
Update src/Config.js
alljinx Nov 24, 2022
856d49f
Revert "Update src/Config.js"
alljinx Nov 24, 2022
41d47ce
Fix undefinedTRIGGER_AFTER in Definitions.js
alljinx Nov 24, 2022
6933894
Remove the 'none' option from LogLevels.
alljinx Nov 24, 2022
c768f8f
Update src/Config.js
alljinx Nov 25, 2022
25c0f61
Update src/Config.js
alljinx Nov 25, 2022
db15883
Fix ctrValidLogLevels -> validLogLevels
alljinx Nov 25, 2022
a6735c0
Merge branch 'alpha' into triggers-logger-config
mtrezza Nov 25, 2022
2f9b9ff
Merge branch 'alpha' into triggers-logger-config
mtrezza Nov 25, 2022
826cc63
Merge branch 'alpha' into triggers-logger-config
mtrezza Nov 26, 2022
477b03e
Fix postgres unit tests
alljinx Nov 26, 2022
a3be352
Merge branch 'alpha' into triggers-logger-config
alljinx Nov 26, 2022
21d2ee5
Merge branch 'alpha' into triggers-logger-config
alljinx Dec 7, 2022
69fdeaf
Merge branch 'alpha' into triggers-logger-config
mtrezza Dec 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions spec/CloudCodeLogger.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,60 @@ describe('Cloud Code Logger', () => {
});
});

it('should log cloud function triggers using the custom log level', async done => {
Parse.Cloud.beforeSave('TestClass', () => {});
Parse.Cloud.afterSave('TestClass', () => {});

{
await reconfigureServer({
silent: true,
logLevel: 'silly',
logLevelUses: {
triggerAfterHook: 'debug',
triggerSuccessBeforeHook: 'silly',
},
});

spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();

const obj = new Parse.Object('TestClass');
await obj.save();

let log = spy.calls.argsFor(1);
expect(log[0]).toEqual('silly');
expect(log[1]).toMatch(/beforeSave triggered for TestClass for user .*/);

log = spy.calls.argsFor(2);
expect(log[0]).toEqual('debug');
expect(log[1]).toMatch(/afterSave triggered for TestClass for user .*/);
}

{
await reconfigureServer({
silent: true,
logLevel: 'info',
logLevelUses: {
triggerSuccessBeforeHook: 'none',
triggerErrorBeforeHook: 'warn',
},
});

spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();

const obj = new Parse.Object('TestClass');
await obj.save();

let log = spy.calls.argsFor(1);
expect(log[0]).toEqual('warn');
expect(log[1]).toMatch('afterSave caught an error');

log = spy.calls.argsFor(2);
expect(log).toEqual([]);
}

done();
});

it('should log cloud function failure', done => {
Parse.Cloud.define('aFunction', () => {
throw 'it failed!';
Expand Down
12 changes: 12 additions & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ParseServerOptions,
} from './Options/Definitions';
import { isBoolean, isString } from 'lodash';
import { logLevels } from './Controllers/LoggerController';

function removeTrailingSlash(str) {
if (!str) {
Expand Down Expand Up @@ -82,6 +83,7 @@ export class Config {
schema,
requestKeywordDenylist,
allowExpiredAuthDataToken,
logLevelUses,
}) {
if (masterKey === readOnlyMasterKey) {
throw new Error('masterKey and readOnlyMasterKey should be different');
Expand Down Expand Up @@ -123,6 +125,7 @@ export class Config {
this.validateEnforcePrivateUsers(enforcePrivateUsers);
this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
this.validateRequestKeywordDenylist(requestKeywordDenylist);
this.validateLogLevelUses(logLevelUses);
}

static validateRequestKeywordDenylist(requestKeywordDenylist) {
Expand Down Expand Up @@ -501,6 +504,15 @@ export class Config {
}
}

static validateLogLevelUses(logLevelUses) {
const possibleValues = ['none', ...logLevels];
for (const entry of Object.entries(logLevelUses)) {
if (possibleValues.indexOf(entry[1]) === -1) {
throw entry[0] + ' must be one of theses ' + possibleValues;
}
}
}

generateEmailVerifyTokenExpiresAt() {
if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/Controllers/LoggerController.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const LogOrder = {
ASCENDING: 'asc',
};

const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly'];
export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly'];

export class LoggerController extends AdaptableController {
constructor(adapter, appId, options = { logLevel: 'info' }) {
Expand Down
10 changes: 10 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,16 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_LOG_LEVEL',
help: 'Sets the level for logs',
},
logLevelUses: {
env: 'PARSE_SERVER_LOG_LEVEL_USES',
help: 'Set the level used internally by Parse Server features',
action: parsers.objectParser,
default: {
triggerAfterHook: 'info',
triggerSuccessBeforeHook: 'info',
triggerErrorBeforeHook: 'error',
},
},
logsFolder: {
env: 'PARSE_SERVER_LOGS_FOLDER',
help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging",
Expand Down
8 changes: 8 additions & 0 deletions src/Options/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
* @property {LiveQueryServerOptions} liveQueryServerOptions Live query server configuration options (will start the liveQuery server)
* @property {Adapter<LoggerAdapter>} loggerAdapter Adapter module for the logging sub-system
* @property {String} logLevel Sets the level for logs
* @property {LogLevelUses} logLevelUses Set the level used internally by Parse Server features
* @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging
* @property {String} masterKey Your Parse Master Key
* @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey`` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1']` which means that only `localhost`, the server itself, is allowed to use the master key.
Expand Down Expand Up @@ -215,3 +216,10 @@
* @interface AuthAdapter
* @property {Boolean} enabled Is `true` if the auth adapter is enabled, `false` otherwise.
*/

/**
* @interface LogLevelUses
* @property {Boolean} triggerAfterHook Log level used by the after hook trigger, default is 'info'.
* @property {Boolean} triggerSuccessBeforeHook Log level used by the success before hook trigger, default is 'info.
* @property {Boolean} triggerErrorBeforeHook Log level used by the error before hook trigger, default is 'error.
*/
18 changes: 18 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export interface ParseServerOptions {
verbose: ?boolean;
/* Sets the level for logs */
logLevel: ?string;
/* Set the level used internally by Parse Server features
:ENV: PARSE_SERVER_LOG_LEVEL_USES */
logLevelUses: ?LogLevelUses;
/* Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null) */
maxLogFiles: ?NumberOrString;
/* Disables console output
Expand Down Expand Up @@ -520,3 +523,18 @@ export interface AuthAdapter {
*/
enabled: ?boolean;
}

export interface LogLevelUses {
/* Log level used by the after hook trigger, default is 'info'.
:DEFAULT: 'info'
*/
triggerAfterHook: ?string;
/* Log level used by the success before hook trigger, default is 'info.
:DEFAULT: 'info'
*/
triggerSuccessBeforeHook: ?string;
/* Log level used by the error before hook trigger, default is 'error.
:DEFAULT: 'error'
*/
triggerErrorBeforeHook: ?string;
}
111 changes: 70 additions & 41 deletions src/triggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,48 +373,54 @@ function userIdForLog(auth) {
return auth && auth.user ? auth.user.id : undefined;
}

function logTriggerAfterHook(triggerType, className, input, auth) {
function logTriggerAfterHook(triggerType, className, input, auth, logLevel) {
const cleanInput = logger.truncateLogMessage(JSON.stringify(input));
logger.info(
`${triggerType} triggered for ${className} for user ${userIdForLog(
auth
)}:\n Input: ${cleanInput}`,
{
className,
triggerType,
user: userIdForLog(auth),
}
);
if (logLevel !== 'none') {
logger[logLevel](
`${triggerType} triggered for ${className} for user ${userIdForLog(
auth
)}:\n Input: ${cleanInput}`,
{
className,
triggerType,
user: userIdForLog(auth),
}
);
}
}

function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth) {
function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth, logLevel) {
const cleanInput = logger.truncateLogMessage(JSON.stringify(input));
const cleanResult = logger.truncateLogMessage(JSON.stringify(result));
logger.info(
`${triggerType} triggered for ${className} for user ${userIdForLog(
auth
)}:\n Input: ${cleanInput}\n Result: ${cleanResult}`,
{
className,
triggerType,
user: userIdForLog(auth),
}
);
if (logLevel !== 'none') {
logger[logLevel](
`${triggerType} triggered for ${className} for user ${userIdForLog(
auth
)}:\n Input: ${cleanInput}\n Result: ${cleanResult}`,
{
className,
triggerType,
user: userIdForLog(auth),
}
);
}
}

function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) {
function logTriggerErrorBeforeHook(triggerType, className, input, auth, error, logLevel) {
const cleanInput = logger.truncateLogMessage(JSON.stringify(input));
logger.error(
`${triggerType} failed for ${className} for user ${userIdForLog(
auth
)}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`,
{
className,
triggerType,
error,
user: userIdForLog(auth),
}
);
if (logLevel !== 'none') {
logger[logLevel](
`${triggerType} failed for ${className} for user ${userIdForLog(
auth
)}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`,
{
className,
triggerType,
error,
user: userIdForLog(auth),
}
);
}
}

export function maybeRunAfterFindTrigger(
Expand Down Expand Up @@ -444,7 +450,14 @@ export function maybeRunAfterFindTrigger(
reject(error);
}
);
logTriggerSuccessBeforeHook(triggerType, className, 'AfterFind', JSON.stringify(objects), auth);
logTriggerSuccessBeforeHook(
triggerType,
className,
'AfterFind',
JSON.stringify(objects),
auth,
config.logLevelUses.triggerSuccessBeforeHook
);
request.objects = objects.map(object => {
//setting the class name to transform into parse object
object.className = className;
Expand All @@ -468,7 +481,13 @@ export function maybeRunAfterFindTrigger(
})
.then(success, error);
}).then(results => {
logTriggerAfterHook(triggerType, className, JSON.stringify(results), auth);
logTriggerAfterHook(
triggerType,
className,
JSON.stringify(results),
auth,
config.logLevelUses.triggerAfterHook
);
return results;
});
}
Expand Down Expand Up @@ -842,7 +861,8 @@ export function maybeRunTrigger(
parseObject.className,
parseObject.toJSON(),
object,
auth
auth,
config.logLevelUses.triggerSuccessBeforeHook
);
if (
triggerType === Types.beforeSave ||
Expand All @@ -860,7 +880,8 @@ export function maybeRunTrigger(
parseObject.className,
parseObject.toJSON(),
auth,
error
error,
config.logLevelUses.triggerErrorBeforeHook
);
reject(error);
}
Expand All @@ -885,7 +906,13 @@ export function maybeRunTrigger(
triggerType === Types.afterDelete ||
triggerType === Types.afterLogin
) {
logTriggerAfterHook(triggerType, parseObject.className, parseObject.toJSON(), auth);
logTriggerAfterHook(
triggerType,
parseObject.className,
parseObject.toJSON(),
auth,
config.logLevelUses.triggerAfterHook
);
}
// beforeSave is expected to return null (nothing)
if (triggerType === Types.beforeSave) {
Expand Down Expand Up @@ -965,7 +992,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
'Parse.File',
{ ...fileObject.file.toJSON(), fileSize: fileObject.fileSize },
result,
auth
auth,
config.logLevelUses.triggerSuccessBeforeHook
);
return result || fileObject;
} catch (error) {
Expand All @@ -974,7 +1002,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
'Parse.File',
{ ...fileObject.file.toJSON(), fileSize: fileObject.fileSize },
auth,
error
error,
config.logLevelUses.triggerErrorBeforeHook
);
throw error;
}
Expand Down