Skip to content

Commit e3bfa30

Browse files
aditi-khare-mongoDBdurranalenakhineika
authored
feat(NODE-4686): Add log messages to CLAM (#3955)
Co-authored-by: Durran Jordan <[email protected]> Co-authored-by: Alena Khineika <[email protected]>
1 parent 9b76a43 commit e3bfa30

16 files changed

+407
-39
lines changed

src/bson.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export {
1313
deserialize,
1414
Document,
1515
Double,
16+
EJSON,
17+
EJSONOptions,
1618
Int32,
1719
Long,
1820
MaxKey,

src/cmap/connect.ts

+4
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ async function performInitialHandshake(
186186
throw error;
187187
}
188188
}
189+
190+
// Connection establishment is socket creation (tcp handshake, tls handshake, MongoDB handshake (saslStart, saslContinue))
191+
// Once connection is established, command logging can log events (if enabled)
192+
conn.established = true;
189193
}
190194

191195
export interface HandshakeDocument extends Document {

src/cmap/connection.ts

+46-7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
MongoWriteConcernError
2525
} from '../error';
2626
import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client';
27+
import { MongoLoggableComponent, type MongoLogger, SeverityLevel } from '../mongo_logger';
2728
import { type CancellationToken, TypedEventEmitter } from '../mongo_types';
2829
import type { ReadPreferenceLike } from '../read_preference';
2930
import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions';
@@ -114,6 +115,8 @@ export interface ConnectionOptions
114115
socketTimeoutMS?: number;
115116
cancellationToken?: CancellationToken;
116117
metadata: ClientMetadata;
118+
/** @internal */
119+
mongoLogger?: MongoLogger | undefined;
117120
}
118121

119122
/** @internal */
@@ -165,6 +168,16 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
165168
public delayedTimeoutId: NodeJS.Timeout | null = null;
166169
public generation: number;
167170
public readonly description: Readonly<StreamDescription>;
171+
/**
172+
* @public
173+
* Represents if the connection has been established:
174+
* - TCP handshake
175+
* - TLS negotiated
176+
* - mongodb handshake (saslStart, saslContinue), includes authentication
177+
*
178+
* Once connection is established, command logging can log events (if enabled)
179+
*/
180+
public established: boolean;
168181

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

178193
/** @event */
179194
static readonly COMMAND_STARTED = COMMAND_STARTED;
@@ -198,6 +213,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
198213
this.socketTimeoutMS = options.socketTimeoutMS ?? 0;
199214
this.monitorCommands = options.monitorCommands;
200215
this.serverApi = options.serverApi;
216+
this.mongoLogger = options.mongoLogger;
217+
this.established = false;
201218

202219
this.description = new StreamDescription(this.address, options);
203220
this.generation = options.generation;
@@ -258,6 +275,16 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
258275
);
259276
}
260277

278+
private get shouldEmitAndLogCommand(): boolean {
279+
return (
280+
(this.monitorCommands ||
281+
(this.established &&
282+
!this.authContext?.reauthenticating &&
283+
this.mongoLogger?.willLog(SeverityLevel.DEBUG, MongoLoggableComponent.COMMAND))) ??
284+
false
285+
);
286+
}
287+
261288
public markAvailable(): void {
262289
this.lastUseTime = now();
263290
}
@@ -441,10 +468,13 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
441468
const message = this.prepareCommand(ns.db, command, options);
442469

443470
let started = 0;
444-
if (this.monitorCommands) {
471+
if (this.shouldEmitAndLogCommand) {
445472
started = now();
446-
this.emit(
473+
this.emitAndLogCommand(
474+
this.monitorCommands,
447475
Connection.COMMAND_STARTED,
476+
message.databaseName,
477+
this.established,
448478
new CommandStartedEvent(this, message, this.description.serverConnectionId)
449479
);
450480
}
@@ -464,9 +494,12 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
464494
throw new MongoServerError(document);
465495
}
466496

467-
if (this.monitorCommands) {
468-
this.emit(
497+
if (this.shouldEmitAndLogCommand) {
498+
this.emitAndLogCommand(
499+
this.monitorCommands,
469500
Connection.COMMAND_SUCCEEDED,
501+
message.databaseName,
502+
this.established,
470503
new CommandSucceededEvent(
471504
this,
472505
message,
@@ -481,10 +514,13 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
481514
this.controller.signal.throwIfAborted();
482515
}
483516
} catch (error) {
484-
if (this.monitorCommands) {
517+
if (this.shouldEmitAndLogCommand) {
485518
if (error.name === 'MongoWriteConcernError') {
486-
this.emit(
519+
this.emitAndLogCommand(
520+
this.monitorCommands,
487521
Connection.COMMAND_SUCCEEDED,
522+
message.databaseName,
523+
this.established,
488524
new CommandSucceededEvent(
489525
this,
490526
message,
@@ -494,8 +530,11 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
494530
)
495531
);
496532
} else {
497-
this.emit(
533+
this.emitAndLogCommand(
534+
this.monitorCommands,
498535
Connection.COMMAND_FAILED,
536+
message.databaseName,
537+
this.established,
499538
new CommandFailedEvent(
500539
this,
501540
message,

src/cmap/connection_pool.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,8 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
699699
...this.options,
700700
id: this[kConnectionCounter].next().value,
701701
generation: this[kGeneration],
702-
cancellationToken: this[kCancellationToken]
702+
cancellationToken: this[kCancellationToken],
703+
mongoLogger: this.mongoLogger
703704
};
704705

705706
this[kPending]++;

src/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ export const CONNECTION_CHECKED_OUT = 'connectionCheckedOut' as const;
6060
/** @internal */
6161
export const CONNECTION_CHECKED_IN = 'connectionCheckedIn' as const;
6262
export const CLUSTER_TIME_RECEIVED = 'clusterTimeReceived' as const;
63+
/** @internal */
6364
export const COMMAND_STARTED = 'commandStarted' as const;
65+
/** @internal */
6466
export const COMMAND_SUCCEEDED = 'commandSucceeded' as const;
67+
/** @internal */
6568
export const COMMAND_FAILED = 'commandFailed' as const;
6669
/** @internal */
6770
export const SERVER_HEARTBEAT_STARTED = 'serverHeartbeatStarted' as const;

src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,9 @@ export type { StreamDescription, StreamDescriptionOptions } from './cmap/stream_
292292
export type { CompressorName } from './cmap/wire_protocol/compression';
293293
export type { CollectionOptions, CollectionPrivate, ModifyResult } from './collection';
294294
export type {
295+
COMMAND_FAILED,
296+
COMMAND_STARTED,
297+
COMMAND_SUCCEEDED,
295298
CONNECTION_CHECK_OUT_FAILED,
296299
CONNECTION_CHECK_OUT_STARTED,
297300
CONNECTION_CHECKED_IN,
@@ -367,6 +370,8 @@ export type {
367370
LogComponentSeveritiesClientOptions,
368371
LogConvertible,
369372
Loggable,
373+
LoggableCommandFailedEvent,
374+
LoggableCommandSucceededEvent,
370375
LoggableEvent,
371376
LoggableServerHeartbeatFailedEvent,
372377
LoggableServerHeartbeatStartedEvent,

src/mongo_logger.ts

+51-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import { type Document, EJSON, type EJSONOptions } from 'bson';
21
import type { Writable } from 'stream';
32
import { inspect } from 'util';
43

5-
import type {
6-
CommandFailedEvent,
7-
CommandStartedEvent,
8-
CommandSucceededEvent
9-
} from './cmap/command_monitoring_events';
4+
import { type Document, EJSON, type EJSONOptions, type ObjectId } from './bson';
5+
import type { CommandStartedEvent } from './cmap/command_monitoring_events';
106
import type {
117
ConnectionCheckedInEvent,
128
ConnectionCheckedOutEvent,
@@ -295,6 +291,40 @@ function compareSeverity(s0: SeverityLevel, s1: SeverityLevel): 1 | 0 | -1 {
295291
return s0Num < s1Num ? -1 : s0Num > s1Num ? 1 : 0;
296292
}
297293

294+
/**
295+
* @internal
296+
* Must be separate from Events API due to differences in spec requirements for logging a command success
297+
*/
298+
export type LoggableCommandSucceededEvent = {
299+
address: string;
300+
connectionId?: string | number;
301+
requestId: number;
302+
duration: number;
303+
commandName: string;
304+
reply: Document | undefined;
305+
serviceId?: ObjectId;
306+
name: typeof COMMAND_SUCCEEDED;
307+
serverConnectionId: bigint | null;
308+
databaseName: string;
309+
};
310+
311+
/**
312+
* @internal
313+
* Must be separate from Events API due to differences in spec requirements for logging a command failure
314+
*/
315+
export type LoggableCommandFailedEvent = {
316+
address: string;
317+
connectionId?: string | number;
318+
requestId: number;
319+
duration: number;
320+
commandName: string;
321+
failure: Error;
322+
serviceId?: ObjectId;
323+
name: typeof COMMAND_FAILED;
324+
serverConnectionId: bigint | null;
325+
databaseName: string;
326+
};
327+
298328
/**
299329
* @internal
300330
* Must be separate from Events API due to differences in spec requirements for logging server heartbeat beginning
@@ -350,8 +380,8 @@ export type LoggableEvent =
350380
| ServerSelectionSucceededEvent
351381
| WaitingForSuitableServerEvent
352382
| CommandStartedEvent
353-
| CommandSucceededEvent
354-
| CommandFailedEvent
383+
| LoggableCommandSucceededEvent
384+
| LoggableCommandFailedEvent
355385
| ConnectionPoolCreatedEvent
356386
| ConnectionPoolReadyEvent
357387
| ConnectionPoolClosedEvent
@@ -383,7 +413,8 @@ export function stringifyWithMaxLen(
383413
maxDocumentLength: number,
384414
options: EJSONOptions = {}
385415
): string {
386-
let strToTruncate: string;
416+
let strToTruncate = '';
417+
387418
if (typeof value === 'function') {
388419
strToTruncate = value.toString();
389420
} else {
@@ -420,7 +451,7 @@ function attachServerSelectionFields(
420451

421452
function attachCommandFields(
422453
log: Record<string, any>,
423-
commandEvent: CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent
454+
commandEvent: CommandStartedEvent | LoggableCommandSucceededEvent | LoggableCommandFailedEvent
424455
) {
425456
log.commandName = commandEvent.commandName;
426457
log.requestId = commandEvent.requestId;
@@ -431,6 +462,8 @@ function attachCommandFields(
431462
if (commandEvent?.serviceId) {
432463
log.serviceId = commandEvent.serviceId.toHexString();
433464
}
465+
log.databaseName = commandEvent.databaseName;
466+
log.serverConnectionId = commandEvent?.serverConnectionId;
434467

435468
return log;
436469
}
@@ -490,20 +523,20 @@ function defaultLogTransform(
490523
case COMMAND_STARTED:
491524
log = attachCommandFields(log, logObject);
492525
log.message = 'Command started';
493-
log.command = stringifyWithMaxLen(logObject.command, maxDocumentLength);
526+
log.command = stringifyWithMaxLen(logObject.command, maxDocumentLength, { relaxed: true });
494527
log.databaseName = logObject.databaseName;
495528
return log;
496529
case COMMAND_SUCCEEDED:
497530
log = attachCommandFields(log, logObject);
498531
log.message = 'Command succeeded';
499532
log.durationMS = logObject.duration;
500-
log.reply = stringifyWithMaxLen(logObject.reply, maxDocumentLength);
533+
log.reply = stringifyWithMaxLen(logObject.reply, maxDocumentLength, { relaxed: true });
501534
return log;
502535
case COMMAND_FAILED:
503536
log = attachCommandFields(log, logObject);
504537
log.message = 'Command failed';
505538
log.durationMS = logObject.duration;
506-
log.failure = logObject.failure;
539+
log.failure = logObject.failure.message ?? '(redacted)';
507540
return log;
508541
case CONNECTION_POOL_CREATED:
509542
log = attachConnectionFields(log, logObject);
@@ -701,12 +734,16 @@ export class MongoLogger {
701734
this.logDestination = options.logDestination;
702735
}
703736

737+
willLog(severity: SeverityLevel, component: MongoLoggableComponent): boolean {
738+
return compareSeverity(severity, this.componentSeverities[component]) <= 0;
739+
}
740+
704741
private log(
705742
severity: SeverityLevel,
706743
component: MongoLoggableComponent,
707744
message: Loggable | string
708745
): void {
709-
if (compareSeverity(severity, this.componentSeverities[component]) > 0) return;
746+
if (!this.willLog(severity, component)) return;
710747

711748
let logMessage: Log = { t: new Date(), c: component, s: severity };
712749
if (typeof message === 'string') {

src/mongo_types.ts

+30-5
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import type {
1212
ObjectId,
1313
Timestamp
1414
} from './bson';
15-
import type {
16-
LoggableServerHeartbeatFailedEvent,
17-
LoggableServerHeartbeatStartedEvent,
18-
LoggableServerHeartbeatSucceededEvent,
15+
import { type CommandStartedEvent } from './cmap/command_monitoring_events';
16+
import {
17+
type LoggableCommandFailedEvent,
18+
type LoggableCommandSucceededEvent,
19+
type LoggableServerHeartbeatFailedEvent,
20+
type LoggableServerHeartbeatStartedEvent,
21+
type LoggableServerHeartbeatSucceededEvent,
1922
MongoLoggableComponent,
20-
MongoLogger
23+
type MongoLogger
2124
} from './mongo_logger';
2225
import type { Sort } from './sort';
2326

@@ -442,6 +445,28 @@ export class TypedEventEmitter<Events extends EventsDescription> extends EventEm
442445
this.mongoLogger?.debug(this.component, loggableHeartbeatEvent);
443446
}
444447
}
448+
/** @internal */
449+
emitAndLogCommand<EventKey extends keyof Events>(
450+
monitorCommands: boolean,
451+
event: EventKey | symbol,
452+
databaseName: string,
453+
connectionEstablished: boolean,
454+
...args: Parameters<Events[EventKey]>
455+
): void {
456+
if (monitorCommands) {
457+
this.emit(event, ...args);
458+
}
459+
if (connectionEstablished) {
460+
const loggableCommandEvent:
461+
| CommandStartedEvent
462+
| LoggableCommandFailedEvent
463+
| LoggableCommandSucceededEvent = {
464+
databaseName: databaseName,
465+
...args[0]
466+
};
467+
this.mongoLogger?.debug(MongoLoggableComponent.COMMAND, loggableCommandEvent);
468+
}
469+
}
445470
}
446471

447472
/** @public */

0 commit comments

Comments
 (0)