Skip to content

feat(NODE-4848): Add runtime error handling to logging #3971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 24, 2024
89 changes: 71 additions & 18 deletions src/mongo_logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import type {
ServerSelectionSucceededEvent,
WaitingForSuitableServerEvent
} from './sdam/server_selection_events';
import { HostAddress, parseUnsignedInteger } from './utils';
import { HostAddress, isPromiseLike, parseUnsignedInteger } from './utils';

/** @internal */
export const SeverityLevel = Object.freeze({
Expand Down Expand Up @@ -197,11 +197,12 @@ export interface MongoLoggerOptions {

/**
* Parses a string as one of SeverityLevel
* @internal
*
* @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 {
export function parseSeverityFromString(s?: string): SeverityLevel | null {
const validSeverities: string[] = Object.values(SeverityLevel);
const lowerSeverity = s?.toLowerCase();

Expand Down Expand Up @@ -281,7 +282,7 @@ export interface Log extends Record<string, any> {

/** @internal */
export interface MongoDBLogWritable {
write(log: Log): void;
write(log: Log): any;
}

function compareSeverity(s0: SeverityLevel, s1: SeverityLevel): 1 | 0 | -1 {
Expand Down Expand Up @@ -415,10 +416,15 @@ export function stringifyWithMaxLen(
): string {
let strToTruncate = '';

if (typeof value === 'function') {
strToTruncate = value.toString();
} else {
strToTruncate = EJSON.stringify(value, options);
try {
strToTruncate =
typeof value !== 'function'
? EJSON.stringify(value, options)
: value.name !== ''
? value.name
: 'anonymous function';
} catch (e) {
return '... ESJON failed : Error ...';
}

return maxDocumentLength !== 0 && strToTruncate.length > maxDocumentLength
Expand Down Expand Up @@ -455,21 +461,21 @@ function attachCommandFields(
) {
log.commandName = commandEvent.commandName;
log.requestId = commandEvent.requestId;
log.driverConnectionId = commandEvent?.connectionId;
const { host, port } = HostAddress.fromString(commandEvent.address).toHostPort();
log.driverConnectionId = commandEvent.connectionId;
const { host, port } = HostAddress.fromString(commandEvent.address ?? '').toHostPort();
log.serverHost = host;
log.serverPort = port;
if (commandEvent?.serviceId) {
log.serviceId = commandEvent.serviceId.toHexString();
}
log.databaseName = commandEvent.databaseName;
log.serverConnectionId = commandEvent?.serverConnectionId;
log.serverConnectionId = commandEvent.serverConnectionId;

return log;
}

function attachConnectionFields(log: Record<string, any>, event: any) {
const { host, port } = HostAddress.fromString(event.address).toHostPort();
const { host, port } = HostAddress.fromString(event.address ?? '').toHostPort();
log.serverHost = host;
log.serverPort = port;

Expand All @@ -491,13 +497,14 @@ function attachServerHeartbeatFields(
const { awaited, connectionId } = serverHeartbeatEvent;
log.awaited = awaited;
log.driverConnectionId = serverHeartbeatEvent.connectionId;
const { host, port } = HostAddress.fromString(connectionId).toHostPort();
const { host, port } = HostAddress.fromString(connectionId ?? '').toHostPort();
log.serverHost = host;
log.serverPort = port;
return log;
}

function defaultLogTransform(
/** @internal */
export function defaultLogTransform(
logObject: LoggableEvent | Record<string, any>,
maxDocumentLength: number = DEFAULT_MAX_DOCUMENT_LENGTH
): Omit<Log, 's' | 't' | 'c'> {
Expand All @@ -509,7 +516,7 @@ function defaultLogTransform(
return log;
case SERVER_SELECTION_FAILED:
log = attachServerSelectionFields(log, logObject, maxDocumentLength);
log.failure = logObject.failure.message;
log.failure = logObject.failure?.message;
return log;
case SERVER_SELECTION_SUCCEEDED:
log = attachServerSelectionFields(log, logObject, maxDocumentLength);
Expand All @@ -536,7 +543,7 @@ function defaultLogTransform(
log = attachCommandFields(log, logObject);
log.message = 'Command failed';
log.durationMS = logObject.duration;
log.failure = logObject.failure.message ?? '(redacted)';
log.failure = logObject.failure?.message ?? '(redacted)';
return log;
case CONNECTION_POOL_CREATED:
log = attachConnectionFields(log, logObject);
Expand All @@ -562,7 +569,7 @@ function defaultLogTransform(
log = attachConnectionFields(log, logObject);
log.message = 'Connection pool cleared';
if (logObject.serviceId?._bsontype === 'ObjectId') {
log.serviceId = logObject.serviceId.toHexString();
log.serviceId = logObject.serviceId?.toHexString();
}
return log;
case CONNECTION_POOL_CLOSED:
Expand Down Expand Up @@ -666,7 +673,7 @@ function defaultLogTransform(
log = attachServerHeartbeatFields(log, logObject);
log.message = 'Server heartbeat failed';
log.durationMS = logObject.duration;
log.failure = logObject.failure.message;
log.failure = logObject.failure?.message;
return log;
case TOPOLOGY_OPENING:
log = attachSDAMFields(log, logObject);
Expand Down Expand Up @@ -701,6 +708,7 @@ export class MongoLogger {
componentSeverities: Record<MongoLoggableComponent, SeverityLevel>;
maxDocumentLength: number;
logDestination: MongoDBLogWritable | Writable;
pendingLog: any | undefined;

/**
* This method should be used when logging errors that do not have a public driver API for
Expand Down Expand Up @@ -732,12 +740,40 @@ export class MongoLogger {
this.componentSeverities = options.componentSeverities;
this.maxDocumentLength = options.maxDocumentLength;
this.logDestination = options.logDestination;
this.pendingLog = undefined;
}

willLog(severity: SeverityLevel, component: MongoLoggableComponent): boolean {
return compareSeverity(severity, this.componentSeverities[component]) <= 0;
}

turnOffSeverities() {
for (const key of Object.keys(MongoLoggableComponent)) {
this.componentSeverities[key as MongoLoggableComponent] = SeverityLevel.OFF;
}
}

private logWriteFailureHandler(error: Error) {
try {
const tempLogDestination = createStdioLogger(process.stderr);
tempLogDestination.write({
t: new Date(),
c: MongoLoggableComponent.CLIENT,
s: SeverityLevel.ERROR,
message: `User input for mongodbLogPath is now invalid. Now logging to stderr.`,
error: error.message
});
this.logDestination = tempLogDestination;
} catch (e) {
this.turnOffSeverities();
}
this.clearPendingLog();
}

private clearPendingLog() {
this.pendingLog = undefined;
}

private log(
severity: SeverityLevel,
component: MongoLoggableComponent,
Expand All @@ -755,7 +791,24 @@ export class MongoLogger {
logMessage = { ...logMessage, ...defaultLogTransform(message, this.maxDocumentLength) };
}
}
this.logDestination.write(logMessage);

if (isPromiseLike(this.pendingLog)) {
this.pendingLog = this.pendingLog
.then(() => this.logDestination.write(logMessage))
.then(this.clearPendingLog.bind(this), this.logWriteFailureHandler.bind(this));
return;
}

try {
const logResult = this.logDestination.write(logMessage);
if (isPromiseLike(logResult))
this.pendingLog = logResult.then(
this.clearPendingLog.bind(this),
this.logWriteFailureHandler.bind(this)
);
} catch (error) {
this.logWriteFailureHandler(error);
}
}

/**
Expand Down
Loading