diff --git a/src/connection_string.ts b/src/connection_string.ts index 30d23a6df9..a1a177433d 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -14,7 +14,7 @@ import { MongoMissingCredentialsError, MongoParseError } from './error'; -import { Logger, LoggerLevel } from './logger'; +import { Logger as LegacyLogger, LoggerLevel as LegacyLoggerLevel } from './logger'; import { DriverInfo, MongoClient, @@ -24,6 +24,7 @@ import { ServerApi, ServerApiVersion } from './mongo_client'; +import { MongoLogger, MongoLoggerEnvOptions, MongoLoggerMongoClientOptions } from './mongo_logger'; import { PromiseProvider } from './promise_provider'; import { ReadConcern, ReadConcernLevel } from './read_concern'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; @@ -35,6 +36,7 @@ import { HostAddress, isRecord, makeClientMetadata, + parseInteger, setDifference } from './utils'; import { W, WriteConcern } from './write_concern'; @@ -199,15 +201,16 @@ function getBoolean(name: string, value: unknown): boolean { throw new MongoParseError(`Expected ${name} to be stringified boolean value, got: ${value}`); } -function getInt(name: string, value: unknown): number { - if (typeof value === 'number') return Math.trunc(value); - const parsedValue = Number.parseInt(String(value), 10); - if (!Number.isNaN(parsedValue)) return parsedValue; +function getIntFromOptions(name: string, value: unknown): number { + const parsedInt = parseInteger(value); + if (parsedInt != null) { + return parsedInt; + } throw new MongoParseError(`Expected ${name} to be stringified int value, got: ${value}`); } -function getUint(name: string, value: unknown): number { - const parsedValue = getInt(name, value); +function getUIntFromOptions(name: string, value: unknown): number { + const parsedValue = getIntFromOptions(name, value); if (parsedValue < 0) { throw new MongoParseError(`${name} can only be a positive int value, got: ${value}`); } @@ -507,6 +510,30 @@ export function parseOptions( ); } + const loggerFeatureFlag = Symbol.for('@@mdb.enableMongoLogger'); + mongoOptions[loggerFeatureFlag] = mongoOptions[loggerFeatureFlag] ?? false; + + let loggerEnvOptions: MongoLoggerEnvOptions = {}; + let loggerClientOptions: MongoLoggerMongoClientOptions = {}; + if (mongoOptions[loggerFeatureFlag]) { + loggerEnvOptions = { + MONGODB_LOG_COMMAND: process.env.MONGODB_LOG_COMMAND, + MONGODB_LOG_TOPOLOGY: process.env.MONGODB_LOG_TOPOLOGY, + MONGODB_LOG_SERVER_SELECTION: process.env.MONGODB_LOG_SERVER_SELECTION, + MONGODB_LOG_CONNECTION: process.env.MONGODB_LOG_CONNECTION, + MONGODB_LOG_ALL: process.env.MONGODB_LOG_ALL, + MONGODB_LOG_MAX_DOCUMENT_LENGTH: process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH, + MONGODB_LOG_PATH: process.env.MONGODB_LOG_PATH + }; + loggerClientOptions = { + mongodbLogPath: mongoOptions.mongodbLogPath + }; + } + mongoOptions.mongoLoggerOptions = MongoLogger.resolveOptions( + loggerEnvOptions, + loggerClientOptions + ); + return mongoOptions; } @@ -561,10 +588,10 @@ function setOption( mongoOptions[name] = getBoolean(name, values[0]); break; case 'int': - mongoOptions[name] = getInt(name, values[0]); + mongoOptions[name] = getIntFromOptions(name, values[0]); break; case 'uint': - mongoOptions[name] = getUint(name, values[0]); + mongoOptions[name] = getUIntFromOptions(name, values[0]); break; case 'string': if (values[0] == null) { @@ -770,7 +797,7 @@ export const OPTIONS = { enableUtf8Validation: { type: 'boolean', default: true }, family: { transform({ name, values: [value] }): 4 | 6 { - const transformValue = getInt(name, value); + const transformValue = getIntFromOptions(name, value); if (transformValue === 4 || transformValue === 6) { return transformValue; } @@ -849,9 +876,9 @@ export const OPTIONS = { type: 'uint' }, logger: { - default: new Logger('MongoClient'), + default: new LegacyLogger('MongoClient'), transform({ values: [value] }) { - if (value instanceof Logger) { + if (value instanceof LegacyLogger) { return value; } emitWarning('Alternative loggers might not be supported'); @@ -863,13 +890,13 @@ export const OPTIONS = { loggerLevel: { target: 'logger', transform({ values: [value] }) { - return new Logger('MongoClient', { loggerLevel: value as LoggerLevel }); + return new LegacyLogger('MongoClient', { loggerLevel: value as LegacyLoggerLevel }); } }, maxConnecting: { default: 2, transform({ name, values: [value] }): number { - const maxConnecting = getUint(name, value); + const maxConnecting = getUIntFromOptions(name, value); if (maxConnecting === 0) { throw new MongoInvalidArgumentError('maxConnecting must be > 0 if specified'); } @@ -887,7 +914,7 @@ export const OPTIONS = { maxStalenessSeconds: { target: 'readPreference', transform({ name, options, values: [value] }) { - const maxStalenessSeconds = getUint(name, value); + const maxStalenessSeconds = getUIntFromOptions(name, value); if (options.readPreference) { return ReadPreference.fromOptions({ readPreference: { ...options.readPreference, maxStalenessSeconds } @@ -1206,7 +1233,7 @@ export const OPTIONS = { const wc = WriteConcern.fromOptions({ writeConcern: { ...options.writeConcern, - wtimeout: getUint('wtimeout', value) + wtimeout: getUIntFromOptions('wtimeout', value) } }); if (wc) return wc; @@ -1219,7 +1246,7 @@ export const OPTIONS = { const wc = WriteConcern.fromOptions({ writeConcern: { ...options.writeConcern, - wtimeoutMS: getUint('wtimeoutMS', value) + wtimeoutMS: getUIntFromOptions('wtimeoutMS', value) } }); if (wc) return wc; @@ -1274,4 +1301,7 @@ export const DEFAULT_OPTIONS = new CaseInsensitiveMap( * Set of permitted feature flags * @internal */ -export const FEATURE_FLAGS = new Set([Symbol.for('@@mdb.skipPingOnConnect')]); +export const FEATURE_FLAGS = new Set([ + Symbol.for('@@mdb.skipPingOnConnect'), + Symbol.for('@@mdb.enableMongoLogger') +]); diff --git a/src/index.ts b/src/index.ts index ff692a3b1b..68fdd89b42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -300,6 +300,14 @@ export type { SupportedTLSSocketOptions, WithSessionCallback } from './mongo_client'; +export type { + MongoLoggableComponent, + MongoLogger, + MongoLoggerEnvOptions, + MongoLoggerMongoClientOptions, + MongoLoggerOptions, + SeverityLevel +} from './mongo_logger'; export type { CommonEvents, EventsDescription, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index e52d8b22f0..4d94d87747 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -15,7 +15,8 @@ import { Db, DbOptions } from './db'; import type { AutoEncrypter, AutoEncryptionOptions } from './deps'; import type { Encrypter } from './encrypter'; import { MongoInvalidArgumentError } from './error'; -import type { Logger, LoggerLevel } from './logger'; +import type { Logger as LegacyLogger, LoggerLevel as LegacyLoggerLevel } from './logger'; +import { MongoLogger, MongoLoggerOptions } from './mongo_logger'; import { TypedEventEmitter } from './mongo_types'; import type { ReadConcern, ReadConcernLevel, ReadConcernLike } from './read_concern'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; @@ -233,9 +234,9 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC */ promiseLibrary?: any; /** The logging level */ - loggerLevel?: LoggerLevel; + loggerLevel?: LegacyLoggerLevel; /** Custom logger object */ - logger?: Logger; + logger?: LegacyLogger; /** Enable command monitoring for this client */ monitorCommands?: boolean; /** Server API version */ @@ -296,7 +297,7 @@ export interface MongoClientPrivate { readonly readConcern?: ReadConcern; readonly writeConcern?: WriteConcern; readonly readPreference: ReadPreference; - readonly logger: Logger; + readonly logger: LegacyLogger; readonly isMongoClient: true; } @@ -334,6 +335,8 @@ export class MongoClient extends TypedEventEmitter { s: MongoClientPrivate; /** @internal */ topology?: Topology; + /** @internal */ + readonly mongoLogger: MongoLogger; /** * The consolidate, parsed, transformed and merged options. @@ -345,6 +348,7 @@ export class MongoClient extends TypedEventEmitter { super(); this[kOptions] = parseOptions(url, this, options); + this.mongoLogger = new MongoLogger(this[kOptions].mongoLoggerOptions); // eslint-disable-next-line @typescript-eslint/no-this-alias const client = this; @@ -417,7 +421,7 @@ export class MongoClient extends TypedEventEmitter { return this.s.bsonOptions; } - get logger(): Logger { + get logger(): LegacyLogger { return this.s.logger; } @@ -708,7 +712,7 @@ export class MongoClient extends TypedEventEmitter { } /** Return the mongo client logger */ - getLogger(): Logger { + getLogger(): LegacyLogger { return this.s.logger; } } @@ -803,4 +807,7 @@ export interface MongoOptions /** @internal */ [featureFlag: symbol]: any; + + /** @internal */ + mongoLoggerOptions: MongoLoggerOptions; } diff --git a/src/mongo_logger.ts b/src/mongo_logger.ts new file mode 100644 index 0000000000..81acd2af70 --- /dev/null +++ b/src/mongo_logger.ts @@ -0,0 +1,201 @@ +import { Writable } from 'stream'; + +import { parseUnsignedInteger } from './utils'; + +/** @internal */ +export const SeverityLevel = Object.freeze({ + EMERGENCY: 'emergency', + ALERT: 'alert', + CRITICAL: 'critical', + ERROR: 'error', + WARNING: 'warn', + NOTICE: 'notice', + INFORMATIONAL: 'info', + DEBUG: 'debug', + TRACE: 'trace', + OFF: 'off' +} as const); + +/** @internal */ +export type SeverityLevel = typeof SeverityLevel[keyof typeof SeverityLevel]; + +/** @internal */ +export const MongoLoggableComponent = Object.freeze({ + COMMAND: 'command', + TOPOLOGY: 'topology', + SERVER_SELECTION: 'serverSelection', + CONNECTION: 'connection' +} as const); + +/** @internal */ +export type MongoLoggableComponent = + typeof MongoLoggableComponent[keyof typeof MongoLoggableComponent]; + +/** @internal */ +export interface MongoLoggerEnvOptions { + /** Severity level for command component */ + MONGODB_LOG_COMMAND?: string; + /** Severity level for topology component */ + MONGODB_LOG_TOPOLOGY?: string; + /** Severity level for server selection component */ + MONGODB_LOG_SERVER_SELECTION?: string; + /** Severity level for CMAP */ + MONGODB_LOG_CONNECTION?: string; + /** Default severity level to be if any of the above are unset */ + MONGODB_LOG_ALL?: string; + /** Max length of embedded EJSON docs. Setting to 0 disables truncation. Defaults to 1000. */ + MONGODB_LOG_MAX_DOCUMENT_LENGTH?: string; + /** Destination for log messages. Must be 'stderr', 'stdout'. Defaults to 'stderr'. */ + MONGODB_LOG_PATH?: string; +} + +/** @internal */ +export interface MongoLoggerMongoClientOptions { + /** Destination for log messages */ + mongodbLogPath?: 'stdout' | 'stderr' | Writable; +} + +/** @internal */ +export interface MongoLoggerOptions { + componentSeverities: { + /** Severity level for command component */ + command: SeverityLevel; + /** Severity level for topology component */ + topology: SeverityLevel; + /** Severity level for server selection component */ + serverSelection: SeverityLevel; + /** Severity level for connection component */ + connection: SeverityLevel; + /** 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. */ + logDestination: Writable; +} + +/** + * Parses a string as one of SeverityLevel + * + * @param s - the value to be parsed + * @returns one of SeverityLevel if value can be parsed as such, otherwise null + */ +function parseSeverityFromString(s?: string): SeverityLevel | null { + const validSeverities: string[] = Object.values(SeverityLevel); + const lowerSeverity = s?.toLowerCase(); + + if (lowerSeverity != null && validSeverities.includes(lowerSeverity)) { + return lowerSeverity as SeverityLevel; + } + + return null; +} + +/** + * resolves the MONGODB_LOG_PATH and mongodbLogPath options from the environment and the + * mongo client options respectively. + * + * @returns the Writable stream to write logs to + */ +function resolveLogPath( + { MONGODB_LOG_PATH }: MongoLoggerEnvOptions, + { + mongodbLogPath + }: { + mongodbLogPath?: unknown; + } +): Writable { + const isValidLogDestinationString = (destination: string) => + ['stdout', 'stderr'].includes(destination.toLowerCase()); + if (typeof mongodbLogPath === 'string' && isValidLogDestinationString(mongodbLogPath)) { + return mongodbLogPath.toLowerCase() === 'stderr' ? process.stderr : process.stdout; + } + + // TODO(NODE-4813): check for minimal interface instead of instanceof Writable + if (typeof mongodbLogPath === 'object' && mongodbLogPath instanceof Writable) { + return mongodbLogPath; + } + + if (typeof MONGODB_LOG_PATH === 'string' && isValidLogDestinationString(MONGODB_LOG_PATH)) { + return MONGODB_LOG_PATH.toLowerCase() === 'stderr' ? process.stderr : process.stdout; + } + + return process.stderr; +} + +/** @internal */ +export class MongoLogger { + componentSeverities: Record; + maxDocumentLength: number; + logDestination: Writable; + + constructor(options: MongoLoggerOptions) { + this.componentSeverities = options.componentSeverities; + this.maxDocumentLength = options.maxDocumentLength; + this.logDestination = options.logDestination; + } + + /* eslint-disable @typescript-eslint/no-unused-vars */ + /* eslint-disable @typescript-eslint/no-empty-function */ + emergency(component: any, message: any): void {} + + alert(component: any, message: any): void {} + + critical(component: any, message: any): void {} + + error(component: any, message: any): void {} + + warn(component: any, message: any): void {} + + notice(component: any, message: any): void {} + + info(component: any, message: any): void {} + + debug(component: any, message: any): void {} + + trace(component: any, message: any): void {} + + /** + * Merges options set through environment variables and the MongoClient, preferring environment + * variables when both are set, and substituting defaults for values not set. Options set in + * constructor take precedence over both environment variables and MongoClient options. + * + * @remarks + * When parsing component severity levels, invalid values are treated as unset and replaced with + * the default severity. + * + * @param envOptions - options set for the logger from the environment + * @param clientOptions - options set for the logger in the MongoClient options + * @returns a MongoLoggerOptions object to be used when instantiating a new MongoLogger + */ + static resolveOptions( + envOptions: MongoLoggerEnvOptions, + clientOptions: MongoLoggerMongoClientOptions + ): MongoLoggerOptions { + // client options take precedence over env options + const combinedOptions = { + ...envOptions, + ...clientOptions, + mongodbLogPath: resolveLogPath(envOptions, clientOptions) + }; + const defaultSeverity = + parseSeverityFromString(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, + default: defaultSeverity + }, + maxDocumentLength: + parseUnsignedInteger(combinedOptions.MONGODB_LOG_MAX_DOCUMENT_LENGTH) ?? 1000, + logDestination: combinedOptions.mongodbLogPath + }; + } +} diff --git a/src/utils.ts b/src/utils.ts index 9dc9348e9c..3fce528b4a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1421,3 +1421,16 @@ export function compareObjectId(oid1?: ObjectId | null, oid2?: ObjectId | null): return oid1.id.compare(oid2.id); } + +export function parseInteger(value: unknown): number | null { + if (typeof value === 'number') return Math.trunc(value); + const parsedValue = Number.parseInt(String(value), 10); + + return Number.isNaN(parsedValue) ? null : parsedValue; +} + +export function parseUnsignedInteger(value: unknown): number | null { + const parsedInt = parseInteger(value); + + return parsedInt != null && parsedInt >= 0 ? parsedInt : null; +} diff --git a/test/integration/node-specific/feature_flags.test.ts b/test/integration/node-specific/feature_flags.test.ts index 6bf9a9a091..aba7103467 100644 --- a/test/integration/node-specific/feature_flags.test.ts +++ b/test/integration/node-specific/feature_flags.test.ts @@ -1,5 +1,8 @@ import { expect } from 'chai'; +import { MongoClient } from '../../../src'; +import { MongoLoggableComponent, SeverityLevel } from '../../../src/mongo_logger'; + describe('Feature Flags', () => { describe('@@mdb.skipPingOnConnect', () => { beforeEach(function () { @@ -42,4 +45,100 @@ describe('Feature Flags', () => { }); } }); + + describe('@@mdb.enableMongoLogger', () => { + let cachedEnv; + const loggerFeatureFlag = Symbol.for('@@mdb.enableMongoLogger'); + const components: Array = [ + 'default', + 'topology', + 'serverSelection', + 'connection', + 'command' + ]; + + before(() => { + cachedEnv = process.env; + }); + + after(() => { + process.env = cachedEnv; + }); + + context('when enabled', () => { + context('when logging is enabled for any component', () => { + before(() => { + process.env.MONGODB_LOG_COMMAND = SeverityLevel.EMERGENCY; + }); + + it('enables logging for the specified component', () => { + const client = new MongoClient('mongodb://localhost:27017', { + [loggerFeatureFlag]: true + }); + expect(client.mongoLogger.componentSeverities).to.have.property( + 'command', + SeverityLevel.EMERGENCY + ); + }); + }); + + context('when logging is not enabled for any component', () => { + before(() => { + process.env = {}; + }); + + it('does not enable logging for any component', () => { + const client = new MongoClient('mongodb://localhost:27017', { + [loggerFeatureFlag]: true + }); + for (const component of components) { + expect(client.mongoLogger.componentSeverities).to.have.property( + component, + SeverityLevel.OFF + ); + } + }); + }); + }); + + for (const featureFlagValue of [false, undefined]) { + context(`when set to ${featureFlagValue}`, () => { + context('when logging is enabled for a component', () => { + before(() => { + process.env['MONGODB_LOG_COMMAND'] = SeverityLevel.EMERGENCY; + }); + + it('does not enable logging', () => { + const client = new MongoClient('mongodb://localhost:27017', { + [loggerFeatureFlag]: featureFlagValue + }); + for (const component of components) { + expect(client.mongoLogger.componentSeverities).to.have.property( + component, + SeverityLevel.OFF + ); + } + }); + }); + + context('when logging is not enabled for any component', () => { + before(() => { + process.env = {}; + }); + + it('does not enable logging', () => { + const client = new MongoClient('mongodb://localhost:27017', { + [loggerFeatureFlag]: featureFlagValue + }); + for (const component of components) { + expect(client.mongoLogger.componentSeverities).to.have.property( + component, + SeverityLevel.OFF + ); + } + }); + }); + }); + } + }); }); diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index f2a825db7d..62d0cee692 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -539,8 +539,9 @@ describe('Connection String', function () { describe('feature flags', () => { it('should be stored in the FEATURE_FLAGS Set', () => { - expect(FEATURE_FLAGS.size).to.equal(1); + expect(FEATURE_FLAGS.size).to.equal(2); expect(FEATURE_FLAGS.has(Symbol.for('@@mdb.skipPingOnConnect'))).to.be.true; + expect(FEATURE_FLAGS.has(Symbol.for('@@mdb.enableMongoLogger'))).to.be.true; // Add more flags here }); diff --git a/test/unit/mongo_client.test.js b/test/unit/mongo_client.test.js index 918b18c36e..1be9fd2add 100644 --- a/test/unit/mongo_client.test.js +++ b/test/unit/mongo_client.test.js @@ -10,6 +10,9 @@ const { ReadPreference } = require('../../src/read_preference'); const { Logger } = require('../../src/logger'); const { MongoCredentials } = require('../../src/cmap/auth/mongo_credentials'); const { MongoClient, MongoParseError, ServerApiVersion } = require('../../src'); +const { MongoLogger } = require('../../src/mongo_logger'); +const sinon = require('sinon'); +const { Writable } = require('stream'); describe('MongoOptions', function () { it('MongoClient should always freeze public options', function () { @@ -847,4 +850,26 @@ describe('MongoOptions', function () { }); }); }); + + context('loggingOptions', function () { + const expectedLoggingObject = { + maxDocumentLength: 20, + logDestination: new Writable() + }; + + before(() => { + sinon.stub(MongoLogger, 'resolveOptions').callsFake(() => { + return expectedLoggingObject; + }); + }); + + after(() => { + sinon.restore(); + }); + + it('assigns the parsed options to the mongoLoggerOptions option', function () { + const client = new MongoClient('mongodb://localhost:27017'); + expect(client.options).to.have.property('mongoLoggerOptions').to.equal(expectedLoggingObject); + }); + }); }); diff --git a/test/unit/mongo_logger.test.ts b/test/unit/mongo_logger.test.ts new file mode 100644 index 0000000000..e9a28b41a4 --- /dev/null +++ b/test/unit/mongo_logger.test.ts @@ -0,0 +1,525 @@ +import { expect } from 'chai'; +import { Readable, Writable } from 'stream'; + +import { MongoLogger, MongoLoggerOptions, SeverityLevel } from '../../src/mongo_logger'; + +class BufferingStream extends Writable { + buffer: string[] = []; + + constructor(options = {}) { + super({ ...options, objectMode: true }); + } + + override _write(chunk, encoding, callback) { + this.buffer.push(chunk); + callback(); + } +} + +describe('meta tests for BufferingStream', function () { + it('the buffer is empty on construction', function () { + const stream = new BufferingStream(); + expect(stream.buffer).to.have.lengthOf(0); + }); + it('pushes messages to the buffer when written to', function () { + const stream = new BufferingStream(); + stream.write('message'); + expect(stream.buffer).to.deep.equal(['message']); + }); +}); + +describe('class MongoLogger', function () { + describe('#constructor()', function () { + it('assigns each property from the options object onto the logging class', function () { + const componentSeverities: MongoLoggerOptions['componentSeverities'] = { + command: 'alert' + } as any; + const stream = new Writable(); + const logger = new MongoLogger({ + componentSeverities, + maxDocumentLength: 10, + logDestination: stream + }); + + expect(logger).to.have.property('componentSeverities', componentSeverities); + expect(logger).to.have.property('maxDocumentLength', 10); + expect(logger).to.have.property('logDestination', stream); + }); + }); + + describe('static #resolveOptions()', function () { + describe('componentSeverities', function () { + const components = new Map([ + ['MONGODB_LOG_COMMAND', 'command'], + ['MONGODB_LOG_TOPOLOGY', 'topology'], + ['MONGODB_LOG_SERVER_SELECTION', 'serverSelection'], + ['MONGODB_LOG_CONNECTION', 'connection'] + ]); + + function* makeValidOptions(): Generator<[string, string]> { + const validOptions = Object.values(SeverityLevel).filter( + option => option !== SeverityLevel.OFF + ); + for (const option of validOptions) { + yield [option, option]; + yield [option.toUpperCase(), option]; + } + } + + const invalidOptions = ['', 'invalid-string']; + const validNonDefaultOptions = new Map(makeValidOptions()); + + context('default', () => { + context('when MONGODB_LOG_ALL is unset', () => { + it('sets default to OFF', () => { + const { componentSeverities } = MongoLogger.resolveOptions({}, {}); + expect(componentSeverities.default).to.equal(SeverityLevel.OFF); + }); + }); + + context('when MONGODB_LOG_ALL is invalid', () => { + for (const invalidOption of invalidOptions) { + context(`{ MONGODB_LOG_ALL: '${invalidOption} }'`, () => { + it('sets default to OFF', () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + MONGODB_LOG_ALL: invalidOption + }, + {} + ); + expect(componentSeverities.default).to.equal(SeverityLevel.OFF); + }); + }); + } + }); + + context('when MONGODB_LOG_ALL is valid', () => { + for (const [validOption, expectedValue] of validNonDefaultOptions) { + context(`{ MONGODB_LOG_ALL: '${validOption} }'`, () => { + it('sets default to the value of MONGODB_LOG_ALL', () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + MONGODB_LOG_ALL: validOption + }, + {} + ); + expect(componentSeverities.default).to.equal(expectedValue); + }); + }); + } + }); + }); + for (const [loggingComponent, componentSeverityOption] of components) { + context(`when ${loggingComponent} is unset`, () => { + context(`when MONGODB_LOG_ALL is unset`, () => { + it(`sets ${componentSeverityOption} to OFF`, () => { + const { componentSeverities } = MongoLogger.resolveOptions({}, {}); + expect(componentSeverities[componentSeverityOption]).to.equal(SeverityLevel.OFF); + }); + }); + + context(`when MONGODB_LOG_ALL is set to an invalid value`, () => { + for (const invalidOption of invalidOptions) { + context(`{ MONGODB_LOG_ALL: ${invalidOption} }`, () => { + it(`sets ${invalidOption} to OFF`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + MONGODB_LOG_ALL: invalidOption + }, + {} + ); + expect(componentSeverities[componentSeverityOption]).to.equal(SeverityLevel.OFF); + }); + }); + } + }); + + context(`when MONGODB_LOG_ALL is set to a valid value`, () => { + for (const [option, expectedValue] of validNonDefaultOptions) { + context(`{ MONGODB_LOG_ALL: ${option} }`, () => { + it(`sets ${option} to the value of MONGODB_LOG_ALL`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + MONGODB_LOG_ALL: option + }, + {} + ); + expect(componentSeverities[componentSeverityOption]).to.equal(expectedValue); + }); + }); + } + }); + }); + + context(`when ${loggingComponent} is set to an invalid value in the environment`, () => { + context(`when MONGODB_LOG_ALL is unset`, () => { + for (const invalidOption of invalidOptions) { + context(`{ ${loggingComponent}: ${invalidOption} }`, () => { + it(`sets ${componentSeverityOption} to OFF`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + [loggingComponent]: invalidOption + }, + {} + ); + + expect(componentSeverities[componentSeverityOption]).to.equal(SeverityLevel.OFF); + }); + }); + } + }); + + context(`when MONGODB_LOG_ALL is set to an invalid value`, () => { + for (const invalidOption of invalidOptions) { + context( + `{ ${loggingComponent}: ${invalidOption}, MONGODB_LOG_ALL: ${invalidOption} }`, + () => { + it(`sets ${componentSeverityOption} to OFF`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + [loggingComponent]: invalidOption, + MONGODB_LOG_ALL: invalidOption + }, + {} + ); + + expect(componentSeverities[componentSeverityOption]).to.equal( + SeverityLevel.OFF + ); + }); + } + ); + } + }); + + context(`when MONGODB_LOG_ALL is set to a valid value`, () => { + const invalidOption = invalidOptions[0]; + + for (const [option, expectedValue] of validNonDefaultOptions) { + context( + `{ MONGODB_LOG_ALL: ${option}, ${componentSeverityOption}: ${option} }`, + () => { + it(`sets ${componentSeverityOption} to the value of MONGODB_LOG_ALL`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + [loggingComponent]: invalidOption, + MONGODB_LOG_ALL: option + }, + {} + ); + expect(componentSeverities[componentSeverityOption]).to.equal(expectedValue); + }); + } + ); + } + }); + }); + + context(`when ${loggingComponent} is set to a valid value in the environment`, () => { + context(`when MONGODB_LOG_ALL is unset`, () => { + for (const [option, expectedValue] of validNonDefaultOptions) { + context(`{ ${loggingComponent}: ${option} }`, () => { + it(`sets ${componentSeverityOption} to the value of ${loggingComponent}`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + [loggingComponent]: option + }, + {} + ); + + expect(componentSeverities[componentSeverityOption]).to.equal(expectedValue); + }); + }); + } + }); + + context(`when MONGODB_LOG_ALL is set to an invalid value`, () => { + const invalidValue = invalidOptions[0]; + for (const [option, expectedValue] of validNonDefaultOptions) { + context( + `{ ${loggingComponent}: ${option}, MONGODB_LOG_ALL: ${invalidValue} }`, + () => { + it(`sets ${componentSeverityOption} to the value of ${loggingComponent}`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + [loggingComponent]: option, + MONGODB_LOG_ALL: invalidValue + }, + {} + ); + + expect(componentSeverities[componentSeverityOption]).to.equal(expectedValue); + }); + } + ); + } + }); + + context(`when MONGODB_LOG_ALL is set to a valid value`, () => { + const validOption = validNonDefaultOptions.keys()[0]; + for (const [option, expectedValue] of validNonDefaultOptions) { + context(`{ ${loggingComponent}: ${option}, MONGODB_LOG_ALL: ${validOption} }`, () => { + it(`sets ${componentSeverityOption} to the value of ${loggingComponent}`, () => { + const { componentSeverities } = MongoLogger.resolveOptions( + { + [loggingComponent]: option, + MONGODB_LOG_ALL: validOption + }, + {} + ); + + expect(componentSeverities[componentSeverityOption]).to.equal(expectedValue); + }); + }); + } + }); + }); + } + }); + + context('maxDocumentLength', function () { + const tests: Array<{ + input: undefined | string; + expected: number; + context: string; + outcome: string; + }> = [ + { + input: undefined, + expected: 1000, + context: 'when unset', + outcome: 'defaults to 1000' + }, + { + input: '33', + context: 'when set to parsable uint', + outcome: 'sets `maxDocumentLength` to the parsed value', + expected: 33 + }, + { + input: '', + context: 'when set to an empty string', + outcome: 'defaults to 1000', + expected: 1000 + }, + { + input: 'asdf', + context: 'when set to a non-integer string', + outcome: 'defaults to 1000', + expected: 1000 + } + ]; + + for (const { input, outcome, expected, context: _context } of tests) { + context(_context, () => { + it(outcome, () => { + const options = MongoLogger.resolveOptions( + { MONGODB_LOG_MAX_DOCUMENT_LENGTH: input }, + {} + ); + expect(options.maxDocumentLength).to.equal(expected); + }); + }); + } + }); + + context('logDestination', function () { + const stream = new Writable(); + const validOptions: Map = new Map([ + ['stdout', process.stdout], + ['stderr', process.stderr], + [stream, stream], + ['stdOut', process.stdout], + ['stdErr', process.stderr] + ] as Array<[any, Writable]>); + const unsetOptions = ['', undefined]; + const invalidEnvironmentOptions = ['non-acceptable-string']; + const invalidClientOptions = ['', ' ', undefined, null, 0, false, new Readable()]; + const validClientOptions = ['stderr', 'stdout', stream, 'stdErr', 'stdOut']; + const validEnvironmentOptions = ['stderr', 'stdout', 'stdOut', 'stdErr']; + context('when MONGODB_LOG_DESTINATION is unset in the environment', function () { + context('when mongodbLogPath is unset as a client option', function () { + for (const unsetEnvironmentOption of unsetOptions) { + for (const unsetOption of unsetOptions) { + it(`{environment: "${unsetEnvironmentOption}", client: "${unsetOption}"} defaults to process.stderr`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: unsetEnvironmentOption + }, + { mongodbLogPath: unsetOption as any } + ); + expect(options.logDestination).to.equal(process.stderr); + }); + } + } + }); + + context('when mongodbLogPath is an invalid client option', function () { + for (const unsetEnvironmentOption of unsetOptions) { + for (const invalidOption of invalidClientOptions) { + it(`{environment: "${unsetEnvironmentOption}", client: "${invalidOption}"} defaults to process.stderr`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: unsetEnvironmentOption + }, + { mongodbLogPath: invalidOption as any } + ); + expect(options.logDestination).to.equal(process.stderr); + }); + } + } + }); + + context('when mongodbLogPath is a valid client option', function () { + for (const unsetEnvironmentOption of unsetOptions) { + for (const validOption of validClientOptions) { + it(`{environment: "${unsetEnvironmentOption}", client: "${validOption}"} uses the value from the client options`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: unsetEnvironmentOption + }, + { mongodbLogPath: validOption as any } + ); + const correctDestination = validOptions.get(validOption); + expect(options.logDestination).to.equal(correctDestination); + }); + } + } + }); + }); + + context( + 'when MONGODB_LOG_DESTINATION is set to an invalid value in the environment', + function () { + context('when mongodbLogPath is unset on the client options', function () { + for (const invalidEnvironmentOption of invalidEnvironmentOptions) { + for (const unsetClientOption of unsetOptions) { + it(`{environment: "${invalidEnvironmentOption}", client: "${unsetClientOption}"} defaults to process.stderr`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: invalidEnvironmentOption + }, + { mongodbLogPath: unsetClientOption as any } + ); + expect(options.logDestination).to.equal(process.stderr); + }); + } + } + }); + + context( + 'when mongodbLogPath is set to an invalid value on the client options', + function () { + for (const invalidEnvironmentOption of invalidEnvironmentOptions) { + for (const invalidOption of invalidClientOptions) { + it(`{environment: "${invalidEnvironmentOption}", client: "${invalidOption}"} defaults to process.stderr`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: invalidEnvironmentOption + }, + { mongodbLogPath: invalidOption as any } + ); + expect(options.logDestination).to.equal(process.stderr); + }); + } + } + } + ); + + context('when mongodbLogPath is set to a valid value on the client options', function () { + for (const invalidEnvironmentOption of invalidEnvironmentOptions) { + for (const validOption of validClientOptions) { + it(`{environment: "${invalidEnvironmentOption}", client: "${validOption}"} uses the value from the client options`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: invalidEnvironmentOption + }, + { mongodbLogPath: validOption as any } + ); + const correctDestination = validOptions.get(validOption); + expect(options.logDestination).to.equal(correctDestination); + }); + } + } + }); + } + ); + + context('when MONGODB_LOG_PATH is set to a valid option in the environment', function () { + context('when mongodbLogPath is unset on the client options', function () { + for (const validEnvironmentOption of validEnvironmentOptions) { + for (const unsetOption of unsetOptions) { + it(`{environment: "${validEnvironmentOption}", client: "${unsetOption}"} uses process.${validEnvironmentOption}`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: validEnvironmentOption + }, + { mongodbLogPath: unsetOption as any } + ); + const correctDestination = validOptions.get(validEnvironmentOption); + expect(options.logDestination).to.equal(correctDestination); + }); + } + } + }); + + context( + 'when mongodbLogPath is set to an invalid value on the client options', + function () { + for (const validEnvironmentOption of validEnvironmentOptions) { + for (const invalidValue of invalidClientOptions) { + it(`{environment: "${validEnvironmentOption}", client: "${invalidValue}"} uses process.${validEnvironmentOption}`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: validEnvironmentOption + }, + { mongodbLogPath: invalidValue as any } + ); + const correctDestination = validOptions.get(validEnvironmentOption); + expect(options.logDestination).to.equal(correctDestination); + }); + } + } + } + ); + + context('when mongodbLogPath is set to valid client option', function () { + for (const validEnvironmentOption of validEnvironmentOptions) { + for (const validValue of validClientOptions) { + it(`{environment: "${validEnvironmentOption}", client: "${validValue}"} uses the value from the client options`, function () { + const options = MongoLogger.resolveOptions( + { + MONGODB_LOG_PATH: validEnvironmentOption + }, + { mongodbLogPath: validValue as any } + ); + const correctDestination = validOptions.get(validValue); + expect(options.logDestination).to.equal(correctDestination); + }); + } + } + }); + }); + }); + }); + + describe('severity helpers', function () { + const severities = Object.values(SeverityLevel).filter(severity => severity !== 'off'); + for (const severityLevel of severities) { + describe(`${severityLevel}()`, function () { + it('does not log when logging for the component is disabled', () => { + const stream = new BufferingStream(); + const logger = new MongoLogger({ + componentSeverities: { + topology: 'off' + } as any, + logDestination: stream + } as any); + + logger[severityLevel]('topology', 'message'); + expect(stream.buffer).to.have.lengthOf(0); + }); + }); + } + }); +});