feat: Add MongoDB client event logging via database option logClientEvents (#9914)

This commit is contained in:
Manuel
2025-11-08 15:48:29 +01:00
committed by GitHub
parent 2424054221
commit b760733b98
9 changed files with 501 additions and 25 deletions

View File

@@ -824,4 +824,243 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
});
});
describe('logClientEvents', () => {
it('should log MongoDB client events when configured', async () => {
const logger = require('../lib/logger').logger;
const logSpy = spyOn(logger, 'warn');
const logClientEvents = [
{
name: 'serverDescriptionChanged',
keys: ['address'],
logLevel: 'warn',
},
];
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});
// 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 logClientEvents = [
{
name: 'connectionPoolReady',
logLevel: 'info',
},
];
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});
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 logClientEvents = [
{
name: 'topologyDescriptionChanged',
keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'],
logLevel: 'warn',
},
];
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});
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();
});
it('should handle invalid log level gracefully', async () => {
const logger = require('../lib/logger').logger;
const infoSpy = spyOn(logger, 'info');
const logClientEvents = [
{
name: 'connectionPoolReady',
keys: ['address'],
logLevel: 'invalidLogLevel', // Invalid log level
},
];
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});
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 logClientEvents = [
{
name: 'customEvent',
logLevel: 'warn',
},
];
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});
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 logClientEvents = [
{
name: 'testEvent',
keys: ['nonexistent.nested.key', 'another.missing'],
logLevel: 'info',
},
];
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});
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 gracefully', async () => {
const logger = require('../lib/logger').logger;
const infoSpy = spyOn(logger, 'info');
const logClientEvents = [
{
name: 'circularEvent',
logLevel: 'info',
},
];
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { logClientEvents },
});
await adapter.connect();
// Create circular reference
const mockEvent = { name: 'test' };
mockEvent.self = mockEvent;
adapter.client.emit('circularEvent', mockEvent);
// Should handle circular reference with [Circular] marker
expect(infoSpy).toHaveBeenCalledWith(
jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/)
);
await adapter.handleShutdown();
});
});
});

View File

@@ -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"}}');
});
});
});