Skip to content

Commit

Permalink
feat(NODE-4686): Add log messages to CLAM (#3955)
Browse files Browse the repository at this point in the history
Co-authored-by: Durran Jordan <durran@gmail.com>
Co-authored-by: Alena Khineika <alena.khineika@gmail.com>
  • Loading branch information
3 people authored Jan 19, 2024
1 parent 9b76a43 commit e3bfa30
Show file tree
Hide file tree
Showing 16 changed files with 407 additions and 39 deletions.
2 changes: 2 additions & 0 deletions src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export {
deserialize,
Document,
Double,
EJSON,
EJSONOptions,
Int32,
Long,
MaxKey,
Expand Down
4 changes: 4 additions & 0 deletions src/cmap/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ async function performInitialHandshake(
throw error;
}
}

// Connection establishment is socket creation (tcp handshake, tls handshake, MongoDB handshake (saslStart, saslContinue))
// Once connection is established, command logging can log events (if enabled)
conn.established = true;
}

export interface HandshakeDocument extends Document {
Expand Down
53 changes: 46 additions & 7 deletions src/cmap/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
MongoWriteConcernError
} from '../error';
import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client';
import { MongoLoggableComponent, type MongoLogger, SeverityLevel } from '../mongo_logger';
import { type CancellationToken, TypedEventEmitter } from '../mongo_types';
import type { ReadPreferenceLike } from '../read_preference';
import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions';
Expand Down Expand Up @@ -114,6 +115,8 @@ export interface ConnectionOptions
socketTimeoutMS?: number;
cancellationToken?: CancellationToken;
metadata: ClientMetadata;
/** @internal */
mongoLogger?: MongoLogger | undefined;
}

/** @internal */
Expand Down Expand Up @@ -165,6 +168,16 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
public delayedTimeoutId: NodeJS.Timeout | null = null;
public generation: number;
public readonly description: Readonly<StreamDescription>;
/**
* @public
* Represents if the connection has been established:
* - TCP handshake
* - TLS negotiated
* - mongodb handshake (saslStart, saslContinue), includes authentication
*
* Once connection is established, command logging can log events (if enabled)
*/
public established: boolean;

private lastUseTime: number;
private socketTimeoutMS: number;
Expand All @@ -174,6 +187,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
private messageStream: Readable;
private socketWrite: (buffer: Uint8Array) => Promise<void>;
private clusterTime: Document | null = null;
/** @internal */
override mongoLogger: MongoLogger | undefined;

/** @event */
static readonly COMMAND_STARTED = COMMAND_STARTED;
Expand All @@ -198,6 +213,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
this.socketTimeoutMS = options.socketTimeoutMS ?? 0;
this.monitorCommands = options.monitorCommands;
this.serverApi = options.serverApi;
this.mongoLogger = options.mongoLogger;
this.established = false;

this.description = new StreamDescription(this.address, options);
this.generation = options.generation;
Expand Down Expand Up @@ -258,6 +275,16 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
);
}

private get shouldEmitAndLogCommand(): boolean {
return (
(this.monitorCommands ||
(this.established &&
!this.authContext?.reauthenticating &&
this.mongoLogger?.willLog(SeverityLevel.DEBUG, MongoLoggableComponent.COMMAND))) ??
false
);
}

public markAvailable(): void {
this.lastUseTime = now();
}
Expand Down Expand Up @@ -441,10 +468,13 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
const message = this.prepareCommand(ns.db, command, options);

let started = 0;
if (this.monitorCommands) {
if (this.shouldEmitAndLogCommand) {
started = now();
this.emit(
this.emitAndLogCommand(
this.monitorCommands,
Connection.COMMAND_STARTED,
message.databaseName,
this.established,
new CommandStartedEvent(this, message, this.description.serverConnectionId)
);
}
Expand All @@ -464,9 +494,12 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
throw new MongoServerError(document);
}

if (this.monitorCommands) {
this.emit(
if (this.shouldEmitAndLogCommand) {
this.emitAndLogCommand(
this.monitorCommands,
Connection.COMMAND_SUCCEEDED,
message.databaseName,
this.established,
new CommandSucceededEvent(
this,
message,
Expand All @@ -481,10 +514,13 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
this.controller.signal.throwIfAborted();
}
} catch (error) {
if (this.monitorCommands) {
if (this.shouldEmitAndLogCommand) {
if (error.name === 'MongoWriteConcernError') {
this.emit(
this.emitAndLogCommand(
this.monitorCommands,
Connection.COMMAND_SUCCEEDED,
message.databaseName,
this.established,
new CommandSucceededEvent(
this,
message,
Expand All @@ -494,8 +530,11 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
)
);
} else {
this.emit(
this.emitAndLogCommand(
this.monitorCommands,
Connection.COMMAND_FAILED,
message.databaseName,
this.established,
new CommandFailedEvent(
this,
message,
Expand Down
3 changes: 2 additions & 1 deletion src/cmap/connection_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,8 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
...this.options,
id: this[kConnectionCounter].next().value,
generation: this[kGeneration],
cancellationToken: this[kCancellationToken]
cancellationToken: this[kCancellationToken],
mongoLogger: this.mongoLogger
};

this[kPending]++;
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ export const CONNECTION_CHECKED_OUT = 'connectionCheckedOut' as const;
/** @internal */
export const CONNECTION_CHECKED_IN = 'connectionCheckedIn' as const;
export const CLUSTER_TIME_RECEIVED = 'clusterTimeReceived' as const;
/** @internal */
export const COMMAND_STARTED = 'commandStarted' as const;
/** @internal */
export const COMMAND_SUCCEEDED = 'commandSucceeded' as const;
/** @internal */
export const COMMAND_FAILED = 'commandFailed' as const;
/** @internal */
export const SERVER_HEARTBEAT_STARTED = 'serverHeartbeatStarted' as const;
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ export type { StreamDescription, StreamDescriptionOptions } from './cmap/stream_
export type { CompressorName } from './cmap/wire_protocol/compression';
export type { CollectionOptions, CollectionPrivate, ModifyResult } from './collection';
export type {
COMMAND_FAILED,
COMMAND_STARTED,
COMMAND_SUCCEEDED,
CONNECTION_CHECK_OUT_FAILED,
CONNECTION_CHECK_OUT_STARTED,
CONNECTION_CHECKED_IN,
Expand Down Expand Up @@ -367,6 +370,8 @@ export type {
LogComponentSeveritiesClientOptions,
LogConvertible,
Loggable,
LoggableCommandFailedEvent,
LoggableCommandSucceededEvent,
LoggableEvent,
LoggableServerHeartbeatFailedEvent,
LoggableServerHeartbeatStartedEvent,
Expand Down
65 changes: 51 additions & 14 deletions src/mongo_logger.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { type Document, EJSON, type EJSONOptions } from 'bson';
import type { Writable } from 'stream';
import { inspect } from 'util';

import type {
CommandFailedEvent,
CommandStartedEvent,
CommandSucceededEvent
} from './cmap/command_monitoring_events';
import { type Document, EJSON, type EJSONOptions, type ObjectId } from './bson';
import type { CommandStartedEvent } from './cmap/command_monitoring_events';
import type {
ConnectionCheckedInEvent,
ConnectionCheckedOutEvent,
Expand Down Expand Up @@ -295,6 +291,40 @@ function compareSeverity(s0: SeverityLevel, s1: SeverityLevel): 1 | 0 | -1 {
return s0Num < s1Num ? -1 : s0Num > s1Num ? 1 : 0;
}

/**
* @internal
* Must be separate from Events API due to differences in spec requirements for logging a command success
*/
export type LoggableCommandSucceededEvent = {
address: string;
connectionId?: string | number;
requestId: number;
duration: number;
commandName: string;
reply: Document | undefined;
serviceId?: ObjectId;
name: typeof COMMAND_SUCCEEDED;
serverConnectionId: bigint | null;
databaseName: string;
};

/**
* @internal
* Must be separate from Events API due to differences in spec requirements for logging a command failure
*/
export type LoggableCommandFailedEvent = {
address: string;
connectionId?: string | number;
requestId: number;
duration: number;
commandName: string;
failure: Error;
serviceId?: ObjectId;
name: typeof COMMAND_FAILED;
serverConnectionId: bigint | null;
databaseName: string;
};

/**
* @internal
* Must be separate from Events API due to differences in spec requirements for logging server heartbeat beginning
Expand Down Expand Up @@ -350,8 +380,8 @@ export type LoggableEvent =
| ServerSelectionSucceededEvent
| WaitingForSuitableServerEvent
| CommandStartedEvent
| CommandSucceededEvent
| CommandFailedEvent
| LoggableCommandSucceededEvent
| LoggableCommandFailedEvent
| ConnectionPoolCreatedEvent
| ConnectionPoolReadyEvent
| ConnectionPoolClosedEvent
Expand Down Expand Up @@ -383,7 +413,8 @@ export function stringifyWithMaxLen(
maxDocumentLength: number,
options: EJSONOptions = {}
): string {
let strToTruncate: string;
let strToTruncate = '';

if (typeof value === 'function') {
strToTruncate = value.toString();
} else {
Expand Down Expand Up @@ -420,7 +451,7 @@ function attachServerSelectionFields(

function attachCommandFields(
log: Record<string, any>,
commandEvent: CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent
commandEvent: CommandStartedEvent | LoggableCommandSucceededEvent | LoggableCommandFailedEvent
) {
log.commandName = commandEvent.commandName;
log.requestId = commandEvent.requestId;
Expand All @@ -431,6 +462,8 @@ function attachCommandFields(
if (commandEvent?.serviceId) {
log.serviceId = commandEvent.serviceId.toHexString();
}
log.databaseName = commandEvent.databaseName;
log.serverConnectionId = commandEvent?.serverConnectionId;

return log;
}
Expand Down Expand Up @@ -490,20 +523,20 @@ function defaultLogTransform(
case COMMAND_STARTED:
log = attachCommandFields(log, logObject);
log.message = 'Command started';
log.command = stringifyWithMaxLen(logObject.command, maxDocumentLength);
log.command = stringifyWithMaxLen(logObject.command, maxDocumentLength, { relaxed: true });
log.databaseName = logObject.databaseName;
return log;
case COMMAND_SUCCEEDED:
log = attachCommandFields(log, logObject);
log.message = 'Command succeeded';
log.durationMS = logObject.duration;
log.reply = stringifyWithMaxLen(logObject.reply, maxDocumentLength);
log.reply = stringifyWithMaxLen(logObject.reply, maxDocumentLength, { relaxed: true });
return log;
case COMMAND_FAILED:
log = attachCommandFields(log, logObject);
log.message = 'Command failed';
log.durationMS = logObject.duration;
log.failure = logObject.failure;
log.failure = logObject.failure.message ?? '(redacted)';
return log;
case CONNECTION_POOL_CREATED:
log = attachConnectionFields(log, logObject);
Expand Down Expand Up @@ -701,12 +734,16 @@ export class MongoLogger {
this.logDestination = options.logDestination;
}

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

private log(
severity: SeverityLevel,
component: MongoLoggableComponent,
message: Loggable | string
): void {
if (compareSeverity(severity, this.componentSeverities[component]) > 0) return;
if (!this.willLog(severity, component)) return;

let logMessage: Log = { t: new Date(), c: component, s: severity };
if (typeof message === 'string') {
Expand Down
35 changes: 30 additions & 5 deletions src/mongo_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import type {
ObjectId,
Timestamp
} from './bson';
import type {
LoggableServerHeartbeatFailedEvent,
LoggableServerHeartbeatStartedEvent,
LoggableServerHeartbeatSucceededEvent,
import { type CommandStartedEvent } from './cmap/command_monitoring_events';
import {
type LoggableCommandFailedEvent,
type LoggableCommandSucceededEvent,
type LoggableServerHeartbeatFailedEvent,
type LoggableServerHeartbeatStartedEvent,
type LoggableServerHeartbeatSucceededEvent,
MongoLoggableComponent,
MongoLogger
type MongoLogger
} from './mongo_logger';
import type { Sort } from './sort';

Expand Down Expand Up @@ -442,6 +445,28 @@ export class TypedEventEmitter<Events extends EventsDescription> extends EventEm
this.mongoLogger?.debug(this.component, loggableHeartbeatEvent);
}
}
/** @internal */
emitAndLogCommand<EventKey extends keyof Events>(
monitorCommands: boolean,
event: EventKey | symbol,
databaseName: string,
connectionEstablished: boolean,
...args: Parameters<Events[EventKey]>
): void {
if (monitorCommands) {
this.emit(event, ...args);
}
if (connectionEstablished) {
const loggableCommandEvent:
| CommandStartedEvent
| LoggableCommandFailedEvent
| LoggableCommandSucceededEvent = {
databaseName: databaseName,
...args[0]
};
this.mongoLogger?.debug(MongoLoggableComponent.COMMAND, loggableCommandEvent);
}
}
}

/** @public */
Expand Down
Loading

0 comments on commit e3bfa30

Please sign in to comment.