feat: Add option to change the log level of the logs emitted by triggers (#8328)
This commit is contained in:
@@ -254,7 +254,7 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
stream.on('data', chunk => {
|
||||
res.write(chunk);
|
||||
});
|
||||
stream.on('error', (e) => {
|
||||
stream.on('error', e => {
|
||||
res.status(404);
|
||||
res.send(e.message);
|
||||
});
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
// configured.
|
||||
// mount is the URL for the root of the API; includes http, domain, etc.
|
||||
|
||||
import { isBoolean, isString } from 'lodash';
|
||||
import net from 'net';
|
||||
import AppCache from './cache';
|
||||
import DatabaseController from './Controllers/DatabaseController';
|
||||
import net from 'net';
|
||||
import { logLevels as validLogLevels } from './Controllers/LoggerController';
|
||||
import {
|
||||
IdempotencyOptions,
|
||||
FileUploadOptions,
|
||||
AccountLockoutOptions,
|
||||
FileUploadOptions,
|
||||
IdempotencyOptions,
|
||||
LogLevels,
|
||||
PagesOptions,
|
||||
SecurityOptions,
|
||||
SchemaOptions,
|
||||
ParseServerOptions,
|
||||
SchemaOptions,
|
||||
SecurityOptions,
|
||||
} from './Options/Definitions';
|
||||
import { isBoolean, isString } from 'lodash';
|
||||
|
||||
function removeTrailingSlash(str) {
|
||||
if (!str) {
|
||||
@@ -82,6 +84,7 @@ export class Config {
|
||||
schema,
|
||||
requestKeywordDenylist,
|
||||
allowExpiredAuthDataToken,
|
||||
logLevels,
|
||||
}) {
|
||||
if (masterKey === readOnlyMasterKey) {
|
||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||
@@ -123,6 +126,7 @@ export class Config {
|
||||
this.validateEnforcePrivateUsers(enforcePrivateUsers);
|
||||
this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
|
||||
this.validateRequestKeywordDenylist(requestKeywordDenylist);
|
||||
this.validateLogLevels(logLevels);
|
||||
}
|
||||
|
||||
static validateRequestKeywordDenylist(requestKeywordDenylist) {
|
||||
@@ -501,6 +505,18 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
static validateLogLevels(logLevels) {
|
||||
for (const key of Object.keys(LogLevels)) {
|
||||
if (logLevels[key]) {
|
||||
if (validLogLevels.indexOf(logLevels[key]) === -1) {
|
||||
throw `'${key}' must be one of ${JSON.stringify(validLogLevels)}`;
|
||||
}
|
||||
} else {
|
||||
logLevels[key] = LogLevels[key].default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateEmailVerifyTokenExpiresAt() {
|
||||
if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) {
|
||||
return undefined;
|
||||
|
||||
@@ -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' }) {
|
||||
|
||||
@@ -290,6 +290,12 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_LOG_LEVEL',
|
||||
help: 'Sets the level for logs',
|
||||
},
|
||||
logLevels: {
|
||||
env: 'PARSE_SERVER_LOG_LEVELS',
|
||||
help: '(Optional) Overrides the log levels used internally by Parse Server to log events.',
|
||||
action: parsers.objectParser,
|
||||
default: {},
|
||||
},
|
||||
logsFolder: {
|
||||
env: 'PARSE_SERVER_LOGS_FOLDER',
|
||||
help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging",
|
||||
@@ -898,3 +904,23 @@ module.exports.AuthAdapter = {
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
module.exports.LogLevels = {
|
||||
triggerAfter: {
|
||||
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER',
|
||||
help:
|
||||
'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterSaveFile`, `afterDeleteFile`, `afterFind`, `afterLogout`. Default is `info`.',
|
||||
default: 'info',
|
||||
},
|
||||
triggerBeforeError: {
|
||||
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR',
|
||||
help:
|
||||
'Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on error. Default is `error `.',
|
||||
default: 'error',
|
||||
},
|
||||
triggerBeforeSuccess: {
|
||||
env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS',
|
||||
help:
|
||||
'Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on success. Default is `info`.',
|
||||
default: 'info',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events.
|
||||
* @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', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.
|
||||
@@ -215,3 +216,10 @@
|
||||
* @interface AuthAdapter
|
||||
* @property {Boolean} enabled Is `true` if the auth adapter is enabled, `false` otherwise.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface LogLevels
|
||||
* @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterSaveFile`, `afterDeleteFile`, `afterFind`, `afterLogout`. Default is `info`.
|
||||
* @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on error. Default is `error `.
|
||||
* @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on success. Default is `info`.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
|
||||
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
|
||||
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
||||
import { CacheAdapter } from '../Adapters/Cache/CacheAdapter';
|
||||
import { MailAdapter } from '../Adapters/Email/MailAdapter';
|
||||
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
|
||||
import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter';
|
||||
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
||||
import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter';
|
||||
import { CheckGroup } from '../Security/CheckGroup';
|
||||
|
||||
@@ -81,6 +81,9 @@ export interface ParseServerOptions {
|
||||
verbose: ?boolean;
|
||||
/* Sets the level for logs */
|
||||
logLevel: ?string;
|
||||
/* (Optional) Overrides the log levels used internally by Parse Server to log events.
|
||||
:DEFAULT: {} */
|
||||
logLevels: ?LogLevels;
|
||||
/* 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
|
||||
@@ -520,3 +523,18 @@ export interface AuthAdapter {
|
||||
*/
|
||||
enabled: ?boolean;
|
||||
}
|
||||
|
||||
export interface LogLevels {
|
||||
/* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterSaveFile`, `afterDeleteFile`, `afterFind`, `afterLogout`. Default is `info`.
|
||||
:DEFAULT: info
|
||||
*/
|
||||
triggerAfter: ?string;
|
||||
/* Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on success. Default is `info`.
|
||||
:DEFAULT: info
|
||||
*/
|
||||
triggerBeforeSuccess: ?string;
|
||||
/* Log level used by the Cloud Code Triggers `beforeSave`, `beforeSaveFile`, `beforeDeleteFile`, `beforeFind`, `beforeLogin` on error. Default is `error `.
|
||||
:DEFAULT: error
|
||||
*/
|
||||
triggerBeforeError: ?string;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import Config from '../../Config';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
/**
|
||||
* The security checks group for Parse Server configuration.
|
||||
* Checks common Parse Server parameters such as access keys.
|
||||
*/
|
||||
* The security checks group for Parse Server configuration.
|
||||
* Checks common Parse Server parameters such as access keys.
|
||||
*/
|
||||
class CheckGroupDatabase extends CheckGroup {
|
||||
setName() {
|
||||
return 'Database';
|
||||
@@ -23,7 +23,8 @@ class CheckGroupDatabase extends CheckGroup {
|
||||
new Check({
|
||||
title: 'Secure database password',
|
||||
warning: 'The database password is insecure and vulnerable to brute force attacks.',
|
||||
solution: 'Choose a longer and/or more complex password with a combination of upper- and lowercase characters, numbers and special characters.',
|
||||
solution:
|
||||
'Choose a longer and/or more complex password with a combination of upper- and lowercase characters, numbers and special characters.',
|
||||
check: () => {
|
||||
const password = databaseUrl.match(/\/\/\S+:(\S+)@/)[1];
|
||||
const hasUpperCase = /[A-Z]/.test(password);
|
||||
|
||||
@@ -373,9 +373,9 @@ 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(
|
||||
logger[logLevel](
|
||||
`${triggerType} triggered for ${className} for user ${userIdForLog(
|
||||
auth
|
||||
)}:\n Input: ${cleanInput}`,
|
||||
@@ -387,10 +387,10 @@ function logTriggerAfterHook(triggerType, className, input, 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(
|
||||
logger[logLevel](
|
||||
`${triggerType} triggered for ${className} for user ${userIdForLog(
|
||||
auth
|
||||
)}:\n Input: ${cleanInput}\n Result: ${cleanResult}`,
|
||||
@@ -402,9 +402,9 @@ function logTriggerSuccessBeforeHook(triggerType, className, input, result, 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(
|
||||
logger[logLevel](
|
||||
`${triggerType} failed for ${className} for user ${userIdForLog(
|
||||
auth
|
||||
)}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`,
|
||||
@@ -444,7 +444,14 @@ export function maybeRunAfterFindTrigger(
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
logTriggerSuccessBeforeHook(triggerType, className, 'AfterFind', JSON.stringify(objects), auth);
|
||||
logTriggerSuccessBeforeHook(
|
||||
triggerType,
|
||||
className,
|
||||
'AfterFind',
|
||||
JSON.stringify(objects),
|
||||
auth,
|
||||
config.logLevels.triggerBeforeSuccess
|
||||
);
|
||||
request.objects = objects.map(object => {
|
||||
//setting the class name to transform into parse object
|
||||
object.className = className;
|
||||
@@ -468,7 +475,13 @@ export function maybeRunAfterFindTrigger(
|
||||
})
|
||||
.then(success, error);
|
||||
}).then(results => {
|
||||
logTriggerAfterHook(triggerType, className, JSON.stringify(results), auth);
|
||||
logTriggerAfterHook(
|
||||
triggerType,
|
||||
className,
|
||||
JSON.stringify(results),
|
||||
auth,
|
||||
config.logLevels.triggerAfter
|
||||
);
|
||||
return results;
|
||||
});
|
||||
}
|
||||
@@ -842,7 +855,10 @@ export function maybeRunTrigger(
|
||||
parseObject.className,
|
||||
parseObject.toJSON(),
|
||||
object,
|
||||
auth
|
||||
auth,
|
||||
triggerType.startsWith('after')
|
||||
? config.logLevels.triggerAfter
|
||||
: config.logLevels.triggerBeforeSuccess
|
||||
);
|
||||
if (
|
||||
triggerType === Types.beforeSave ||
|
||||
@@ -860,7 +876,8 @@ export function maybeRunTrigger(
|
||||
parseObject.className,
|
||||
parseObject.toJSON(),
|
||||
auth,
|
||||
error
|
||||
error,
|
||||
config.logLevels.triggerBeforeError
|
||||
);
|
||||
reject(error);
|
||||
}
|
||||
@@ -885,7 +902,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.logLevels.triggerAfter
|
||||
);
|
||||
}
|
||||
// beforeSave is expected to return null (nothing)
|
||||
if (triggerType === Types.beforeSave) {
|
||||
@@ -965,7 +988,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
|
||||
'Parse.File',
|
||||
{ ...fileObject.file.toJSON(), fileSize: fileObject.fileSize },
|
||||
result,
|
||||
auth
|
||||
auth,
|
||||
config.logLevels.triggerBeforeSuccess
|
||||
);
|
||||
return result || fileObject;
|
||||
} catch (error) {
|
||||
@@ -974,7 +998,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
|
||||
'Parse.File',
|
||||
{ ...fileObject.file.toJSON(), fileSize: fileObject.fileSize },
|
||||
auth,
|
||||
error
|
||||
error,
|
||||
config.logLevels.triggerBeforeError
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user