From 909a285b6336aa5202f675a35b42dc1dd9df0e8c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 03:24:30 +0100 Subject: [PATCH 01/11] feat --- .../Storage/Mongo/MongoStorageAdapter.js | 28 +++++++++++++++++++ src/Options/Definitions.js | 6 ++++ src/Options/docs.js | 1 + src/Options/index.js | 2 ++ 4 files changed, 37 insertions(+) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 39b335d52e..3a2894502b 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -132,6 +132,7 @@ export class MongoStorageAdapter implements StorageAdapter { _mongoOptions: Object; _onchange: any; _stream: any; + _clientLogEvents: ?Array; // Public connectionPromise: ?Promise; database: any; @@ -154,6 +155,7 @@ export class MongoStorageAdapter implements StorageAdapter { this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; + this._clientLogEvents = mongoOptions.clientLogEvents; // Remove Parse Server-specific options that should not be passed to MongoDB client // Note: We only delete from this._mongoOptions, not from the original mongoOptions object, // because other components (like DatabaseController) need access to these options @@ -162,6 +164,7 @@ export class MongoStorageAdapter implements StorageAdapter { 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation', + 'clientLogEvents', 'createIndexUserUsername', 'createIndexUserUsernameCaseInsensitive', 'createIndexUserEmail', @@ -203,6 +206,31 @@ export class MongoStorageAdapter implements StorageAdapter { client.on('close', () => { delete this.connectionPromise; }); + + // Set up client event logging if configured + if (this._clientLogEvents && Array.isArray(this._clientLogEvents)) { + this._clientLogEvents.forEach(eventConfig => { + client.on(eventConfig.name, event => { + let logData = {}; + if (!eventConfig.keys || eventConfig.keys.length === 0) { + logData = event; + } else { + eventConfig.keys.forEach(keyPath => { + const keyParts = keyPath.split('.'); + let value = event; + keyParts.forEach(part => { + value = value ? value[part] : undefined; + }); + logData[keyPath] = value; + }); + } + + const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData)}`; + logger[eventConfig.logLevel](logMessage); + }); + }); + } + this.client = client; this.database = database; }) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 96d1b9b32b..a9c56dcad1 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1117,6 +1117,12 @@ module.exports.DatabaseOptions = { 'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.', action: parsers.numberParser('autoSelectFamilyAttemptTimeout'), }, + clientLogEvents: { + env: 'PARSE_SERVER_DATABASE_CLIENT_LOG_EVENTS', + help: + "An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
", + action: parsers.arrayParser, + }, compressors: { env: 'PARSE_SERVER_DATABASE_COMPRESSORS', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index ade153cdb3..f9c24bd0fe 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -246,6 +246,7 @@ * @property {String} authSource The MongoDB driver option to specify the database name associated with the user's credentials. * @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address. * @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. + * @property {Any[]} clientLogEvents An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
* @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. * @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. * @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. diff --git a/src/Options/index.js b/src/Options/index.js index 35b5821987..1922e92022 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -725,6 +725,8 @@ export interface DatabaseOptions { createIndexRoleName: ?boolean; /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ disableIndexFieldValidation: ?boolean; + /* An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
*/ + clientLogEvents: ?(any[]); } export interface AuthAdapter { From 98c71c3a2e004b6795e3f05eda8b90fcd1cb4a5e Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 03:32:34 +0100 Subject: [PATCH 02/11] test --- spec/MongoStorageAdapter.spec.js | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 7d0d220cff..fa7c07d79d 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -824,4 +824,112 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined(); }); }); + + describe('clientLogEvents', () => { + it('should log MongoDB client events when configured', async () => { + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'warn'); + + const clientLogEvents = [ + { + name: 'serverDescriptionChanged', + keys: ['address'], + logLevel: 'warn', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { clientLogEvents }, + }); + + // Connect to trigger event listeners setup + await adapter.connect(); + + // Manually trigger the event to test the listener + const mockEvent = { + address: 'localhost:27017', + previousDescription: { type: 'Unknown' }, + newDescription: { type: 'Standalone' }, + }; + + adapter.client.emit('serverDescriptionChanged', mockEvent); + + // Verify the log was called with the correct message + expect(logSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event serverDescriptionChanged:.*"address":"localhost:27017"/) + ); + + await adapter.handleShutdown(); + }); + + it('should log entire event when keys are not specified', async () => { + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'info'); + + const clientLogEvents = [ + { + name: 'connectionPoolReady', + logLevel: 'info', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { clientLogEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + address: 'localhost:27017', + options: { maxPoolSize: 100 }, + }; + + adapter.client.emit('connectionPoolReady', mockEvent); + + expect(logSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017".*"options"/) + ); + + await adapter.handleShutdown(); + }); + + it('should extract nested keys using dot notation', async () => { + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'warn'); + + const clientLogEvents = [ + { + name: 'topologyDescriptionChanged', + keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'], + logLevel: 'warn', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { clientLogEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + topologyId: 1, + previousDescription: { type: 'Unknown' }, + newDescription: { + type: 'ReplicaSetWithPrimary', + servers: { size: 3 }, + }, + }; + + adapter.client.emit('topologyDescriptionChanged', mockEvent); + + expect(logSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event topologyDescriptionChanged:.*"previousDescription.type":"Unknown".*"newDescription.type":"ReplicaSetWithPrimary".*"newDescription.servers.size":3/) + ); + + await adapter.handleShutdown(); + }); + }); }); From f50da139cd62b9946de6ee9a28070ff84b25bd2f Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 04:04:04 +0100 Subject: [PATCH 03/11] fix --- spec/MongoStorageAdapter.spec.js | 131 ++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 41 ++++-- src/Options/docs.js | 2 +- src/Options/index.js | 11 +- 4 files changed, 169 insertions(+), 16 deletions(-) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index fa7c07d79d..1ac9099613 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -931,5 +931,136 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { await adapter.handleShutdown(); }); + + it('should handle invalid log level gracefully', async () => { + const logger = require('../lib/logger').logger; + const infoSpy = spyOn(logger, 'info'); + + const clientLogEvents = [ + { + name: 'connectionPoolReady', + keys: ['address'], + logLevel: 'invalidLogLevel', // Invalid log level + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { clientLogEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + address: 'localhost:27017', + }; + + adapter.client.emit('connectionPoolReady', mockEvent); + + // Should fallback to 'info' level + expect(infoSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017"/) + ); + + await adapter.handleShutdown(); + }); + + it('should handle Map and Set instances in events', async () => { + const logger = require('../lib/logger').logger; + const warnSpy = spyOn(logger, 'warn'); + + const clientLogEvents = [ + { + name: 'customEvent', + logLevel: 'warn', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { clientLogEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + mapData: new Map([['key1', 'value1'], ['key2', 'value2']]), + setData: new Set([1, 2, 3]), + }; + + adapter.client.emit('customEvent', mockEvent); + + // Should serialize Map and Set properly + expect(warnSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event customEvent:.*"mapData":\{"key1":"value1","key2":"value2"\}.*"setData":\[1,2,3\]/) + ); + + await adapter.handleShutdown(); + }); + + it('should handle missing keys in event object', async () => { + const logger = require('../lib/logger').logger; + const infoSpy = spyOn(logger, 'info'); + + const clientLogEvents = [ + { + name: 'testEvent', + keys: ['nonexistent.nested.key', 'another.missing'], + logLevel: 'info', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { clientLogEvents }, + }); + + await adapter.connect(); + + const mockEvent = { + actualField: 'value', + }; + + adapter.client.emit('testEvent', mockEvent); + + // Should handle missing keys gracefully with undefined values + expect(infoSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event testEvent:/) + ); + + await adapter.handleShutdown(); + }); + + it('should handle circular references with fallback warning', async () => { + const logger = require('../lib/logger').logger; + const warnSpy = spyOn(logger, 'warn'); + + const clientLogEvents = [ + { + name: 'circularEvent', + logLevel: 'info', + }, + ]; + + const adapter = new MongoStorageAdapter({ + uri: databaseURI, + mongoOptions: { clientLogEvents }, + }); + + await adapter.connect(); + + // Create circular reference + const mockEvent = { name: 'test' }; + mockEvent.self = mockEvent; + + adapter.client.emit('circularEvent', mockEvent); + + // Should fallback to warning when JSON.stringify fails + expect(warnSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event circularEvent logged with error:/) + ); + + await adapter.handleShutdown(); + }); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 3a2894502b..f97d6f9513 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -211,22 +211,35 @@ export class MongoStorageAdapter implements StorageAdapter { if (this._clientLogEvents && Array.isArray(this._clientLogEvents)) { this._clientLogEvents.forEach(eventConfig => { client.on(eventConfig.name, event => { - let logData = {}; - if (!eventConfig.keys || eventConfig.keys.length === 0) { - logData = event; - } else { - eventConfig.keys.forEach(keyPath => { - const keyParts = keyPath.split('.'); - let value = event; - keyParts.forEach(part => { - value = value ? value[part] : undefined; + try { + let logData = {}; + if (!eventConfig.keys || eventConfig.keys.length === 0) { + logData = event; + } else { + eventConfig.keys.forEach(keyPath => { + logData[keyPath] = _.get(event, keyPath); }); - logData[keyPath] = value; - }); + } + + // Validate log level exists, fallback to 'info' + const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info'; + + // Safe JSON serialization with Map/Set support + const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, (key, value) => { + if (value instanceof Map) { + return Object.fromEntries(value); + } + if (value instanceof Set) { + return Array.from(value); + } + return value; + })}`; + + logger[logLevel](logMessage); + } catch (error) { + // Fallback if serialization completely fails + logger.warn(`MongoDB client event ${eventConfig.name} logged with error: ${error.message}`); } - - const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData)}`; - logger[eventConfig.logLevel](logMessage); }); }); } diff --git a/src/Options/docs.js b/src/Options/docs.js index f9c24bd0fe..99f1bd5ac4 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -246,7 +246,7 @@ * @property {String} authSource The MongoDB driver option to specify the database name associated with the user's credentials. * @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address. * @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. - * @property {Any[]} clientLogEvents An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
+ * @property {ClientLogEvent[]} clientLogEvents An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
* @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. * @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. * @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. diff --git a/src/Options/index.js b/src/Options/index.js index 1922e92022..0e1b18cd21 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -608,6 +608,15 @@ export interface FileUploadOptions { enableForPublic: ?boolean; } +export type ClientLogEvent = { + /* The MongoDB driver event name to listen for. */ + name: string, + /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */ + keys?: string[], + /* The log level to use for this event. */ + logLevel: string, +}; + export interface DatabaseOptions { /* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. :DEFAULT: false */ @@ -726,7 +735,7 @@ export interface DatabaseOptions { /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ disableIndexFieldValidation: ?boolean; /* An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
*/ - clientLogEvents: ?(any[]); + clientLogEvents: ?(ClientLogEvent[]); } export interface AuthAdapter { From fdfefd7a42f6cde8562d7a5caf522f95704a0adb Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 04:17:33 +0100 Subject: [PATCH 04/11] rename --- spec/MongoStorageAdapter.spec.js | 30 +++++++++---------- .../Storage/Mongo/MongoStorageAdapter.js | 22 +++++++------- src/Options/Definitions.js | 12 ++++---- src/Options/docs.js | 2 +- src/Options/index.js | 4 +-- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 1ac9099613..a5375e26f3 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -825,12 +825,12 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); }); - describe('clientLogEvents', () => { + describe('logClientEvents', () => { it('should log MongoDB client events when configured', async () => { const logger = require('../lib/logger').logger; const logSpy = spyOn(logger, 'warn'); - const clientLogEvents = [ + const logClientEvents = [ { name: 'serverDescriptionChanged', keys: ['address'], @@ -840,7 +840,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const adapter = new MongoStorageAdapter({ uri: databaseURI, - mongoOptions: { clientLogEvents }, + mongoOptions: { logClientEvents }, }); // Connect to trigger event listeners setup @@ -867,7 +867,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const logger = require('../lib/logger').logger; const logSpy = spyOn(logger, 'info'); - const clientLogEvents = [ + const logClientEvents = [ { name: 'connectionPoolReady', logLevel: 'info', @@ -876,7 +876,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const adapter = new MongoStorageAdapter({ uri: databaseURI, - mongoOptions: { clientLogEvents }, + mongoOptions: { logClientEvents }, }); await adapter.connect(); @@ -899,7 +899,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const logger = require('../lib/logger').logger; const logSpy = spyOn(logger, 'warn'); - const clientLogEvents = [ + const logClientEvents = [ { name: 'topologyDescriptionChanged', keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'], @@ -909,7 +909,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const adapter = new MongoStorageAdapter({ uri: databaseURI, - mongoOptions: { clientLogEvents }, + mongoOptions: { logClientEvents }, }); await adapter.connect(); @@ -936,7 +936,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const logger = require('../lib/logger').logger; const infoSpy = spyOn(logger, 'info'); - const clientLogEvents = [ + const logClientEvents = [ { name: 'connectionPoolReady', keys: ['address'], @@ -946,7 +946,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const adapter = new MongoStorageAdapter({ uri: databaseURI, - mongoOptions: { clientLogEvents }, + mongoOptions: { logClientEvents }, }); await adapter.connect(); @@ -969,7 +969,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const logger = require('../lib/logger').logger; const warnSpy = spyOn(logger, 'warn'); - const clientLogEvents = [ + const logClientEvents = [ { name: 'customEvent', logLevel: 'warn', @@ -978,7 +978,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const adapter = new MongoStorageAdapter({ uri: databaseURI, - mongoOptions: { clientLogEvents }, + mongoOptions: { logClientEvents }, }); await adapter.connect(); @@ -1002,7 +1002,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const logger = require('../lib/logger').logger; const infoSpy = spyOn(logger, 'info'); - const clientLogEvents = [ + const logClientEvents = [ { name: 'testEvent', keys: ['nonexistent.nested.key', 'another.missing'], @@ -1012,7 +1012,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const adapter = new MongoStorageAdapter({ uri: databaseURI, - mongoOptions: { clientLogEvents }, + mongoOptions: { logClientEvents }, }); await adapter.connect(); @@ -1035,7 +1035,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const logger = require('../lib/logger').logger; const warnSpy = spyOn(logger, 'warn'); - const clientLogEvents = [ + const logClientEvents = [ { name: 'circularEvent', logLevel: 'info', @@ -1044,7 +1044,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const adapter = new MongoStorageAdapter({ uri: databaseURI, - mongoOptions: { clientLogEvents }, + mongoOptions: { logClientEvents }, }); await adapter.connect(); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index f97d6f9513..a7a114367a 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -1,16 +1,16 @@ // @flow +import { format as formatUrl, parse as parseUrl } from '../../../vendor/mongodbUrl'; +import type { QueryOptions, QueryType, SchemaType, StorageClass } from '../StorageAdapter'; +import { StorageAdapter } from '../StorageAdapter'; import MongoCollection from './MongoCollection'; import MongoSchemaCollection from './MongoSchemaCollection'; -import { StorageAdapter } from '../StorageAdapter'; -import type { SchemaType, QueryType, StorageClass, QueryOptions } from '../StorageAdapter'; -import { parse as parseUrl, format as formatUrl } from '../../../vendor/mongodbUrl'; import { - parseObjectToMongoObjectForCreate, mongoObjectToParseObject, + parseObjectToMongoObjectForCreate, transformKey, - transformWhere, - transformUpdate, transformPointerString, + transformUpdate, + transformWhere, } from './MongoTransform'; // @flow-disable-next import Parse from 'parse/node'; @@ -132,7 +132,7 @@ export class MongoStorageAdapter implements StorageAdapter { _mongoOptions: Object; _onchange: any; _stream: any; - _clientLogEvents: ?Array; + _logClientEvents: ?Array; // Public connectionPromise: ?Promise; database: any; @@ -155,7 +155,7 @@ export class MongoStorageAdapter implements StorageAdapter { this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; - this._clientLogEvents = mongoOptions.clientLogEvents; + this._logClientEvents = mongoOptions.logClientEvents; // Remove Parse Server-specific options that should not be passed to MongoDB client // Note: We only delete from this._mongoOptions, not from the original mongoOptions object, // because other components (like DatabaseController) need access to these options @@ -164,7 +164,7 @@ export class MongoStorageAdapter implements StorageAdapter { 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation', - 'clientLogEvents', + 'logClientEvents', 'createIndexUserUsername', 'createIndexUserUsernameCaseInsensitive', 'createIndexUserEmail', @@ -208,8 +208,8 @@ export class MongoStorageAdapter implements StorageAdapter { }); // Set up client event logging if configured - if (this._clientLogEvents && Array.isArray(this._clientLogEvents)) { - this._clientLogEvents.forEach(eventConfig => { + if (this._logClientEvents && Array.isArray(this._logClientEvents)) { + this._logClientEvents.forEach(eventConfig => { client.on(eventConfig.name, event => { try { let logData = {}; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index a9c56dcad1..0394920306 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1117,12 +1117,6 @@ module.exports.DatabaseOptions = { 'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.', action: parsers.numberParser('autoSelectFamilyAttemptTimeout'), }, - clientLogEvents: { - env: 'PARSE_SERVER_DATABASE_CLIENT_LOG_EVENTS', - help: - "An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
", - action: parsers.arrayParser, - }, compressors: { env: 'PARSE_SERVER_DATABASE_COMPRESSORS', help: @@ -1225,6 +1219,12 @@ module.exports.DatabaseOptions = { 'The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances.', action: parsers.numberParser('localThresholdMS'), }, + logClientEvents: { + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS', + help: + "An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
", + action: parsers.arrayParser, + }, maxConnecting: { env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index 99f1bd5ac4..f9f9a1221e 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -246,7 +246,6 @@ * @property {String} authSource The MongoDB driver option to specify the database name associated with the user's credentials. * @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address. * @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. - * @property {ClientLogEvent[]} clientLogEvents An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
* @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance. * @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. * @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.

⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server. @@ -263,6 +262,7 @@ * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. * @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. * @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. + * @property {LogClientEvent[]} logClientEvents An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
* @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. * @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. diff --git a/src/Options/index.js b/src/Options/index.js index 0e1b18cd21..08e40ac35f 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -608,7 +608,7 @@ export interface FileUploadOptions { enableForPublic: ?boolean; } -export type ClientLogEvent = { +export type LogClientEvent = { /* The MongoDB driver event name to listen for. */ name: string, /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */ @@ -735,7 +735,7 @@ export interface DatabaseOptions { /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ disableIndexFieldValidation: ?boolean; /* An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
*/ - clientLogEvents: ?(ClientLogEvent[]); + logClientEvents: ?(LogClientEvent[]); } export interface AuthAdapter { From b56ea1e8515c4a890d2004f107e75be45a9e8205 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 10:50:04 +0100 Subject: [PATCH 05/11] refactor --- spec/MongoStorageAdapter.spec.js | 10 +-- spec/Utils.spec.js | 65 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 44 +++++-------- src/Utils.js | 34 ++++++++++ 4 files changed, 120 insertions(+), 33 deletions(-) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index a5375e26f3..8b14973243 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -1031,9 +1031,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { await adapter.handleShutdown(); }); - it('should handle circular references with fallback warning', async () => { + it('should handle circular references gracefully', async () => { const logger = require('../lib/logger').logger; - const warnSpy = spyOn(logger, 'warn'); + const infoSpy = spyOn(logger, 'info'); const logClientEvents = [ { @@ -1055,9 +1055,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { adapter.client.emit('circularEvent', mockEvent); - // Should fallback to warning when JSON.stringify fails - expect(warnSpy).toHaveBeenCalledWith( - jasmine.stringMatching(/MongoDB client event circularEvent logged with error:/) + // Should handle circular reference with [Circular] marker + expect(infoSpy).toHaveBeenCalledWith( + jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/) ); await adapter.handleShutdown(); diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 14747af6aa..b1277c6bfe 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -57,4 +57,69 @@ describe('Utils', () => { }); }); }); + + describe('getCircularReplacer', () => { + it('should handle Map instances', () => { + const obj = { + name: 'test', + mapData: new Map([ + ['key1', 'value1'], + ['key2', 'value2'] + ]) + }; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","mapData":{"key1":"value1","key2":"value2"}}'); + }); + + it('should handle Set instances', () => { + const obj = { + name: 'test', + setData: new Set([1, 2, 3]) + }; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","setData":[1,2,3]}'); + }); + + it('should handle circular references', () => { + const obj = { name: 'test', value: 123 }; + obj.self = obj; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","value":123,"self":"[Circular]"}'); + }); + + it('should handle nested circular references', () => { + const obj = { + name: 'parent', + child: { + name: 'child' + } + }; + obj.child.parent = obj; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"parent","child":{"name":"child","parent":"[Circular]"}}'); + }); + + it('should handle mixed Map, Set, and circular references', () => { + const obj = { + mapData: new Map([['key', 'value']]), + setData: new Set([1, 2]), + regular: 'data' + }; + obj.circular = obj; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"mapData":{"key":"value"},"setData":[1,2],"regular":"data","circular":"[Circular]"}'); + }); + + it('should handle normal objects without modification', () => { + const obj = { + name: 'test', + number: 42, + nested: { + key: 'value' + } + }; + const result = JSON.stringify(obj, Utils.getCircularReplacer()); + expect(result).toBe('{"name":"test","number":42,"nested":{"key":"value"}}'); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index a7a114367a..57f7543085 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -18,6 +18,7 @@ import Parse from 'parse/node'; import _ from 'lodash'; import defaults from '../../../defaults'; import logger from '../../../logger'; +import Utils from '../../../Utils'; // @flow-disable-next const mongodb = require('mongodb'); @@ -211,35 +212,22 @@ export class MongoStorageAdapter implements StorageAdapter { if (this._logClientEvents && Array.isArray(this._logClientEvents)) { this._logClientEvents.forEach(eventConfig => { client.on(eventConfig.name, event => { - try { - let logData = {}; - if (!eventConfig.keys || eventConfig.keys.length === 0) { - logData = event; - } else { - eventConfig.keys.forEach(keyPath => { - logData[keyPath] = _.get(event, keyPath); - }); - } - - // Validate log level exists, fallback to 'info' - const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info'; - - // Safe JSON serialization with Map/Set support - const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, (key, value) => { - if (value instanceof Map) { - return Object.fromEntries(value); - } - if (value instanceof Set) { - return Array.from(value); - } - return value; - })}`; - - logger[logLevel](logMessage); - } catch (error) { - // Fallback if serialization completely fails - logger.warn(`MongoDB client event ${eventConfig.name} logged with error: ${error.message}`); + let logData = {}; + if (!eventConfig.keys || eventConfig.keys.length === 0) { + logData = event; + } else { + eventConfig.keys.forEach(keyPath => { + logData[keyPath] = _.get(event, keyPath); + }); } + + // Validate log level exists, fallback to 'info' + const logLevel = typeof logger[eventConfig.logLevel] === 'function' ? eventConfig.logLevel : 'info'; + + // Safe JSON serialization with Map/Set and circular reference support + const logMessage = `MongoDB client event ${eventConfig.name}: ${JSON.stringify(logData, Utils.getCircularReplacer())}`; + + logger[logLevel](logMessage); }); }); } diff --git a/src/Utils.js b/src/Utils.js index 72b49aeeb2..f46e09de1e 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -410,6 +410,40 @@ class Utils { '%' + char.charCodeAt(0).toString(16).toUpperCase() ); } + + /** + * Creates a JSON replacer function that handles Map, Set, and circular references. + * This replacer can be used with JSON.stringify to safely serialize complex objects. + * + * @returns {Function} A replacer function for JSON.stringify that: + * - Converts Map instances to plain objects + * - Converts Set instances to arrays + * - Replaces circular references with '[Circular]' marker + * + * @example + * const obj = { name: 'test', map: new Map([['key', 'value']]) }; + * obj.self = obj; // circular reference + * JSON.stringify(obj, Utils.getCircularReplacer()); + * // Output: {"name":"test","map":{"key":"value"},"self":"[Circular]"} + */ + static getCircularReplacer() { + const seen = new WeakSet(); + return (key, value) => { + if (value instanceof Map) { + return Object.fromEntries(value); + } + if (value instanceof Set) { + return Array.from(value); + } + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + return value; + }; + } } module.exports = Utils; From 0dec35803defa548943a43e43233829eb44c43f0 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:42:29 +0100 Subject: [PATCH 06/11] Update README.md --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index ad51487019..036f835e23 100644 --- a/README.md +++ b/README.md @@ -754,6 +754,36 @@ Logs are also viewable in Parse Dashboard. **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` +### MongoDB Client Event Logging + +Monitor MongoDB driver behavior by logging specific client events. This is useful for debugging connection issues, tracking topology changes, and monitoring connection pool health. + +```js +const server = new ParseServer({ + databaseOptions: { + logClientEvents: [ + { + name: 'serverDescriptionChanged', + keys: ['address', 'previousDescription.type', 'newDescription.type'], + logLevel: 'warn' + }, + { + name: 'connectionPoolCleared', + keys: ['address', 'serviceId'], + logLevel: 'error' + } + ] + } +}); +``` + +Each event configuration requires: +- `name` - The MongoDB driver event (e.g., `topologyDescriptionChanged`, `connectionPoolReady`) +- `keys` - (Optional) Dot-notation paths to extract specific data; omit to log the entire event +- `logLevel` - Log level: `error`, `warn`, `info`, or `debug` + +See [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. + # Deprecations See the [Deprecation Plan](https://github.com/parse-community/parse-server/blob/master/DEPRECATIONS.md) for an overview of deprecations and planned breaking changes. From 4c1039d7fa19297c3093138156ed07e31fbc6130 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:00:45 +0100 Subject: [PATCH 07/11] fix options docs --- README.md | 30 ------------------------------ src/Options/Definitions.js | 22 ++++++++++++++++++++-- src/Options/docs.js | 9 ++++++++- src/Options/index.js | 14 +++++++------- 4 files changed, 35 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 036f835e23..ad51487019 100644 --- a/README.md +++ b/README.md @@ -754,36 +754,6 @@ Logs are also viewable in Parse Dashboard. **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` -### MongoDB Client Event Logging - -Monitor MongoDB driver behavior by logging specific client events. This is useful for debugging connection issues, tracking topology changes, and monitoring connection pool health. - -```js -const server = new ParseServer({ - databaseOptions: { - logClientEvents: [ - { - name: 'serverDescriptionChanged', - keys: ['address', 'previousDescription.type', 'newDescription.type'], - logLevel: 'warn' - }, - { - name: 'connectionPoolCleared', - keys: ['address', 'serviceId'], - logLevel: 'error' - } - ] - } -}); -``` - -Each event configuration requires: -- `name` - The MongoDB driver event (e.g., `topologyDescriptionChanged`, `connectionPoolReady`) -- `keys` - (Optional) Dot-notation paths to extract specific data; omit to log the entire event -- `logLevel` - Log level: `error`, `warn`, `info`, or `debug` - -See [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. - # Deprecations See the [Deprecation Plan](https://github.com/parse-community/parse-server/blob/master/DEPRECATIONS.md) for an overview of deprecations and planned breaking changes. diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 0394920306..68bdd00727 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1083,6 +1083,25 @@ module.exports.FileUploadOptions = { default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'], }, }; +module.exports.LogClientEvent = { + keys: { + env: 'undefinedKEYS', + help: + 'Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.', + action: parsers.arrayParser, + }, + logLevel: { + env: 'undefinedLOG_LEVEL', + help: 'The log level to use for this event.', + required: true, + }, + name: { + env: 'undefinedNAME', + help: + 'The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.', + required: true, + }, +}; module.exports.DatabaseOptions = { appName: { env: 'PARSE_SERVER_DATABASE_APP_NAME', @@ -1221,8 +1240,7 @@ module.exports.DatabaseOptions = { }, logClientEvents: { env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS', - help: - "An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
", + help: 'An array of MongoDB client event configurations to enable logging of specific events.', action: parsers.arrayParser, }, maxConnecting: { diff --git a/src/Options/docs.js b/src/Options/docs.js index f9f9a1221e..ec8e8abc69 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -238,6 +238,13 @@ * @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.

Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser. */ +/** + * @interface LogClientEvent + * @property {String[]} keys Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. + * @property {String} logLevel The log level to use for this event. + * @property {String} name The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. + */ + /** * @interface DatabaseOptions * @property {String} appName The MongoDB driver option to specify the name of the application that created this MongoClient instance. @@ -262,7 +269,7 @@ * @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment. * @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service. * @property {Number} localThresholdMS The MongoDB driver option to specify the size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. - * @property {LogClientEvent[]} logClientEvents An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
+ * @property {LogClientEvent[]} logClientEvents An array of MongoDB client event configurations to enable logging of specific events. * @property {Number} maxConnecting The MongoDB driver option to specify the maximum number of connections that may be in the process of being established concurrently by the connection pool. * @property {Number} maxIdleTimeMS The MongoDB driver option to specify the amount of time in milliseconds that a connection can remain idle in the connection pool before being removed and closed. * @property {Number} maxPoolSize The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver. diff --git a/src/Options/index.js b/src/Options/index.js index 08e40ac35f..9d99989da0 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -608,14 +608,14 @@ export interface FileUploadOptions { enableForPublic: ?boolean; } -export type LogClientEvent = { - /* The MongoDB driver event name to listen for. */ - name: string, +export interface LogClientEvent { + /* The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */ + name: string; /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */ - keys?: string[], + keys: ?(string[]); /* The log level to use for this event. */ - logLevel: string, -}; + logLevel: string; +} export interface DatabaseOptions { /* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. @@ -734,7 +734,7 @@ export interface DatabaseOptions { createIndexRoleName: ?boolean; /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ disableIndexFieldValidation: ?boolean; - /* An array of MongoDB client event configurations to enable logging of specific events. Each configuration object should contain:
  • `name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')
  • `keys` (optional array of dot-notation paths to extract specific data from the event object; if not provided or empty, the entire event object will be logged)
  • `logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).
*/ + /* An array of MongoDB client event configurations to enable logging of specific events. */ logClientEvents: ?(LogClientEvent[]); } From 3f30cfd266ba8efa60b0be74d3e04dfc921c939e Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:18:47 +0100 Subject: [PATCH 08/11] docs --- src/Options/Definitions.js | 48 +++++++++++++++++++++++++++++++++----- src/Options/docs.js | 22 ++++++++++++----- src/Options/index.js | 28 +++++++++++++++++----- 3 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 68bdd00727..959e5a5bb3 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1083,6 +1083,39 @@ module.exports.FileUploadOptions = { default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'], }, }; +/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */ +module.exports.LogLevel = { + debug: { + env: 'undefinedDEBUG', + help: 'Debug level', + required: true, + }, + error: { + env: 'undefinedERROR', + help: 'Error level - highest priority', + required: true, + }, + info: { + env: 'undefinedINFO', + help: 'Info level - default', + required: true, + }, + silly: { + env: 'undefinedSILLY', + help: 'Silly level - lowest priority', + required: true, + }, + verbose: { + env: 'undefinedVERBOSE', + help: 'Verbose level', + required: true, + }, + warn: { + env: 'undefinedWARN', + help: 'Warning level', + required: true, + }, +}; module.exports.LogClientEvent = { keys: { env: 'undefinedKEYS', @@ -1092,7 +1125,8 @@ module.exports.LogClientEvent = { }, logLevel: { env: 'undefinedLOG_LEVEL', - help: 'The log level to use for this event.', + help: + "The log level to use for this event. See [LogLevel](LogLevel.html) for available values. If an invalid level is provided, it will default to `'info'`.", required: true, }, name: { @@ -1427,30 +1461,32 @@ module.exports.AuthAdapter = { module.exports.LogLevels = { cloudFunctionError: { env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_ERROR', - help: 'Log level used by the Cloud Code Functions on error. Default is `error`.', + help: + 'Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.', default: 'error', }, cloudFunctionSuccess: { env: 'PARSE_SERVER_LOG_LEVELS_CLOUD_FUNCTION_SUCCESS', - help: 'Log level used by the Cloud Code Functions on success. Default is `info`.', + help: + 'Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, triggerAfter: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', help: - 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.', + 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, triggerBeforeError: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', help: - 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.', + 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values.', default: 'error', }, triggerBeforeSuccess: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', help: - 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.', + 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, }; diff --git a/src/Options/docs.js b/src/Options/docs.js index ec8e8abc69..b6ade61c99 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -238,10 +238,20 @@ * @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.

Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser. */ +/** + * @interface LogLevel + * @property {StringLiteral} debug Debug level + * @property {StringLiteral} error Error level - highest priority + * @property {StringLiteral} info Info level - default + * @property {StringLiteral} silly Silly level - lowest priority + * @property {StringLiteral} verbose Verbose level + * @property {StringLiteral} warn Warning level + */ + /** * @interface LogClientEvent * @property {String[]} keys Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. - * @property {String} logLevel The log level to use for this event. + * @property {String} logLevel The log level to use for this event. See [LogLevel](LogLevel.html) for available values. If an invalid level is provided, it will default to `'info'`. * @property {String} name The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */ @@ -311,9 +321,9 @@ /** * @interface LogLevels - * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. - * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. - * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. - * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. - * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. + * @property {String} cloudFunctionError Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. + * @property {String} cloudFunctionSuccess Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. + * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values. + * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. + * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. */ diff --git a/src/Options/index.js b/src/Options/index.js index 9d99989da0..5918091cbd 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -608,12 +608,28 @@ export interface FileUploadOptions { enableForPublic: ?boolean; } +/* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */ +export interface LogLevel { + /* Error level - highest priority */ + error: 'error'; + /* Warning level */ + warn: 'warn'; + /* Info level - default */ + info: 'info'; + /* Verbose level */ + verbose: 'verbose'; + /* Debug level */ + debug: 'debug'; + /* Silly level - lowest priority */ + silly: 'silly'; +} + export interface LogClientEvent { /* The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */ name: string; /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */ keys: ?(string[]); - /* The log level to use for this event. */ + /* The log level to use for this event. See [LogLevel](LogLevel.html) for available values. If an invalid level is provided, it will default to `'info'`. */ logLevel: string; } @@ -747,23 +763,23 @@ export interface AuthAdapter { } export interface LogLevels { - /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. + /* Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: info */ triggerAfter: ?string; - /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. + /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: info */ triggerBeforeSuccess: ?string; - /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. + /* Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: error */ triggerBeforeError: ?string; - /* Log level used by the Cloud Code Functions on success. Default is `info`. + /* Log level used by the Cloud Code Functions on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: info */ cloudFunctionSuccess: ?string; - /* Log level used by the Cloud Code Functions on error. Default is `error`. + /* Log level used by the Cloud Code Functions on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: error */ cloudFunctionError: ?string; From 0576cb2f54abc7427b919204c1e907176f41368d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:20:34 +0100 Subject: [PATCH 09/11] docs --- src/Options/Definitions.js | 4 ++-- src/Options/docs.js | 2 +- src/Options/index.js | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 959e5a5bb3..22c156d9ae 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1126,8 +1126,8 @@ module.exports.LogClientEvent = { logLevel: { env: 'undefinedLOG_LEVEL', help: - "The log level to use for this event. See [LogLevel](LogLevel.html) for available values. If an invalid level is provided, it will default to `'info'`.", - required: true, + "The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.", + default: 'info', }, name: { env: 'undefinedNAME', diff --git a/src/Options/docs.js b/src/Options/docs.js index b6ade61c99..d3e1f258d9 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -251,7 +251,7 @@ /** * @interface LogClientEvent * @property {String[]} keys Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. - * @property {String} logLevel The log level to use for this event. See [LogLevel](LogLevel.html) for available values. If an invalid level is provided, it will default to `'info'`. + * @property {String} logLevel The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`. * @property {String} name The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events. */ diff --git a/src/Options/index.js b/src/Options/index.js index 5918091cbd..090dba62e7 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -629,8 +629,9 @@ export interface LogClientEvent { name: string; /* Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged. */ keys: ?(string[]); - /* The log level to use for this event. See [LogLevel](LogLevel.html) for available values. If an invalid level is provided, it will default to `'info'`. */ - logLevel: string; + /* The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`. + :DEFAULT: info */ + logLevel: ?string; } export interface DatabaseOptions { From 431af6af271d7c72eeb5654b6745264103e1c471 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:26:50 +0100 Subject: [PATCH 10/11] docs docs logo --- jsdoc-conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsdoc-conf.json b/jsdoc-conf.json index b410d239b0..e90f82556b 100644 --- a/jsdoc-conf.json +++ b/jsdoc-conf.json @@ -30,7 +30,7 @@ "theme_opts": { "default_theme": "dark", "title": "", - "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }" + "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px; height: auto; max-width: 100%; object-fit: contain; }" } }, "markdown": { From c795b318bb7480ee6f3494f57adf93c1504e3c9d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:38:10 +0100 Subject: [PATCH 11/11] fix env --- resources/buildConfigDefinitions.js | 8 +++++--- src/Options/Definitions.js | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 5b9084f863..f4813975f4 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -36,15 +36,17 @@ const nestedOptionEnvPrefix = { IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_', LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_', LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_', + LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_', + LogLevel: 'PARSE_SERVER_LOG_LEVEL_', + LogLevels: 'PARSE_SERVER_LOG_LEVELS_', PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_', PagesOptions: 'PARSE_SERVER_PAGES_', PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', ParseServerOptions: 'PARSE_SERVER_', PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_', - SecurityOptions: 'PARSE_SERVER_SECURITY_', - SchemaOptions: 'PARSE_SERVER_SCHEMA_', - LogLevels: 'PARSE_SERVER_LOG_LEVELS_', RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_', + SchemaOptions: 'PARSE_SERVER_SCHEMA_', + SecurityOptions: 'PARSE_SERVER_SECURITY_', }; function last(array) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 22c156d9ae..6930020de7 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1086,51 +1086,51 @@ module.exports.FileUploadOptions = { /* The available log levels for Parse Server logging. Valid values are:
- `'error'` - Error level (highest priority)
- `'warn'` - Warning level
- `'info'` - Info level (default)
- `'verbose'` - Verbose level
- `'debug'` - Debug level
- `'silly'` - Silly level (lowest priority) */ module.exports.LogLevel = { debug: { - env: 'undefinedDEBUG', + env: 'PARSE_SERVER_LOG_LEVEL_DEBUG', help: 'Debug level', required: true, }, error: { - env: 'undefinedERROR', + env: 'PARSE_SERVER_LOG_LEVEL_ERROR', help: 'Error level - highest priority', required: true, }, info: { - env: 'undefinedINFO', + env: 'PARSE_SERVER_LOG_LEVEL_INFO', help: 'Info level - default', required: true, }, silly: { - env: 'undefinedSILLY', + env: 'PARSE_SERVER_LOG_LEVEL_SILLY', help: 'Silly level - lowest priority', required: true, }, verbose: { - env: 'undefinedVERBOSE', + env: 'PARSE_SERVER_LOG_LEVEL_VERBOSE', help: 'Verbose level', required: true, }, warn: { - env: 'undefinedWARN', + env: 'PARSE_SERVER_LOG_LEVEL_WARN', help: 'Warning level', required: true, }, }; module.exports.LogClientEvent = { keys: { - env: 'undefinedKEYS', + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_KEYS', help: 'Optional array of dot-notation paths to extract specific data from the event object. If not provided or empty, the entire event object will be logged.', action: parsers.arrayParser, }, logLevel: { - env: 'undefinedLOG_LEVEL', + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_LOG_LEVEL', help: "The log level to use for this event. See [LogLevel](LogLevel.html) for available values. Defaults to `'info'`.", default: 'info', }, name: { - env: 'undefinedNAME', + env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_NAME', help: 'The MongoDB driver event name to listen for. See the [MongoDB driver events documentation](https://www.mongodb.com/docs/drivers/node/current/fundamentals/monitoring/) for available events.', required: true, @@ -1276,6 +1276,7 @@ module.exports.DatabaseOptions = { env: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS', help: 'An array of MongoDB client event configurations to enable logging of specific events.', action: parsers.arrayParser, + type: 'LogClientEvent[]', }, maxConnecting: { env: 'PARSE_SERVER_DATABASE_MAX_CONNECTING',