Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
108 changes: 108 additions & 0 deletions spec/MongoStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
28 changes: 28 additions & 0 deletions src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export class MongoStorageAdapter implements StorageAdapter {
_mongoOptions: Object;
_onchange: any;
_stream: any;
_clientLogEvents: ?Array<any>;
// Public
connectionPromise: ?Promise<any>;
database: any;
Expand All @@ -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
Expand All @@ -162,6 +164,7 @@ export class MongoStorageAdapter implements StorageAdapter {
'schemaCacheTtl',
'maxTimeMS',
'disableIndexFieldValidation',
'clientLogEvents',
'createIndexUserUsername',
'createIndexUserUsernameCaseInsensitive',
'createIndexUserEmail',
Expand Down Expand Up @@ -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;
})
Expand Down
6 changes: 6 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br><ul><li>`name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')</li><li>`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)</li><li>`logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).</li></ul>",
action: parsers.arrayParser,
},
compressors: {
env: 'PARSE_SERVER_DATABASE_COMPRESSORS',
help:
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br><ul><li>`name` (the event name, e.g., 'topologyDescriptionChanged', 'serverDescriptionChanged', 'connectionPoolCleared', 'connectionPoolReady')</li><li>`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)</li><li>`logLevel` (the log level to use for this event: 'error', 'warn', 'info', 'debug', etc.).</li></ul> */
clientLogEvents: ?(any[]);
}

export interface AuthAdapter {
Expand Down
Loading