opusdev/vector-similarity-api
1
1import { inspect, promisify } from 'util';2 3import {4 type Binary,5 type BSONRegExp,6 type BSONSymbol,7 type Code,8 type DBRef,9 type Decimal128,10 type Document,11 type Double,12 EJSON,13 type EJSONOptions,14 type Int32,15 type Long,16 type MaxKey,17 type MinKey,18 type ObjectId,19 type Timestamp20} from './bson';21import type { CommandStartedEvent } from './cmap/command_monitoring_events';22import type {23 ConnectionCheckedInEvent,24 ConnectionCheckedOutEvent,25 ConnectionCheckOutFailedEvent,26 ConnectionCheckOutStartedEvent,27 ConnectionClosedEvent,28 ConnectionCreatedEvent,29 ConnectionPoolClearedEvent,30 ConnectionPoolClosedEvent,31 ConnectionPoolCreatedEvent,32 ConnectionPoolReadyEvent,33 ConnectionReadyEvent34} from './cmap/connection_pool_events';35import {36 COMMAND_FAILED,37 COMMAND_STARTED,38 COMMAND_SUCCEEDED,39 CONNECTION_CHECK_OUT_FAILED,40 CONNECTION_CHECK_OUT_STARTED,41 CONNECTION_CHECKED_IN,42 CONNECTION_CHECKED_OUT,43 CONNECTION_CLOSED,44 CONNECTION_CREATED,45 CONNECTION_POOL_CLEARED,46 CONNECTION_POOL_CLOSED,47 CONNECTION_POOL_CREATED,48 CONNECTION_POOL_READY,49 CONNECTION_READY,50 SERVER_CLOSED,51 SERVER_HEARTBEAT_FAILED,52 SERVER_HEARTBEAT_STARTED,53 SERVER_HEARTBEAT_SUCCEEDED,54 SERVER_OPENING,55 SERVER_SELECTION_FAILED,56 SERVER_SELECTION_STARTED,57 SERVER_SELECTION_SUCCEEDED,58 TOPOLOGY_CLOSED,59 TOPOLOGY_DESCRIPTION_CHANGED,60 TOPOLOGY_OPENING,61 WAITING_FOR_SUITABLE_SERVER62} from './constants';63import type {64 ServerClosedEvent,65 ServerOpeningEvent,66 TopologyClosedEvent,67 TopologyDescriptionChangedEvent,68 TopologyOpeningEvent69} from './sdam/events';70import type {71 ServerSelectionEvent,72 ServerSelectionFailedEvent,73 ServerSelectionStartedEvent,74 ServerSelectionSucceededEvent,75 WaitingForSuitableServerEvent76} from './sdam/server_selection_events';77import { HostAddress, isPromiseLike, isUint8Array, parseUnsignedInteger } from './utils';78 79/**80 * @public81 * Severity levels align with unix syslog.82 * Most typical driver functions will log to debug.83 */84export const SeverityLevel = Object.freeze({85 EMERGENCY: 'emergency',86 ALERT: 'alert',87 CRITICAL: 'critical',88 ERROR: 'error',89 WARNING: 'warn',90 NOTICE: 'notice',91 INFORMATIONAL: 'info',92 DEBUG: 'debug',93 TRACE: 'trace',94 OFF: 'off'95} as const);96 97/** @internal */98export const DEFAULT_MAX_DOCUMENT_LENGTH = 1000;99/** @public */100export type SeverityLevel = (typeof SeverityLevel)[keyof typeof SeverityLevel];101 102/** @internal */103class SeverityLevelMap extends Map<SeverityLevel | number, SeverityLevel | number> {104 constructor(entries: [SeverityLevel | number, SeverityLevel | number][]) {105 const newEntries: [number | SeverityLevel, SeverityLevel | number][] = [];106 for (const [level, value] of entries) {107 newEntries.push([value, level]);108 }109 110 newEntries.push(...entries);111 super(newEntries);112 }113 114 getNumericSeverityLevel(severity: SeverityLevel): number {115 return this.get(severity) as number;116 }117 118 getSeverityLevelName(level: number): SeverityLevel | undefined {119 return this.get(level) as SeverityLevel | undefined;120 }121}122 123/** @internal */124export const SEVERITY_LEVEL_MAP = new SeverityLevelMap([125 [SeverityLevel.OFF, -Infinity],126 [SeverityLevel.EMERGENCY, 0],127 [SeverityLevel.ALERT, 1],128 [SeverityLevel.CRITICAL, 2],129 [SeverityLevel.ERROR, 3],130 [SeverityLevel.WARNING, 4],131 [SeverityLevel.NOTICE, 5],132 [SeverityLevel.INFORMATIONAL, 6],133 [SeverityLevel.DEBUG, 7],134 [SeverityLevel.TRACE, 8]135]);136 137/** @public */138export const MongoLoggableComponent = Object.freeze({139 COMMAND: 'command',140 TOPOLOGY: 'topology',141 SERVER_SELECTION: 'serverSelection',142 CONNECTION: 'connection',143 CLIENT: 'client'144} as const);145 146/** @public */147export type MongoLoggableComponent =148 (typeof MongoLoggableComponent)[keyof typeof MongoLoggableComponent];149 150/** @internal */151export interface MongoLoggerEnvOptions {152 /** Severity level for command component */153 MONGODB_LOG_COMMAND?: string;154 /** Severity level for topology component */155 MONGODB_LOG_TOPOLOGY?: string;156 /** Severity level for server selection component */157 MONGODB_LOG_SERVER_SELECTION?: string;158 /** Severity level for CMAP */159 MONGODB_LOG_CONNECTION?: string;160 /** Severity level for client */161 MONGODB_LOG_CLIENT?: string;162 /** Default severity level to be if any of the above are unset */163 MONGODB_LOG_ALL?: string;164 /** Max length of embedded EJSON docs. Setting to 0 disables truncation. Defaults to 1000. */165 MONGODB_LOG_MAX_DOCUMENT_LENGTH?: string;166 /** Destination for log messages. Must be 'stderr', 'stdout'. Defaults to 'stderr'. */167 MONGODB_LOG_PATH?: string;168}169 170/** @public */171export interface LogComponentSeveritiesClientOptions {172 /** Optional severity level for command component */173 command?: SeverityLevel;174 /** Optional severity level for topology component */175 topology?: SeverityLevel;176 /** Optional severity level for server selection component */177 serverSelection?: SeverityLevel;178 /** Optional severity level for connection component */179 connection?: SeverityLevel;180 /** Optional severity level for client component */181 client?: SeverityLevel;182 /** Optional default severity level to be used if any of the above are unset */183 default?: SeverityLevel;184}185 186/** @internal */187export interface MongoLoggerMongoClientOptions {188 /** Destination for log messages */189 mongodbLogPath?: 'stdout' | 'stderr' | MongoDBLogWritable;190 /** Severity levels for logger components */191 mongodbLogComponentSeverities?: LogComponentSeveritiesClientOptions;192 /** Max length of embedded EJSON docs. Setting to 0 disables truncation. Defaults to 1000. */193 mongodbLogMaxDocumentLength?: number;194}195 196/** @internal */197export interface MongoLoggerOptions {198 componentSeverities: {199 /** Severity level for command component */200 command: SeverityLevel;201 /** Severity level for topology component */202 topology: SeverityLevel;203 /** Severity level for server selection component */204 serverSelection: SeverityLevel;205 /** Severity level for connection component */206 connection: SeverityLevel;207 /** Severity level for client component */208 client: SeverityLevel;209 /** Default severity level to be used if any of the above are unset */210 default: SeverityLevel;211 };212 /** Max length of embedded EJSON docs. Setting to 0 disables truncation. Defaults to 1000. */213 maxDocumentLength: number;214 /** Destination for log messages. */215 logDestination: MongoDBLogWritable;216 /** For internal check to see if error should stop logging. */217 logDestinationIsStdErr: boolean;218}219 220/**221 * Parses a string as one of SeverityLevel222 * @internal223 *224 * @param s - the value to be parsed225 * @returns one of SeverityLevel if value can be parsed as such, otherwise null226 */227export function parseSeverityFromString(s?: string): SeverityLevel | null {228 const validSeverities: string[] = Object.values(SeverityLevel);229 const lowerSeverity = s?.toLowerCase();230 231 if (lowerSeverity != null && validSeverities.includes(lowerSeverity)) {232 return lowerSeverity as SeverityLevel;233 }234 235 return null;236}237 238/** @internal */239export function createStdioLogger(stream: {240 write: NodeJS.WriteStream['write'];241}): MongoDBLogWritable {242 return {243 write: promisify((log: Log, cb: (error?: Error) => void): unknown => {244 const logLine = inspect(log, { compact: true, breakLength: Infinity });245 stream.write(`${logLine}\n`, 'utf-8', cb);246 return;247 })248 };249}250 251/**252 * resolves the MONGODB_LOG_PATH and mongodbLogPath options from the environment and the253 * mongo client options respectively. The mongodbLogPath can be either 'stdout', 'stderr', a NodeJS254 * Writable or an object which has a `write` method with the signature:255 * ```ts256 * write(log: Log): void257 * ```258 *259 * @returns the MongoDBLogWritable object to write logs to260 */261function resolveLogPath(262 { MONGODB_LOG_PATH }: MongoLoggerEnvOptions,263 { mongodbLogPath }: MongoLoggerMongoClientOptions264): { mongodbLogPath: MongoDBLogWritable; mongodbLogPathIsStdErr: boolean } {265 if (typeof mongodbLogPath === 'string' && /^stderr$/i.test(mongodbLogPath)) {266 return { mongodbLogPath: createStdioLogger(process.stderr), mongodbLogPathIsStdErr: true };267 }268 if (typeof mongodbLogPath === 'string' && /^stdout$/i.test(mongodbLogPath)) {269 return { mongodbLogPath: createStdioLogger(process.stdout), mongodbLogPathIsStdErr: false };270 }271 272 if (typeof mongodbLogPath === 'object' && typeof mongodbLogPath?.write === 'function') {273 return { mongodbLogPath: mongodbLogPath, mongodbLogPathIsStdErr: false };274 }275 276 if (MONGODB_LOG_PATH && /^stderr$/i.test(MONGODB_LOG_PATH)) {277 return { mongodbLogPath: createStdioLogger(process.stderr), mongodbLogPathIsStdErr: true };278 }279 if (MONGODB_LOG_PATH && /^stdout$/i.test(MONGODB_LOG_PATH)) {280 return { mongodbLogPath: createStdioLogger(process.stdout), mongodbLogPathIsStdErr: false };281 }282 283 return { mongodbLogPath: createStdioLogger(process.stderr), mongodbLogPathIsStdErr: true };284}285 286function resolveSeverityConfiguration(287 clientOption: string | undefined,288 environmentOption: string | undefined,289 defaultSeverity: SeverityLevel290): SeverityLevel {291 return (292 parseSeverityFromString(clientOption) ??293 parseSeverityFromString(environmentOption) ??294 defaultSeverity295 );296}297 298/** @public */299export interface Log extends Record<string, any> {300 t: Date;301 c: MongoLoggableComponent;302 s: SeverityLevel;303 message?: string;304}305 306/**307 * @public308 *309 * A custom destination for structured logging messages.310 */311export interface MongoDBLogWritable {312 /**313 * This function will be called for every enabled log message.314 *315 * It can be sync or async:316 * - If it is synchronous it will block the driver from proceeding until this method returns.317 * - If it is asynchronous the driver will not await the returned promise. It will attach fulfillment handling (`.then`).318 * If the promise rejects the logger will write an error message to stderr and stop functioning.319 * If the promise resolves the driver proceeds to the next log message (or waits for new ones to occur).320 *321 * Tips:322 * - We recommend writing an async `write` function that _never_ rejects.323 * Instead handle logging errors as necessary to your use case and make the write function a noop, until it can be recovered.324 * - The Log messages are structured but **subject to change** since the intended purpose is informational.325 * Program against this defensively and err on the side of stringifying whatever is passed in to write in some form or another.326 *327 */328 write(log: Log): PromiseLike<unknown> | unknown;329}330 331function compareSeverity(s0: SeverityLevel, s1: SeverityLevel): 1 | 0 | -1 {332 const s0Num = SEVERITY_LEVEL_MAP.getNumericSeverityLevel(s0);333 const s1Num = SEVERITY_LEVEL_MAP.getNumericSeverityLevel(s1);334 335 return s0Num < s1Num ? -1 : s0Num > s1Num ? 1 : 0;336}337 338/**339 * @internal340 * Must be separate from Events API due to differences in spec requirements for logging a command success341 */342export type LoggableCommandSucceededEvent = {343 address: string;344 connectionId?: string | number;345 requestId: number;346 duration: number;347 commandName: string;348 reply: Document | undefined;349 serviceId?: ObjectId;350 name: typeof COMMAND_SUCCEEDED;351 serverConnectionId: bigint | null;352 databaseName: string;353};354 355/**356 * @internal357 * Must be separate from Events API due to differences in spec requirements for logging a command failure358 */359export type LoggableCommandFailedEvent = {360 address: string;361 connectionId?: string | number;362 requestId: number;363 duration: number;364 commandName: string;365 failure: Error;366 serviceId?: ObjectId;367 name: typeof COMMAND_FAILED;368 serverConnectionId: bigint | null;369 databaseName: string;370};371 372/**373 * @internal374 * Must be separate from Events API due to differences in spec requirements for logging server heartbeat beginning375 */376export type LoggableServerHeartbeatStartedEvent = {377 topologyId: number;378 awaited: boolean;379 connectionId: string;380 name: typeof SERVER_HEARTBEAT_STARTED;381};382 383/**384 * @internal385 * Must be separate from Events API due to differences in spec requirements for logging server heartbeat success386 */387export type LoggableServerHeartbeatSucceededEvent = {388 topologyId: number;389 awaited: boolean;390 connectionId: string;391 reply: Document;392 serverConnectionId: number | '<monitor>';393 duration: number;394 name: typeof SERVER_HEARTBEAT_SUCCEEDED;395};396 397/**398 * @internal399 * Must be separate from Events API due to differences in spec requirements for logging server heartbeat failure400 */401export type LoggableServerHeartbeatFailedEvent = {402 topologyId: number;403 awaited: boolean;404 connectionId: string;405 failure: Error;406 duration: number;407 name: typeof SERVER_HEARTBEAT_FAILED;408};409 410type SDAMLoggableEvent =411 | ServerClosedEvent412 | LoggableServerHeartbeatFailedEvent413 | LoggableServerHeartbeatStartedEvent414 | LoggableServerHeartbeatSucceededEvent415 | ServerOpeningEvent416 | TopologyClosedEvent417 | TopologyDescriptionChangedEvent418 | TopologyOpeningEvent;419 420/** @internal */421export type LoggableEvent =422 | ServerSelectionStartedEvent423 | ServerSelectionFailedEvent424 | ServerSelectionSucceededEvent425 | WaitingForSuitableServerEvent426 | CommandStartedEvent427 | LoggableCommandSucceededEvent428 | LoggableCommandFailedEvent429 | ConnectionPoolCreatedEvent430 | ConnectionPoolReadyEvent431 | ConnectionPoolClosedEvent432 | ConnectionPoolClearedEvent433 | ConnectionCreatedEvent434 | ConnectionReadyEvent435 | ConnectionClosedEvent436 | ConnectionCheckedInEvent437 | ConnectionCheckedOutEvent438 | ConnectionCheckOutStartedEvent439 | ConnectionCheckOutFailedEvent440 | ServerClosedEvent441 | LoggableServerHeartbeatFailedEvent442 | LoggableServerHeartbeatStartedEvent443 | LoggableServerHeartbeatSucceededEvent444 | ServerOpeningEvent445 | TopologyClosedEvent446 | TopologyDescriptionChangedEvent447 | TopologyOpeningEvent;448 449/** @internal */450export interface LogConvertible extends Record<string, any> {451 toLog(): Record<string, any>;452}453 454type BSONObject =455 | BSONRegExp456 | BSONSymbol457 | Code458 | DBRef459 | Decimal128460 | Double461 | Int32462 | Long463 | MaxKey464 | MinKey465 | ObjectId466 | Timestamp467 | Binary;468/** @internal */469export function stringifyWithMaxLen(470 value: any,471 maxDocumentLength: number,472 options: EJSONOptions = {}473): string {474 let strToTruncate = '';475 476 let currentLength = 0;477 const maxDocumentLengthEnsurer = function maxDocumentLengthEnsurer(key: string, value: any) {478 if (currentLength >= maxDocumentLength) {479 return undefined;480 }481 // Account for root document482 if (key === '') {483 // Account for starting brace484 currentLength += 1;485 return value;486 }487 488 // +4 accounts for 2 quotation marks, colon and comma after value489 // Note that this potentially undercounts since it does not account for escape sequences which490 // will have an additional backslash added to them once passed through JSON.stringify.491 currentLength += key.length + 4;492 493 if (value == null) return value;494 495 switch (typeof value) {496 case 'string':497 // +2 accounts for quotes498 // Note that this potentially undercounts similarly to the key length calculation499 currentLength += value.length + 2;500 break;501 case 'number':502 case 'bigint':503 currentLength += String(value).length;504 break;505 case 'boolean':506 currentLength += value ? 4 : 5;507 break;508 case 'object':509 if (isUint8Array(value)) {510 // '{"$binary":{"base64":"<base64 string>","subType":"XX"}}'511 // This is an estimate based on the fact that the base64 is approximately 1.33x the length of512 // the actual binary sequence https://en.wikipedia.org/wiki/Base64513 currentLength += (22 + value.byteLength + value.byteLength * 0.33 + 18) | 0;514 } else if ('_bsontype' in value) {515 const v = value as BSONObject;516 switch (v._bsontype) {517 case 'Int32':518 currentLength += String(v.value).length;519 break;520 case 'Double':521 // Account for representing integers as <value>.0522 currentLength +=523 (v.value | 0) === v.value ? String(v.value).length + 2 : String(v.value).length;524 break;525 case 'Long':526 currentLength += v.toString().length;527 break;528 case 'ObjectId':529 // '{"$oid":"XXXXXXXXXXXXXXXXXXXXXXXX"}'530 currentLength += 35;531 break;532 case 'MaxKey':533 case 'MinKey':534 // '{"$maxKey":1}' or '{"$minKey":1}'535 currentLength += 13;536 break;537 case 'Binary':538 // '{"$binary":{"base64":"<base64 string>","subType":"XX"}}'539 // This is an estimate based on the fact that the base64 is approximately 1.33x the length of540 // the actual binary sequence https://en.wikipedia.org/wiki/Base64541 currentLength += (22 + value.position + value.position * 0.33 + 18) | 0;542 break;543 case 'Timestamp':544 // '{"$timestamp":{"t":<t>,"i":<i>}}'545 currentLength += 19 + String(v.t).length + 5 + String(v.i).length + 2;546 break;547 case 'Code':548 // '{"$code":"<code>"}' or '{"$code":"<code>","$scope":<scope>}'549 if (v.scope == null) {550 currentLength += v.code.length + 10 + 2;551 } else {552 // Ignoring actual scope object, so this undercounts by a significant amount553 currentLength += v.code.length + 10 + 11;554 }555 break;556 case 'BSONRegExp':557 // '{"$regularExpression":{"pattern":"<pattern>","options":"<options>"}}'558 currentLength += 34 + v.pattern.length + 13 + v.options.length + 3;559 break;560 }561 }562 }563 return value;564 };565 566 if (typeof value === 'string') {567 strToTruncate = value;568 } else if (typeof value === 'function') {569 strToTruncate = value.name;570 } else {571 try {572 if (maxDocumentLength !== 0) {573 strToTruncate = EJSON.stringify(value, maxDocumentLengthEnsurer, 0, options);574 } else {575 strToTruncate = EJSON.stringify(value, options);576 }577 } catch (e) {578 strToTruncate = `Extended JSON serialization failed with: ${e.message}`;579 }580 }581 582 // handle truncation that occurs in the middle of multi-byte codepoints583 if (584 maxDocumentLength !== 0 &&585 strToTruncate.length > maxDocumentLength &&586 strToTruncate.charCodeAt(maxDocumentLength - 1) !==587 strToTruncate.codePointAt(maxDocumentLength - 1)588 ) {589 maxDocumentLength--;590 if (maxDocumentLength === 0) {591 return '';592 }593 }594 595 return maxDocumentLength !== 0 && strToTruncate.length > maxDocumentLength596 ? `${strToTruncate.slice(0, maxDocumentLength)}...`597 : strToTruncate;598}599 600/** @internal */601export type Loggable = LoggableEvent | LogConvertible;602 603function isLogConvertible(obj: Loggable): obj is LogConvertible {604 const objAsLogConvertible = obj as LogConvertible;605 // eslint-disable-next-line no-restricted-syntax606 return objAsLogConvertible.toLog !== undefined && typeof objAsLogConvertible.toLog === 'function';607}608 609function attachServerSelectionFields(610 log: Record<string, any>,611 serverSelectionEvent: ServerSelectionEvent,612 maxDocumentLength: number = DEFAULT_MAX_DOCUMENT_LENGTH613) {614 const { selector, operation, topologyDescription, message } = serverSelectionEvent;615 log.selector = stringifyWithMaxLen(selector, maxDocumentLength);616 log.operation = operation;617 log.topologyDescription = stringifyWithMaxLen(topologyDescription, maxDocumentLength);618 log.message = message;619 620 return log;621}622 623function attachCommandFields(624 log: Record<string, any>,625 commandEvent: CommandStartedEvent | LoggableCommandSucceededEvent | LoggableCommandFailedEvent626) {627 log.commandName = commandEvent.commandName;628 log.requestId = commandEvent.requestId;629 log.driverConnectionId = commandEvent.connectionId;630 const { host, port } = HostAddress.fromString(commandEvent.address).toHostPort();631 log.serverHost = host;632 log.serverPort = port;633 if (commandEvent?.serviceId) {634 log.serviceId = commandEvent.serviceId.toHexString();635 }636 log.databaseName = commandEvent.databaseName;637 log.serverConnectionId = commandEvent.serverConnectionId;638 639 return log;640}641 642function attachConnectionFields(log: Record<string, any>, event: any) {643 const { host, port } = HostAddress.fromString(event.address).toHostPort();644 log.serverHost = host;645 log.serverPort = port;646 647 return log;648}649 650function attachSDAMFields(log: Record<string, any>, sdamEvent: SDAMLoggableEvent) {651 log.topologyId = sdamEvent.topologyId;652 return log;653}654 655function attachServerHeartbeatFields(656 log: Record<string, any>,657 serverHeartbeatEvent:658 | LoggableServerHeartbeatFailedEvent659 | LoggableServerHeartbeatStartedEvent660 | LoggableServerHeartbeatSucceededEvent661) {662 const { awaited, connectionId } = serverHeartbeatEvent;663 log.awaited = awaited;664 log.driverConnectionId = serverHeartbeatEvent.connectionId;665 const { host, port } = HostAddress.fromString(connectionId).toHostPort();666 log.serverHost = host;667 log.serverPort = port;668 return log;669}670 671/** @internal */672export function defaultLogTransform(673 logObject: LoggableEvent | Record<string, any>,674 maxDocumentLength: number = DEFAULT_MAX_DOCUMENT_LENGTH675): Omit<Log, 's' | 't' | 'c'> {676 let log: Omit<Log, 's' | 't' | 'c'> = Object.create(null);677 678 switch (logObject.name) {679 case SERVER_SELECTION_STARTED:680 log = attachServerSelectionFields(log, logObject, maxDocumentLength);681 return log;682 case SERVER_SELECTION_FAILED:683 log = attachServerSelectionFields(log, logObject, maxDocumentLength);684 log.failure = logObject.failure?.message;685 return log;686 case SERVER_SELECTION_SUCCEEDED:687 log = attachServerSelectionFields(log, logObject, maxDocumentLength);688 log.serverHost = logObject.serverHost;689 log.serverPort = logObject.serverPort;690 return log;691 case WAITING_FOR_SUITABLE_SERVER:692 log = attachServerSelectionFields(log, logObject, maxDocumentLength);693 log.remainingTimeMS = logObject.remainingTimeMS;694 return log;695 case COMMAND_STARTED:696 log = attachCommandFields(log, logObject);697 log.message = 'Command started';698 log.command = stringifyWithMaxLen(logObject.command, maxDocumentLength, { relaxed: true });699 log.databaseName = logObject.databaseName;700 return log;701 case COMMAND_SUCCEEDED:702 log = attachCommandFields(log, logObject);703 log.message = 'Command succeeded';704 log.durationMS = logObject.duration;705 log.reply = stringifyWithMaxLen(logObject.reply, maxDocumentLength, { relaxed: true });706 return log;707 case COMMAND_FAILED:708 log = attachCommandFields(log, logObject);709 log.message = 'Command failed';710 log.durationMS = logObject.duration;711 log.failure = logObject.failure?.message ?? '(redacted)';712 return log;713 case CONNECTION_POOL_CREATED:714 log = attachConnectionFields(log, logObject);715 log.message = 'Connection pool created';716 if (logObject.options) {717 const { maxIdleTimeMS, minPoolSize, maxPoolSize, maxConnecting, waitQueueTimeoutMS } =718 logObject.options;719 log = {720 ...log,721 maxIdleTimeMS,722 minPoolSize,723 maxPoolSize,724 maxConnecting,725 waitQueueTimeoutMS726 };727 }728 return log;729 case CONNECTION_POOL_READY:730 log = attachConnectionFields(log, logObject);731 log.message = 'Connection pool ready';732 return log;733 case CONNECTION_POOL_CLEARED:734 log = attachConnectionFields(log, logObject);735 log.message = 'Connection pool cleared';736 if (logObject.serviceId?._bsontype === 'ObjectId') {737 log.serviceId = logObject.serviceId?.toHexString();738 }739 return log;740 case CONNECTION_POOL_CLOSED:741 log = attachConnectionFields(log, logObject);742 log.message = 'Connection pool closed';743 return log;744 case CONNECTION_CREATED:745 log = attachConnectionFields(log, logObject);746 log.message = 'Connection created';747 log.driverConnectionId = logObject.connectionId;748 return log;749 case CONNECTION_READY:750 log = attachConnectionFields(log, logObject);751 log.message = 'Connection ready';752 log.driverConnectionId = logObject.connectionId;753 log.durationMS = logObject.durationMS;754 return log;755 case CONNECTION_CLOSED:756 log = attachConnectionFields(log, logObject);757 log.message = 'Connection closed';758 log.driverConnectionId = logObject.connectionId;759 switch (logObject.reason) {760 case 'stale':761 log.reason = 'Connection became stale because the pool was cleared';762 break;763 case 'idle':764 log.reason =765 'Connection has been available but unused for longer than the configured max idle time';766 break;767 case 'error':768 log.reason = 'An error occurred while using the connection';769 if (logObject.error) {770 log.error = logObject.error;771 }772 break;773 case 'poolClosed':774 log.reason = 'Connection pool was closed';775 break;776 default:777 log.reason = `Unknown close reason: ${logObject.reason}`;778 }779 return log;780 case CONNECTION_CHECK_OUT_STARTED:781 log = attachConnectionFields(log, logObject);782 log.message = 'Connection checkout started';783 return log;784 case CONNECTION_CHECK_OUT_FAILED:785 log = attachConnectionFields(log, logObject);786 log.message = 'Connection checkout failed';787 switch (logObject.reason) {788 case 'poolClosed':789 log.reason = 'Connection pool was closed';790 break;791 case 'timeout':792 log.reason = 'Wait queue timeout elapsed without a connection becoming available';793 break;794 case 'connectionError':795 log.reason = 'An error occurred while trying to establish a new connection';796 if (logObject.error) {797 log.error = logObject.error;798 }799 break;800 default:801 log.reason = `Unknown close reason: ${logObject.reason}`;802 }803 log.durationMS = logObject.durationMS;804 return log;805 case CONNECTION_CHECKED_OUT:806 log = attachConnectionFields(log, logObject);807 log.message = 'Connection checked out';808 log.driverConnectionId = logObject.connectionId;809 log.durationMS = logObject.durationMS;810 return log;811 case CONNECTION_CHECKED_IN:812 log = attachConnectionFields(log, logObject);813 log.message = 'Connection checked in';814 log.driverConnectionId = logObject.connectionId;815 return log;816 case SERVER_OPENING:817 log = attachSDAMFields(log, logObject);818 log = attachConnectionFields(log, logObject);819 log.message = 'Starting server monitoring';820 return log;821 case SERVER_CLOSED:822 log = attachSDAMFields(log, logObject);823 log = attachConnectionFields(log, logObject);824 log.message = 'Stopped server monitoring';825 return log;826 case SERVER_HEARTBEAT_STARTED:827 log = attachSDAMFields(log, logObject);828 log = attachServerHeartbeatFields(log, logObject);829 log.message = 'Server heartbeat started';830 return log;831 case SERVER_HEARTBEAT_SUCCEEDED:832 log = attachSDAMFields(log, logObject);833 log = attachServerHeartbeatFields(log, logObject);834 log.message = 'Server heartbeat succeeded';835 log.durationMS = logObject.duration;836 log.serverConnectionId = logObject.serverConnectionId;837 log.reply = stringifyWithMaxLen(logObject.reply, maxDocumentLength, { relaxed: true });838 return log;839 case SERVER_HEARTBEAT_FAILED:840 log = attachSDAMFields(log, logObject);841 log = attachServerHeartbeatFields(log, logObject);842 log.message = 'Server heartbeat failed';843 log.durationMS = logObject.duration;844 log.failure = logObject.failure?.message;845 return log;846 case TOPOLOGY_OPENING:847 log = attachSDAMFields(log, logObject);848 log.message = 'Starting topology monitoring';849 return log;850 case TOPOLOGY_CLOSED:851 log = attachSDAMFields(log, logObject);852 log.message = 'Stopped topology monitoring';853 return log;854 case TOPOLOGY_DESCRIPTION_CHANGED:855 log = attachSDAMFields(log, logObject);856 log.message = 'Topology description changed';857 log.previousDescription = log.reply = stringifyWithMaxLen(858 logObject.previousDescription,859 maxDocumentLength860 );861 log.newDescription = log.reply = stringifyWithMaxLen(862 logObject.newDescription,863 maxDocumentLength864 );865 return log;866 default:867 for (const [key, value] of Object.entries(logObject)) {868 if (value != null) log[key] = value;869 }870 }871 return log;872}873 874/** @internal */875export class MongoLogger {876 componentSeverities: Record<MongoLoggableComponent, SeverityLevel>;877 maxDocumentLength: number;878 logDestination: MongoDBLogWritable;879 logDestinationIsStdErr: boolean;880 pendingLog: PromiseLike<unknown> | unknown = null;881 private severities: Record<MongoLoggableComponent, Record<SeverityLevel, boolean>>;882 883 /**884 * This method should be used when logging errors that do not have a public driver API for885 * reporting errors.886 */887 error = this.log.bind(this, 'error');888 /**889 * This method should be used to log situations where undesirable application behaviour might890 * occur. For example, failing to end sessions on `MongoClient.close`.891 */892 warn = this.log.bind(this, 'warn');893 /**894 * This method should be used to report high-level information about normal driver behaviour.895 * For example, the creation of a `MongoClient`.896 */897 info = this.log.bind(this, 'info');898 /**899 * This method should be used to report information that would be helpful when debugging an900 * application. For example, a command starting, succeeding or failing.901 */902 debug = this.log.bind(this, 'debug');903 /**904 * This method should be used to report fine-grained details related to logic flow. For example,905 * entering and exiting a function body.906 */907 trace = this.log.bind(this, 'trace');908 909 constructor(options: MongoLoggerOptions) {910 this.componentSeverities = options.componentSeverities;911 this.maxDocumentLength = options.maxDocumentLength;912 this.logDestination = options.logDestination;913 this.logDestinationIsStdErr = options.logDestinationIsStdErr;914 this.severities = this.createLoggingSeverities();915 }916 917 createLoggingSeverities(): Record<MongoLoggableComponent, Record<SeverityLevel, boolean>> {918 const severities = Object();919 for (const component of Object.values(MongoLoggableComponent)) {920 severities[component] = {};921 for (const severityLevel of Object.values(SeverityLevel)) {922 severities[component][severityLevel] =923 compareSeverity(severityLevel, this.componentSeverities[component]) <= 0;924 }925 }926 return severities;927 }928 929 turnOffSeverities() {930 for (const component of Object.values(MongoLoggableComponent)) {931 this.componentSeverities[component] = SeverityLevel.OFF;932 for (const severityLevel of Object.values(SeverityLevel)) {933 this.severities[component][severityLevel] = false;934 }935 }936 }937 938 private logWriteFailureHandler(error: Error) {939 if (this.logDestinationIsStdErr) {940 this.turnOffSeverities();941 this.clearPendingLog();942 return;943 }944 this.logDestination = createStdioLogger(process.stderr);945 this.logDestinationIsStdErr = true;946 this.clearPendingLog();947 this.error(MongoLoggableComponent.CLIENT, {948 toLog: function () {949 return {950 message: 'User input for mongodbLogPath is now invalid. Logging is halted.',951 error: error.message952 };953 }954 });955 this.turnOffSeverities();956 this.clearPendingLog();957 }958 959 private clearPendingLog() {960 this.pendingLog = null;961 }962 963 willLog(component: MongoLoggableComponent, severity: SeverityLevel): boolean {964 if (severity === SeverityLevel.OFF) return false;965 return this.severities[component][severity];966 }967 968 private log(969 severity: SeverityLevel,970 component: MongoLoggableComponent,971 message: Loggable | string972 ): void {973 if (!this.willLog(component, severity)) return;974 975 let logMessage: Log = { t: new Date(), c: component, s: severity };976 if (typeof message === 'string') {977 logMessage.message = message;978 } else if (typeof message === 'object') {979 if (isLogConvertible(message)) {980 logMessage = { ...logMessage, ...message.toLog() };981 } else {982 logMessage = { ...logMessage, ...defaultLogTransform(message, this.maxDocumentLength) };983 }984 }985 986 if (isPromiseLike(this.pendingLog)) {987 this.pendingLog = this.pendingLog988 989 .then(() => this.logDestination.write(logMessage))990 991 .then(this.clearPendingLog.bind(this), this.logWriteFailureHandler.bind(this));992 return;993 }994 995 try {996 const logResult = this.logDestination.write(logMessage);997 if (isPromiseLike(logResult)) {998 this.pendingLog = logResult.then(999 this.clearPendingLog.bind(this),1000 this.logWriteFailureHandler.bind(this)1001 );1002 }1003 } catch (error) {1004 this.logWriteFailureHandler(error);1005 }1006 }1007 1008 /**1009 * Merges options set through environment variables and the MongoClient, preferring environment1010 * variables when both are set, and substituting defaults for values not set. Options set in1011 * constructor take precedence over both environment variables and MongoClient options.1012 *1013 * @remarks1014 * When parsing component severity levels, invalid values are treated as unset and replaced with1015 * the default severity.1016 *1017 * @param envOptions - options set for the logger from the environment1018 * @param clientOptions - options set for the logger in the MongoClient options1019 * @returns a MongoLoggerOptions object to be used when instantiating a new MongoLogger1020 */1021 static resolveOptions(1022 envOptions: MongoLoggerEnvOptions,1023 clientOptions: MongoLoggerMongoClientOptions1024 ): MongoLoggerOptions {1025 // client options take precedence over env options1026 const resolvedLogPath = resolveLogPath(envOptions, clientOptions);1027 const combinedOptions = {1028 ...envOptions,1029 ...clientOptions,1030 mongodbLogPath: resolvedLogPath.mongodbLogPath,1031 mongodbLogPathIsStdErr: resolvedLogPath.mongodbLogPathIsStdErr1032 };1033 const defaultSeverity = resolveSeverityConfiguration(1034 combinedOptions.mongodbLogComponentSeverities?.default,1035 combinedOptions.MONGODB_LOG_ALL,1036 SeverityLevel.OFF1037 );1038 1039 return {1040 componentSeverities: {1041 command: resolveSeverityConfiguration(1042 combinedOptions.mongodbLogComponentSeverities?.command,1043 combinedOptions.MONGODB_LOG_COMMAND,1044 defaultSeverity1045 ),1046 topology: resolveSeverityConfiguration(1047 combinedOptions.mongodbLogComponentSeverities?.topology,1048 combinedOptions.MONGODB_LOG_TOPOLOGY,1049 defaultSeverity1050 ),1051 serverSelection: resolveSeverityConfiguration(1052 combinedOptions.mongodbLogComponentSeverities?.serverSelection,1053 combinedOptions.MONGODB_LOG_SERVER_SELECTION,1054 defaultSeverity1055 ),1056 connection: resolveSeverityConfiguration(1057 combinedOptions.mongodbLogComponentSeverities?.connection,1058 combinedOptions.MONGODB_LOG_CONNECTION,1059 defaultSeverity1060 ),1061 client: resolveSeverityConfiguration(1062 combinedOptions.mongodbLogComponentSeverities?.client,1063 combinedOptions.MONGODB_LOG_CLIENT,1064 defaultSeverity1065 ),1066 default: defaultSeverity1067 },1068 maxDocumentLength:1069 combinedOptions.mongodbLogMaxDocumentLength ??1070 parseUnsignedInteger(combinedOptions.MONGODB_LOG_MAX_DOCUMENT_LENGTH) ??1071 1000,1072 logDestination: combinedOptions.mongodbLogPath,1073 logDestinationIsStdErr: combinedOptions.mongodbLogPathIsStdErr1074 };1075 }1076}1077 