diff --git a/src/connection_string.ts b/src/connection_string.ts index 1d178d3b83..c25753ece7 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -530,7 +530,9 @@ export function parseOptions( ...mongoOptions[Symbol.for('@@mdb.internalLoggerConfig')] }; loggerClientOptions = { - mongodbLogPath: mongoOptions.mongodbLogPath + mongodbLogPath: mongoOptions.mongodbLogPath, + mongodbLogComponentSeverities: mongoOptions.mongodbLogComponentSeverities, + mongodbLogMaxDocumentLength: mongoOptions.mongodbLogMaxDocumentLength }; } mongoOptions.mongoLoggerOptions = MongoLogger.resolveOptions( @@ -1229,7 +1231,17 @@ export const OPTIONS = { * @internal * TODO: NODE-5671 - remove internal flag */ - mongodbLogPath: { type: 'any' } + mongodbLogPath: { type: 'any' }, + /** + * @internal + * TODO: NODE-5671 - remove internal flag + */ + mongodbLogComponentSeverities: { type: 'any' }, + /** + * @internal + * TODO: NODE-5671 - remove internal flag + */ + mongodbLogMaxDocumentLength: { type: 'uint' } } as Record; export const DEFAULT_OPTIONS = new CaseInsensitiveMap( diff --git a/src/index.ts b/src/index.ts index 0c013a3b3b..c6ddc2bc9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -353,6 +353,7 @@ export type { } from './mongo_client'; export type { Log, + LogComponentSeveritiesClientOptions, LogConvertible, Loggable, LoggableEvent, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 1a06f250d3..9bccf66ebe 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -21,7 +21,12 @@ import { MONGO_CLIENT_EVENTS } from './constants'; import { Db, type DbOptions } from './db'; import type { Encrypter } from './encrypter'; import { MongoInvalidArgumentError } from './error'; -import { type MongoDBLogWritable, MongoLogger, type MongoLoggerOptions } from './mongo_logger'; +import { + type LogComponentSeveritiesClientOptions, + type MongoDBLogWritable, + MongoLogger, + type MongoLoggerOptions +} from './mongo_logger'; import { TypedEventEmitter } from './mongo_types'; import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; @@ -262,6 +267,16 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC * TODO: NODE-5671 - remove internal flag */ mongodbLogPath?: 'stderr' | 'stdout' | MongoDBLogWritable; + /** + * @internal + * TODO: NODE-5671 - remove internal flag + */ + mongodbLogComponentSeverities?: LogComponentSeveritiesClientOptions; + /** + * @internal + * TODO: NODE-5671 - remove internal flag + */ + mongodbLogMaxDocumentLength?: number; /** @internal */ [featureFlag: symbol]: any; diff --git a/src/mongo_logger.ts b/src/mongo_logger.ts index 3e26d40369..27560ac0d6 100644 --- a/src/mongo_logger.ts +++ b/src/mongo_logger.ts @@ -126,10 +126,30 @@ export interface MongoLoggerEnvOptions { MONGODB_LOG_PATH?: string; } +/** @internal */ +export interface LogComponentSeveritiesClientOptions { + /** Optional severity level for command component */ + command?: SeverityLevel; + /** Optional severity level for topology component */ + topology?: SeverityLevel; + /** Optionsl severity level for server selection component */ + serverSelection?: SeverityLevel; + /** Optional severity level for connection component */ + connection?: SeverityLevel; + /** Optional severity level for client component */ + client?: SeverityLevel; + /** Optional default severity level to be used if any of the above are unset */ + default?: SeverityLevel; +} + /** @internal */ export interface MongoLoggerMongoClientOptions { /** Destination for log messages */ mongodbLogPath?: 'stdout' | 'stderr' | MongoDBLogWritable; + /** Severity levels for logger components */ + mongodbLogComponentSeverities?: LogComponentSeveritiesClientOptions; + /** Max length of embedded EJSON docs. Setting to 0 disables truncation. Defaults to 1000. */ + mongodbLogMaxDocumentLength?: number; } /** @internal */ @@ -148,7 +168,6 @@ export interface MongoLoggerOptions { /** Default severity level to be used if any of the above are unset */ default: SeverityLevel; }; - /** Max length of embedded EJSON docs. Setting to 0 disables truncation. Defaults to 1000. */ maxDocumentLength: number; /** Destination for log messages. */ @@ -219,6 +238,18 @@ function resolveLogPath( return createStdioLogger(process.stderr); } +function resolveSeverityConfiguration( + clientOption: string | undefined, + environmentOption: string | undefined, + defaultSeverity: SeverityLevel +): SeverityLevel { + return ( + parseSeverityFromString(clientOption) ?? + parseSeverityFromString(environmentOption) ?? + defaultSeverity + ); +} + /** @internal */ export interface Log extends Record { t: Date; @@ -522,22 +553,45 @@ export class MongoLogger { ...clientOptions, mongodbLogPath: resolveLogPath(envOptions, clientOptions) }; - const defaultSeverity = - parseSeverityFromString(combinedOptions.MONGODB_LOG_ALL) ?? SeverityLevel.OFF; + const defaultSeverity = resolveSeverityConfiguration( + combinedOptions.mongodbLogComponentSeverities?.default, + combinedOptions.MONGODB_LOG_ALL, + SeverityLevel.OFF + ); return { componentSeverities: { - command: parseSeverityFromString(combinedOptions.MONGODB_LOG_COMMAND) ?? defaultSeverity, - topology: parseSeverityFromString(combinedOptions.MONGODB_LOG_TOPOLOGY) ?? defaultSeverity, - serverSelection: - parseSeverityFromString(combinedOptions.MONGODB_LOG_SERVER_SELECTION) ?? defaultSeverity, - connection: - parseSeverityFromString(combinedOptions.MONGODB_LOG_CONNECTION) ?? defaultSeverity, - client: parseSeverityFromString(combinedOptions.MONGODB_LOG_CLIENT) ?? defaultSeverity, + command: resolveSeverityConfiguration( + combinedOptions.mongodbLogComponentSeverities?.command, + combinedOptions.MONGODB_LOG_COMMAND, + defaultSeverity + ), + topology: resolveSeverityConfiguration( + combinedOptions.mongodbLogComponentSeverities?.topology, + combinedOptions.MONGODB_LOG_TOPOLOGY, + defaultSeverity + ), + serverSelection: resolveSeverityConfiguration( + combinedOptions.mongodbLogComponentSeverities?.serverSelection, + combinedOptions.MONGODB_LOG_SERVER_SELECTION, + defaultSeverity + ), + connection: resolveSeverityConfiguration( + combinedOptions.mongodbLogComponentSeverities?.connection, + combinedOptions.MONGODB_LOG_CONNECTION, + defaultSeverity + ), + client: resolveSeverityConfiguration( + combinedOptions.mongodbLogComponentSeverities?.client, + combinedOptions.MONGODB_LOG_CLIENT, + defaultSeverity + ), default: defaultSeverity }, maxDocumentLength: - parseUnsignedInteger(combinedOptions.MONGODB_LOG_MAX_DOCUMENT_LENGTH) ?? 1000, + combinedOptions.mongodbLogMaxDocumentLength ?? + parseUnsignedInteger(combinedOptions.MONGODB_LOG_MAX_DOCUMENT_LENGTH) ?? + 1000, logDestination: combinedOptions.mongodbLogPath }; } diff --git a/test/unit/mongo_client.test.js b/test/unit/mongo_client.test.js index 8757146baf..96fb1aee81 100644 --- a/test/unit/mongo_client.test.js +++ b/test/unit/mongo_client.test.js @@ -13,6 +13,8 @@ const { ReadPreference } = require('../mongodb'); const { MongoCredentials } = require('../mongodb'); const { MongoClient, MongoParseError, ServerApiVersion } = require('../mongodb'); const { MongoLogger } = require('../mongodb'); +// eslint-disable-next-line no-restricted-modules +const { SeverityLevel, MongoLoggableComponent } = require('../../src/mongo_logger'); const sinon = require('sinon'); const { Writable } = require('stream'); @@ -817,71 +819,193 @@ describe('MongoOptions', function () { }); }); - context('when mongodbLogPath is in options', function () { + describe('logging client options', function () { const loggerFeatureFlag = Symbol.for('@@mdb.enableMongoLogger'); + context('when mongodbLogPath is in options', function () { + let stderrStub; + let stdoutStub; - let stderrStub; - let stdoutStub; + beforeEach(() => { + stdoutStub = sinon.stub(process.stdout); + stderrStub = sinon.stub(process.stderr); + }); - beforeEach(() => { - stdoutStub = sinon.stub(process.stdout); - stderrStub = sinon.stub(process.stderr); - }); + afterEach(() => { + sinon.restore(); + }); - afterEach(() => { - sinon.restore(); - }); + context('when option is `stderr`', function () { + it('it is accessible through mongoLogger.logDestination', function () { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogPath: 'stderr' + }); + const log = { t: new Date(), c: 'constructorStdErr', s: 'error' }; + client.options.mongoLoggerOptions.logDestination.write(log); + expect(stderrStub.write).calledWith( + inspect(log, { breakLength: Infinity, compact: true }) + ); + }); + }); - context('when option is `stderr`', function () { - it('it is accessible through mongoLogger.logDestination', function () { - const client = new MongoClient('mongodb://a/', { - [loggerFeatureFlag]: true, - mongodbLogPath: 'stderr' + context('when option is a MongoDBLogWritable stream', function () { + it('it is accessible through mongoLogger.logDestination', function () { + const writable = { + buffer: [], + write(log) { + this.buffer.push(log); + } + }; + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogPath: writable + }); + expect(client.options.mongoLoggerOptions.logDestination).to.deep.equal(writable); }); - const log = { t: new Date(), c: 'constructorStdErr', s: 'error' }; - client.options.mongoLoggerOptions.logDestination.write(log); - expect(stderrStub.write).calledWith(inspect(log, { breakLength: Infinity, compact: true })); }); - }); - context('when option is a MongoDBLogWritable stream', function () { - it('it is accessible through mongoLogger.logDestination', function () { - const writable = { - buffer: [], - write(log) { - this.buffer.push(log); - } - }; - const client = new MongoClient('mongodb://a/', { - [loggerFeatureFlag]: true, - mongodbLogPath: writable + context('when option is `stdout`', function () { + it('it is accessible through mongoLogger.logDestination', function () { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogPath: 'stdout' + }); + const log = { t: new Date(), c: 'constructorStdOut', s: 'error' }; + client.options.mongoLoggerOptions.logDestination.write(log); + expect(stdoutStub.write).calledWith( + inspect(log, { breakLength: Infinity, compact: true }) + ); }); - expect(client.options.mongoLoggerOptions.logDestination).to.deep.equal(writable); }); - }); - context('when option is `stdout`', function () { - it('it is accessible through mongoLogger.logDestination', function () { - const client = new MongoClient('mongodb://a/', { - [loggerFeatureFlag]: true, - mongodbLogPath: 'stdout' + context('when option is invalid', function () { + it('it defaults to stderr', function () { + const invalidOption = 'stdnothing'; + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogPath: invalidOption + }); + const log = { t: new Date(), c: 'constructorInvalidOption', s: 'error' }; + client.options.mongoLoggerOptions.logDestination.write(log); + expect(stderrStub.write).calledWith( + inspect(log, { breakLength: Infinity, compact: true }) + ); }); - const log = { t: new Date(), c: 'constructorStdOut', s: 'error' }; - client.options.mongoLoggerOptions.logDestination.write(log); - expect(stdoutStub.write).calledWith(inspect(log, { breakLength: Infinity, compact: true })); }); }); - - context('when option is invalid', function () { - it('it defaults to stderr', function () { - const invalidOption = 'stdnothing'; - const client = new MongoClient('mongodb://a/', { - [loggerFeatureFlag]: true, - mongodbLogPath: invalidOption + describe('component severities', function () { + const components = Object.values(MongoLoggableComponent); + const env_component_names = [ + 'MONGODB_LOG_COMMAND', + 'MONGODB_LOG_TOPOLOGY', + 'MONGODB_LOG_SERVER_SELECTION', + 'MONGODB_LOG_CONNECTION', + 'MONGODB_LOG_CLIENT' + ]; + context('when only client option is provided', function () { + for (let i = 0; i < components.length; i++) { + it(`it stores severity levels for ${components[i]} component correctly`, function () { + for (const severityLevel of Object.values(SeverityLevel)) { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogComponentSeverities: { + [components[i]]: severityLevel + } + }); + for (const [curComponent, curSeverity] of Object.entries( + client.options.mongoLoggerOptions.componentSeverities + )) { + if (curComponent === components[i]) { + expect(curSeverity).to.equal(severityLevel); + } else { + expect(curSeverity).to.equal(SeverityLevel.OFF); + } + } + } + }); + } + }); + context('when both client and environment option are provided', function () { + for (let i = 0; i < components.length; i++) { + it(`it stores severity level for ${components[i]} component correctly (client options have precedence)`, function () { + process.env[env_component_names[i]] = 'emergency'; + for (const severityLevel of Object.values(SeverityLevel)) { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogComponentSeverities: { + [components[i]]: severityLevel + } + }); + for (const [curComponent, curSeverity] of Object.entries( + client.options.mongoLoggerOptions.componentSeverities + )) { + if (curComponent === components[i]) { + expect(curSeverity).to.equal(severityLevel); + } else { + expect(curSeverity).to.equal(SeverityLevel.OFF); + } + } + process.env[env_component_names[i]] = undefined; + } + }); + } + }); + context('when default is provided', function () { + it('unspecified components have default value, while specified components retain value', function () { + for (let i = 0; i < components.length; i++) { + for (const severityLevel of Object.values(SeverityLevel)) { + for (const defaultSeverityLevel of Object.values(SeverityLevel)) { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogComponentSeverities: { + [components[i]]: severityLevel, + default: defaultSeverityLevel + } + }); + for (const [curComponent, curSeverity] of Object.entries( + client.options.mongoLoggerOptions.componentSeverities + )) { + if (curComponent === components[i]) { + expect(curSeverity).to.equal(severityLevel); + } else { + expect(curSeverity).to.equal(defaultSeverityLevel); + } + } + } + } + } + }); + }); + }); + context('when mongodbLogMaxDocumentLength is in options', function () { + context('when env option for MONGODB_LOG_MAX_DOCUMENT_LENGTH is not provided', function () { + it('it stores value for maxDocumentLength correctly', function () { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogMaxDocumentLength: 290 + }); + expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(290); + }); + it('it throws error for negative input', function () { + expect( + () => + new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogMaxDocumentLength: -290 + }) + ).to.throw(MongoParseError); + }); + }); + context('when env option for MONGODB_LOG_MAX_DOCUMENT_LENGTH is provided', function () { + it('it stores value for maxDocumentLength correctly (client option value takes precedence)', function () { + process.env['MONGODB_LOG_MAX_DOCUMENT_LENGTH'] = '155'; + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogMaxDocumentLength: 290 + }); + expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(290); + process.env['MONGODB_LOG_MAX_DOCUMENT_LENGTH'] = undefined; }); - const log = { t: new Date(), c: 'constructorInvalidOption', s: 'error' }; - client.options.mongoLoggerOptions.logDestination.write(log); - expect(stderrStub.write).calledWith(inspect(log, { breakLength: Infinity, compact: true })); }); }); });