From 305e90fba18b9e5caf81b7fc21d9fe929a54e6da Mon Sep 17 00:00:00 2001 From: Andy Mina Date: Tue, 22 Nov 2022 17:04:28 -0500 Subject: [PATCH] test: add test for MongoLogger --- src/connection_string.ts | 32 +++---- src/logging_api.ts | 153 --------------------------------- src/mongo_client.ts | 21 +++-- src/mongo_logger.ts | 131 ++++++++++++++++++++++++++++ test/unit/mongo_client.test.js | 32 ++++++- test/unit/mongo_logger.ts | 41 +++++++++ 6 files changed, 232 insertions(+), 178 deletions(-) delete mode 100644 src/logging_api.ts create mode 100644 src/mongo_logger.ts create mode 100644 test/unit/mongo_logger.ts diff --git a/src/connection_string.ts b/src/connection_string.ts index 19a17463268..34080639145 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -14,8 +14,7 @@ import { MongoMissingCredentialsError, MongoParseError } from './error'; -import { Logger, LoggerLevel } from './logger'; -import { extractLoggerOptions, Logger as LoggingApi, SeverityLevel } from './logging_api'; +import { Logger as LegacyLogger, LoggerLevel as LegacyLoggerLevel } from './logger'; import { DriverInfo, MongoClient, @@ -25,6 +24,7 @@ import { ServerApi, ServerApiVersion } from './mongo_client'; +import { Logger, SeverityLevel } from './mongo_logger'; import { PromiseProvider } from './promise_provider'; import { ReadConcern, ReadConcernLevel } from './read_concern'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; @@ -207,7 +207,7 @@ function getInt(name: string, value: unknown): number { throw new MongoParseError(`Expected ${name} to be stringified int value, got: ${value}`); } -function getUint(name: string, value: unknown): number { +export function getUint(name: string, value: unknown): number { const parsedValue = getInt(name, value); if (parsedValue < 0) { throw new MongoParseError(`${name} can only be a positive int value, got: ${value}`); @@ -508,18 +508,18 @@ export function parseOptions( ); } - const loggingApiMongoClientOptions = { mongodbLogPath: mongoOptions.mongodbLogPath }; - const loggingApiOptions = extractLoggerOptions(loggingApiMongoClientOptions); + const loggerMongoClientOptions = { mongodbLogPath: mongoOptions.mongodbLogPath }; + const loggerOptions = Logger.resolveOptions(loggerMongoClientOptions); const loggingComponents = [ - loggingApiOptions.commandSeverity, - loggingApiOptions.topologySeverity, - loggingApiOptions.serverSelectionSeverity, - loggingApiOptions.connectionSeverity, - loggingApiOptions.defaultSeverity + loggerOptions.command, + loggerOptions.topology, + loggerOptions.serverSelection, + loggerOptions.connection, + loggerOptions.defaultSeverity ]; if (loggingComponents.some(severity => severity !== SeverityLevel.OFF)) { - mongoOptions.loggingApi = new LoggingApi(loggingApiOptions); + mongoOptions.mongoLogger = new Logger(loggerOptions); } return mongoOptions; @@ -864,9 +864,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'); @@ -878,11 +878,11 @@ 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 }); } }, - loggingApi: { - target: 'loggingApi' + mongoLogger: { + target: 'mongoLogger' }, maxConnecting: { default: 2, diff --git a/src/logging_api.ts b/src/logging_api.ts deleted file mode 100644 index 6eef418c90d..00000000000 --- a/src/logging_api.ts +++ /dev/null @@ -1,153 +0,0 @@ -import * as fs from 'fs'; -import type { Writable } from 'stream'; - -import { MongoInvalidArgumentError } from './error'; - -/** @public */ -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); - -/** @public */ -export type SeverityLevel = typeof SeverityLevel[keyof typeof SeverityLevel]; - -/** @internal */ -export const LoggableComponent = Object.freeze({ - COMMAND: 'command', - TOPOLOGY: 'topology', - SERVER_SELECTION: 'serverSelection', - CONNECTION: 'connection' -} as const); - -/** @internal */ -type LoggableComponent = typeof LoggableComponent[keyof typeof LoggableComponent]; - -/** @public */ -export interface LoggerMongoClientOptions { - mongodbLogPath?: string | Writable; -} - -/** @public */ -export interface LoggerOptions { - commandSeverity: SeverityLevel; - topologySeverity: SeverityLevel; - serverSelectionSeverity: SeverityLevel; - connectionSeverity: SeverityLevel; - defaultSeverity: SeverityLevel; - maxDocumentLength: number; - logDestination: string | Writable; -} - -/** - * @public - * TODO(andymina): add docs for this - */ -export function extractLoggerOptions(clientOptions?: LoggerMongoClientOptions): LoggerOptions { - const validSeverities = Object.values(SeverityLevel); - - return { - commandSeverity: - validSeverities.find(severity => severity === process.env.MONGODB_LOG_COMMAND) ?? - SeverityLevel.OFF, - topologySeverity: - validSeverities.find(severity => severity === process.env.MONGODB_LOG_TOPOLOGY) ?? - SeverityLevel.OFF, - serverSelectionSeverity: - validSeverities.find(severity => severity === process.env.MONGODB_LOG_SERVER_SELECTION) ?? - SeverityLevel.OFF, - connectionSeverity: - validSeverities.find(severity => severity === process.env.MONGODB_LOG_CONNECTION) ?? - SeverityLevel.OFF, - defaultSeverity: - validSeverities.find(severity => severity === process.env.MONGODB_LOG_COMMAND) ?? - SeverityLevel.OFF, - maxDocumentLength: - typeof process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH === 'string' - ? Number.parseInt(process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH) - : 1000, - logDestination: - typeof process.env.MONGODB_LOG_PATH === 'string' - ? process.env.MONGODB_LOG_PATH - : clientOptions?.mongodbLogPath ?? 'stderr' - }; -} - -/** @public */ -export class Logger { - /** @internal */ - componentSeverities: Map = new Map(); - maxDocumentLength: number; - logDestination: Writable; - - constructor(options: LoggerOptions) { - // validate log path - if (typeof options.logDestination === 'string') { - this.logDestination = - options.logDestination === 'stderr' || options.logDestination === 'stdout' - ? process[options.logDestination] - : fs.createWriteStream(options.logDestination, { flags: 'a+' }); - } else { - this.logDestination = options.logDestination; - } - - // fill component severities - this.componentSeverities.set( - LoggableComponent.COMMAND, - options.commandSeverity !== SeverityLevel.OFF - ? options.commandSeverity - : options.defaultSeverity - ); - this.componentSeverities.set( - LoggableComponent.TOPOLOGY, - options.topologySeverity !== SeverityLevel.OFF - ? options.topologySeverity - : options.defaultSeverity - ); - this.componentSeverities.set( - LoggableComponent.SERVER_SELECTION, - options.serverSelectionSeverity !== SeverityLevel.OFF - ? options.serverSelectionSeverity - : options.defaultSeverity - ); - this.componentSeverities.set( - LoggableComponent.CONNECTION, - options.connectionSeverity !== SeverityLevel.OFF - ? options.connectionSeverity - : options.defaultSeverity - ); - - // fill max doc length - if (options.maxDocumentLength < 0) - throw new MongoInvalidArgumentError('MONGODB_LOG_MAX_DOCUMENT_LENGTH must be >= 0'); - this.maxDocumentLength = options.maxDocumentLength; - } - - /* 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 {} -} diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 0cfe6757cfb..746ec53fee9 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -15,8 +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 LoggingApi } from './logging_api'; +import type { Logger as LegacyLogger, LoggerLevel as LegacyLoggerLevel } from './logger'; +import type { Logger } from './mongo_logger'; import { TypedEventEmitter } from './mongo_types'; import type { ReadConcern, ReadConcernLevel, ReadConcernLike } from './read_concern'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; @@ -234,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 */ @@ -297,7 +297,7 @@ export interface MongoClientPrivate { readonly readConcern?: ReadConcern; readonly writeConcern?: WriteConcern; readonly readPreference: ReadPreference; - readonly logger: Logger; + readonly logger: LegacyLogger; readonly isMongoClient: true; } @@ -335,6 +335,8 @@ export class MongoClient extends TypedEventEmitter { s: MongoClientPrivate; /** @internal */ topology?: Topology; + /** @internal */ + readonly mongoLogger: Logger | null; /** * The consolidate, parsed, transformed and merged options. @@ -346,6 +348,7 @@ export class MongoClient extends TypedEventEmitter { super(); this[kOptions] = parseOptions(url, this, options); + this.mongoLogger = this[kOptions].mongoLogger ?? null; // eslint-disable-next-line @typescript-eslint/no-this-alias const client = this; @@ -418,7 +421,7 @@ export class MongoClient extends TypedEventEmitter { return this.s.bsonOptions; } - get logger(): Logger { + get logger(): LegacyLogger { return this.s.logger; } @@ -709,7 +712,7 @@ export class MongoClient extends TypedEventEmitter { } /** Return the mongo client logger */ - getLogger(): Logger { + getLogger(): LegacyLogger { return this.s.logger; } } @@ -773,7 +776,6 @@ export interface MongoOptions proxyPort?: number; proxyUsername?: string; proxyPassword?: string; - loggingApi?: LoggingApi; /** @internal */ connectionType?: typeof Connection; @@ -805,4 +807,7 @@ export interface MongoOptions /** @internal */ [featureFlag: symbol]: any; + + /** @internal */ + mongoLogger?: Logger; } diff --git a/src/mongo_logger.ts b/src/mongo_logger.ts new file mode 100644 index 00000000000..ee3fa2f38fa --- /dev/null +++ b/src/mongo_logger.ts @@ -0,0 +1,131 @@ +import * as fs from 'fs'; +import { env } from 'process'; +import type { Writable } from 'stream'; + +import { getUint } from './connection_string'; + +/** @public */ +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); + +/** @public */ +export type SeverityLevel = typeof SeverityLevel[keyof typeof SeverityLevel]; + +/** @returns one of SeverityLevel or null if it is not a valid SeverityLevel */ +function toValidSeverity(severity?: string): SeverityLevel | null { + const validSeverities: string[] = Object.values(SeverityLevel); + const lowerSeverity = severity?.toLowerCase(); + + if (lowerSeverity != null && validSeverities.includes(lowerSeverity)) { + return lowerSeverity as SeverityLevel; + } + + return null; +} + +/** @internal */ +export const LoggableComponent = Object.freeze({ + COMMAND: 'command', + TOPOLOGY: 'topology', + SERVER_SELECTION: 'serverSelection', + CONNECTION: 'connection' +} as const); + +/** @internal */ +type LoggableComponent = typeof LoggableComponent[keyof typeof LoggableComponent]; + +/** @internal */ +export interface LoggerMongoClientOptions { + mongodbLogPath?: string | Writable; +} + +/** @public */ +export interface LoggerOptions { + command: SeverityLevel; + topology: SeverityLevel; + serverSelection: SeverityLevel; + connection: SeverityLevel; + defaultSeverity: SeverityLevel; + maxDocumentLength: number; + logPath: string | Writable; +} + +/** + * @internal + * TODO(andymina): add docs + */ +export class Logger { + /** @internal */ + componentSeverities: Record; + maxDocumentLength: number; + logPath: Writable; + + constructor(options: LoggerOptions) { + // validate log path + if (typeof options.logPath === 'string') { + this.logPath = + options.logPath === 'stderr' || options.logPath === 'stdout' + ? process[options.logPath] + : fs.createWriteStream(options.logPath, { flags: 'a+' }); + } else { + this.logPath = options.logPath; + } + + // extract comp severities + this.componentSeverities = options; + + // fill max doc length + this.maxDocumentLength = options.maxDocumentLength; + } + + /* 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 {} + + static resolveOptions(clientOptions?: LoggerMongoClientOptions): LoggerOptions { + const defaultSeverity = toValidSeverity(env.MONGODB_LOG_ALL) ?? SeverityLevel.OFF; + + return { + command: toValidSeverity(env.MONGODB_LOG_COMMAND) ?? defaultSeverity, + topology: toValidSeverity(env.MONGODB_LOG_TOPOLOGY) ?? defaultSeverity, + serverSelection: toValidSeverity(env.MONGODB_LOG_SERVER_SELECTION) ?? defaultSeverity, + connection: toValidSeverity(env.MONGODB_LOG_CONNECTION) ?? defaultSeverity, + defaultSeverity, + maxDocumentLength: + typeof env.MONGODB_LOG_MAX_DOCUMENT_LENGTH === 'string' && + env.MONGODB_LOG_MAX_DOCUMENT_LENGTH !== '' + ? getUint('MONGODB_LOG_MAX_DOCUMENT_LENGTH', env.MONGODB_LOG_MAX_DOCUMENT_LENGTH) + : 1000, + logPath: + typeof env.MONGODB_LOG_PATH === 'string' && env.MONGODB_LOG_PATH !== '' + ? env.MONGODB_LOG_PATH + : clientOptions?.mongodbLogPath ?? 'stderr' + }; + } +} diff --git a/test/unit/mongo_client.test.js b/test/unit/mongo_client.test.js index 918b18c36e0..aff5476aabd 100644 --- a/test/unit/mongo_client.test.js +++ b/test/unit/mongo_client.test.js @@ -1,15 +1,17 @@ 'use strict'; const os = require('os'); const fs = require('fs'); +const { env } = require('process'); const { expect } = require('chai'); const { getSymbolFrom } = require('../tools/utils'); const { parseOptions, resolveSRVRecord } = require('../../src/connection_string'); const { ReadConcern } = require('../../src/read_concern'); const { WriteConcern } = require('../../src/write_concern'); const { ReadPreference } = require('../../src/read_preference'); -const { Logger } = require('../../src/logger'); +const { Logger } = require('../../src/mongo_logger'); const { MongoCredentials } = require('../../src/cmap/auth/mongo_credentials'); const { MongoClient, MongoParseError, ServerApiVersion } = require('../../src'); +const { SeverityLevel } = require('../../src/mongo_logger'); describe('MongoOptions', function () { it('MongoClient should always freeze public options', function () { @@ -847,4 +849,32 @@ describe('MongoOptions', function () { }); }); }); + + context('logger', function () { + const severityVars = [ + 'MONGODB_LOG_COMMAND', + 'MONGODB_LOG_TOPOLOGY', + 'MONGODB_LOG_SERVER_SELECTION', + 'MONGODB_LOG_CONNECTION', + 'MONGODB_LOG_ALL' + ]; + + for (const name of severityVars) { + it(`should enable logging if at least ${name} is set to a valid value`, function () { + env[name] = SeverityLevel.CRITICAL; + const client = new MongoClient('mongodb://localhost:27017'); + expect(client.mongoLogger).to.be.instanceOf(Logger); + env[name] = undefined; + }); + } + + for (const name of severityVars) { + it(`should not enable logging if ${name} is set to an invalid value`, function () { + env[name] = 'invalid'; + const client = new MongoClient('mongodb://localhost:27017'); + expect(client).property('mongoLogger', null); + env[name] = undefined; + }); + } + }); }); diff --git a/test/unit/mongo_logger.ts b/test/unit/mongo_logger.ts new file mode 100644 index 00000000000..8c91564e913 --- /dev/null +++ b/test/unit/mongo_logger.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai'; + +import { LoggableComponent, Logger, LoggerOptions, SeverityLevel } from '../../src/mongo_logger'; + +describe('Logger', function () { + describe('options parsing', function () { + let loggerOptions: LoggerOptions; + + before(function () { + // MONGODB_LOG_COMMAND is not set so it defaults to undefined + process.env.MONGODB_LOG_TOPOLOGY = ''; + process.env.MONGODB_LOG_SERVER_SELECTION = 'invalid'; + process.env.MONGODB_LOG_CONNECTION = 'CRITICAL'; + process.env.MONGODB_LOG_ALL = 'eRrOr'; + process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH = '100'; + process.env.MONGODB_LOG_PATH = 'stderr'; + + loggerOptions = Logger.resolveOptions(); + }); + + it('treats severity values as case-insensitive', function () { + expect(loggerOptions.connection).to.equal(SeverityLevel.CRITICAL); + expect(loggerOptions.defaultSeverity).to.equal(SeverityLevel.ERROR); + }); + + it('will only use MONGODB_LOG_ALL for component severities that are not set or invalid', function () { + expect(loggerOptions.command).to.equal(loggerOptions.defaultSeverity); // empty str + expect(loggerOptions.topology).to.equal(loggerOptions.defaultSeverity); // undefined + expect(loggerOptions.serverSelection).to.equal(loggerOptions.defaultSeverity); // invalid + }); + + it('can set severity levels per component', function () { + const { componentSeverities } = new Logger(loggerOptions); + + expect(componentSeverities).property(LoggableComponent.COMMAND, SeverityLevel.ERROR); + expect(componentSeverities).property(LoggableComponent.TOPOLOGY, SeverityLevel.ERROR); + expect(componentSeverities).property(LoggableComponent.SERVER_SELECTION, SeverityLevel.ERROR); + expect(componentSeverities).property(LoggableComponent.CONNECTION, SeverityLevel.CRITICAL); + }); + }); +});