-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(NODE-4686): Add log messages to CLAM #3955
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
Changes from all commits
6d32c32
5f5d77e
209ba27
3bee17a
fcfc98b
422c6a9
c360ec2
054094f
0d8d051
383f890
7033dc8
1d70579
0ae8792
e92849d
9776ec9
3332f94
cacaed2
fa223cf
299e8a1
1181991
8f6c860
dc93151
cbb5df6
9ace7a0
e9d4f8d
7892601
3c18428
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,8 @@ export { | |
deserialize, | ||
Document, | ||
Double, | ||
EJSON, | ||
EJSONOptions, | ||
Int32, | ||
Long, | ||
MaxKey, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -114,6 +115,8 @@ export interface ConnectionOptions | |
socketTimeoutMS?: number; | ||
cancellationToken?: CancellationToken; | ||
metadata: ClientMetadata; | ||
/** @internal */ | ||
mongoLogger?: MongoLogger | undefined; | ||
} | ||
|
||
/** @internal */ | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the expected behavior with command logging when we re-authenticate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reauthentication will send commands we don't want to log I think. We may either need to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, pinged you on slack for more info on reauth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry I didn't notice this earlier ( I didn't actually check ) but instead of modifying one idea is a getter on the connection class (or we could just inline it): get shouldEmitAndLog(): boolean {
return this.monitorCommands ||
(this.established && !this.context.reauthenticating && this.mongoLogger?.willLog(SeverityLevel.DEBUG, MongoLoggableComponent.COMMAND))
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it more accurate to edit established as well, though? Since during authentication, the connection is not established. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understood established to mean that the connection had been established, not that it was "ready"/currently established. What I would like to avoid is duplicate data that might get out of sync. If established=false can also represent a connection reauthenticating, then it's possible for us to end up in a scenario context.reauthenticating=true with established=false (or vice versa) - which is a poorly defined state for the driver. One way to avoid this is to define established as whether or not the connection was initially established and rely on context.reauthenticating (my suggestion). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds good. I've removed the extra auth changes to |
||
|
||
private lastUseTime: number; | ||
private socketTimeoutMS: number; | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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(); | ||
} | ||
|
@@ -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) | ||
); | ||
} | ||
|
@@ -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, | ||
|
@@ -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, | ||
|
@@ -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, | ||
|
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, | ||
|
@@ -295,6 +291,40 @@ function compareSeverity(s0: SeverityLevel, s1: SeverityLevel): 1 | 0 | -1 { | |
return s0Num < s1Num ? -1 : s0Num > s1Num ? 1 : 0; | ||
} | ||
|
||
durran marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* @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 | ||
|
@@ -350,8 +380,8 @@ export type LoggableEvent = | |
| ServerSelectionSucceededEvent | ||
| WaitingForSuitableServerEvent | ||
| CommandStartedEvent | ||
| CommandSucceededEvent | ||
| CommandFailedEvent | ||
| LoggableCommandSucceededEvent | ||
| LoggableCommandFailedEvent | ||
| ConnectionPoolCreatedEvent | ||
| ConnectionPoolReadyEvent | ||
| ConnectionPoolClosedEvent | ||
|
@@ -383,7 +413,8 @@ export function stringifyWithMaxLen( | |
maxDocumentLength: number, | ||
options: EJSONOptions = {} | ||
): string { | ||
let strToTruncate: string; | ||
let strToTruncate = ''; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking and definitely only tangentially related: the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is something we will solve / take a further look at in NODE-5839. |
||
|
||
if (typeof value === 'function') { | ||
strToTruncate = value.toString(); | ||
} else { | ||
|
@@ -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; | ||
|
@@ -431,6 +462,8 @@ function attachCommandFields( | |
if (commandEvent?.serviceId) { | ||
log.serviceId = commandEvent.serviceId.toHexString(); | ||
} | ||
log.databaseName = commandEvent.databaseName; | ||
log.serverConnectionId = commandEvent?.serverConnectionId; | ||
|
||
return log; | ||
} | ||
|
@@ -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 }); | ||
nbbeeken marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
|
@@ -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') { | ||
|
Uh oh!
There was an error while loading. Please reload this page.