diff --git a/src/connection_string.ts b/src/connection_string.ts index 28d1c1fbc3..fdc4e47ab2 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -23,9 +23,11 @@ import { ServerApiVersion } from './mongo_client'; import { + MongoLoggableComponent, MongoLogger, type MongoLoggerEnvOptions, - type MongoLoggerMongoClientOptions + type MongoLoggerMongoClientOptions, + SeverityLevel } from './mongo_logger'; import { ReadConcern, type ReadConcernLevel } from './read_concern'; import { ReadPreference, type ReadPreferenceMode } from './read_preference'; @@ -1246,12 +1248,53 @@ export const OPTIONS = { * @internal * TODO: NODE-5671 - remove internal flag */ - mongodbLogPath: { type: 'any' }, + mongodbLogPath: { + transform({ values: [value] }) { + if ( + !( + (typeof value === 'string' && ['stderr', 'stdout'].includes(value)) || + (value && + typeof value === 'object' && + 'write' in value && + typeof value.write === 'function') + ) + ) { + throw new MongoAPIError( + `Option 'mongodbLogPath' must be of type 'stderr' | 'stdout' | MongoDBLogWritable` + ); + } + return value; + } + }, /** * @internal * TODO: NODE-5671 - remove internal flag */ - mongodbLogComponentSeverities: { type: 'any' }, + mongodbLogComponentSeverities: { + transform({ values: [value] }) { + if (typeof value !== 'object' || !value) { + throw new MongoAPIError(`Option 'mongodbLogComponentSeverities' must be a non-null object`); + } + for (const [k, v] of Object.entries(value)) { + if (typeof v !== 'string' || typeof k !== 'string') { + throw new MongoAPIError( + `User input for option 'mongodbLogComponentSeverities' object cannot include a non-string key or value` + ); + } + if (!Object.values(MongoLoggableComponent).some(val => val === k) && k !== 'default') { + throw new MongoAPIError( + `User input for option 'mongodbLogComponentSeverities' contains invalid key: ${k}` + ); + } + if (!Object.values(SeverityLevel).some(val => val === v)) { + throw new MongoAPIError( + `Option 'mongodbLogComponentSeverities' does not support ${v} as a value for ${k}` + ); + } + } + return value; + } + }, /** * @internal * TODO: NODE-5671 - remove internal flag diff --git a/src/mongo_logger.ts b/src/mongo_logger.ts index 390179bf4d..6ffcee304a 100644 --- a/src/mongo_logger.ts +++ b/src/mongo_logger.ts @@ -281,7 +281,10 @@ export interface Log extends Record { message?: string; } -/** @internal */ +/** + * @internal + * TODO: NODE-5671 - remove internal flag and add API comments + */ export interface MongoDBLogWritable { write(log: Log): PromiseLike | unknown; } diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 7f86d1a677..30ba650d6d 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -912,13 +912,11 @@ describe('Connection String', function () { }); context('when option is invalid', function () { - it('it defaults to stderr', function () { - const client = new MongoClient('mongodb://a/?mongodbLogPath=stdnothing', { - [loggerFeatureFlag]: true - }); - const log: Log = { t: new Date(), c: 'ConnectionStringInvalidOption', s: 'error' }; - client.options.mongoLoggerOptions.logDestination.write(log); - expect(stderrStub.write).calledWith(inspect(log, { breakLength: Infinity, compact: true })); + it('should throw error at construction', function () { + expect( + () => + new MongoClient('mongodb://a/?mongodbLogPath=stdnothing', { [loggerFeatureFlag]: true }) + ).to.throw(MongoAPIError); }); }); }); diff --git a/test/unit/mongo_client.test.js b/test/unit/mongo_client.test.js index 96fb1aee81..d325b07356 100644 --- a/test/unit/mongo_client.test.js +++ b/test/unit/mongo_client.test.js @@ -11,7 +11,7 @@ const { ReadConcern } = require('../mongodb'); const { WriteConcern } = require('../mongodb'); const { ReadPreference } = require('../mongodb'); const { MongoCredentials } = require('../mongodb'); -const { MongoClient, MongoParseError, ServerApiVersion } = require('../mongodb'); +const { MongoClient, MongoParseError, ServerApiVersion, MongoAPIError } = require('../mongodb'); const { MongoLogger } = require('../mongodb'); // eslint-disable-next-line no-restricted-modules const { SeverityLevel, MongoLoggableComponent } = require('../../src/mongo_logger'); @@ -821,71 +821,114 @@ describe('MongoOptions', function () { describe('logging client options', function () { const loggerFeatureFlag = Symbol.for('@@mdb.enableMongoLogger'); - context('when mongodbLogPath is in options', function () { - let stderrStub; - let stdoutStub; + describe('mongodbLogPath', function () { + context('when mongodbLogPath is in options', function () { + 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 () { + context(`when option is an string that is not 'stderr' or 'stdout'`, function () { + it('should throw error at construction', function () { + const invalidOption = 'stdnothing'; + expect( + () => + new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogPath: invalidOption + }) + ).to.throw(MongoAPIError); + }); + }); + context('when option is not a valid MongoDBLogWritable stream', function () { + it('should throw error at construction', function () { + const writable = { + buffer: [], + misnamedWrite(log) { + this.buffer.push(log); + } + }; + + expect( + () => + new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogPath: writable + }) + ).to.throw(MongoAPIError); + }); }); - 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'; + context('when mongodbLogPath is not in options', function () { + let stderrStub; + + beforeEach(() => { + stderrStub = sinon.stub(process.stderr); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should default to stderr', function () { const client = new MongoClient('mongodb://a/', { - [loggerFeatureFlag]: true, - mongodbLogPath: invalidOption + [loggerFeatureFlag]: true }); - const log = { t: new Date(), c: 'constructorInvalidOption', s: 'error' }; + 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 }) @@ -893,7 +936,7 @@ describe('MongoOptions', function () { }); }); }); - describe('component severities', function () { + describe('mongodbLogComponentSeverities', function () { const components = Object.values(MongoLoggableComponent); const env_component_names = [ 'MONGODB_LOG_COMMAND', @@ -976,35 +1019,162 @@ describe('MongoOptions', function () { } }); }); - }); - 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 + + describe('invalid values', function () { + context('when invalid client option is provided', function () { + const badClientCreator = () => + new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogComponentSeverities: { + default: 'imFake' + } + }); + + context('when valid environment option is provided', function () { + it('should still throw error at construction', function () { + process.env.MONGODB_LOG_ALL = 'emergency'; + expect(badClientCreator).to.throw(MongoAPIError); + delete process.env.MONGODB_LOG_ALL; + }); + }); + + context('when environment option is not provided', function () { + it('should still throw error at construction', function () { + expect(badClientCreator).to.throw(MongoAPIError); + }); + }); + + context('when invalid environment option is provided', function () { + afterEach(function () { + delete process.env.MONGODB_LOG_ALL; + }); + + it('should still throw error at construction', function () { + process.env.MONGODB_LOG_ALL = 'imFakeToo'; + expect(badClientCreator).to.throw(MongoAPIError); + }); }); - expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(290); }); - it('it throws error for negative input', function () { - expect( - () => - new MongoClient('mongodb://a/', { + + context('when invalid environment option is provided', function () { + beforeEach(async function () { + process.env.MONGODB_LOG_ALL = 'imFakeToo'; + }); + afterEach(async function () { + delete process.env.MONGODB_LOG_ALL; + }); + + context('when client option is not provided', function () { + it(`should not throw error, and set component severity to 'off'`, function () { + expect( + () => + new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogComponentSeverities: {} + }) + ).to.not.throw(MongoAPIError); + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogComponentSeverities: {} + }); + expect(client.mongoLogger.componentSeverities.default).to.equal('off'); + }); + }); + context('when valid client option is provided', function () { + it('should not throw error, and set component severity to client value', function () { + expect( + () => + new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogComponentSeverities: { default: 'emergency' } + }) + ).to.not.throw(MongoAPIError); + const client = new MongoClient('mongodb://a/', { [loggerFeatureFlag]: true, - mongodbLogMaxDocumentLength: -290 - }) - ).to.throw(MongoParseError); + mongodbLogComponentSeverities: { default: 'emergency' } + }); + expect(client.mongoLogger.componentSeverities.default).to.equal('emergency'); + }); + }); }); }); - 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 + }); + describe('mongodbLogMaxDocumentLength', function () { + context('when mongodbLogMaxDocumentLength is in options', function () { + context('when env option for MONGODB_LOG_MAX_DOCUMENT_LENGTH is not provided', function () { + it('should store value for maxDocumentLength correctly', function () { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogMaxDocumentLength: 290 + }); + expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(290); + }); + it('should throw 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 () { + beforeEach(function () { + process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH = '155'; + }); + + afterEach(function () { + delete process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH; + }); + + it('should store value for maxDocumentLength correctly (client option value takes precedence)', function () { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogMaxDocumentLength: 290 + }); + expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(290); + }); + it('should throw error for negative input', function () { + expect( + () => + new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true, + mongodbLogMaxDocumentLength: -290 + }) + ).to.throw(MongoParseError); + }); + }); + }); + context('when mongodbLogMaxDocumentLength is not in options', function () { + context('when env option for MONGODB_LOG_MAX_DOCUMENT_LENGTH is not provided', function () { + it('should store value for default maxDocumentLength correctly', function () { + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true + }); + expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(1000); + }); + }); + context('when env option for MONGODB_LOG_MAX_DOCUMENT_LENGTH is provided', function () { + afterEach(function () { + delete process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH; + }); + + it('should store value for maxDocumentLength correctly', function () { + process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH = '155'; + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true + }); + expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(155); + }); + + it('should not throw error for negative MONGODB_MAX_DOCUMENT_LENGTH and set to default', function () { + process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH = '-14'; + const client = new MongoClient('mongodb://a/', { + [loggerFeatureFlag]: true + }); + expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(1000); }); - expect(client.options.mongoLoggerOptions.maxDocumentLength).to.equal(290); - process.env['MONGODB_LOG_MAX_DOCUMENT_LENGTH'] = undefined; }); }); });