Skip to content

Commit 7f3ce0b

Browse files
feat(NODE-4687): Add logging to server selection (#3946)
Co-authored-by: Durran Jordan <[email protected]>
1 parent b93d405 commit 7f3ce0b

24 files changed

+3033
-54
lines changed

src/change_stream.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -934,10 +934,14 @@ export class ChangeStream<
934934
this.cursor.close().catch(() => null);
935935

936936
const topology = getTopology(this.parent);
937-
topology.selectServer(this.cursor.readPreference, {}, serverSelectionError => {
938-
if (serverSelectionError) return this._closeEmitterModeWithError(changeStreamError);
939-
this.cursor = this._createChangeStreamCursor(this.cursor.resumeOptions);
940-
});
937+
topology.selectServer(
938+
this.cursor.readPreference,
939+
{ operationName: 'reconnect topology in change stream' },
940+
serverSelectionError => {
941+
if (serverSelectionError) return this._closeEmitterModeWithError(changeStreamError);
942+
this.cursor = this._createChangeStreamCursor(this.cursor.resumeOptions);
943+
}
944+
);
941945
} else {
942946
this._closeEmitterModeWithError(changeStreamError);
943947
}
@@ -962,7 +966,9 @@ export class ChangeStream<
962966
await this.cursor.close().catch(() => null);
963967
const topology = getTopology(this.parent);
964968
try {
965-
await topology.selectServerAsync(this.cursor.readPreference, {});
969+
await topology.selectServerAsync(this.cursor.readPreference, {
970+
operationName: 'reconnect topology in change stream'
971+
});
966972
this.cursor = this._createChangeStreamCursor(this.cursor.resumeOptions);
967973
} catch {
968974
// if the topology can't reconnect, close the stream

src/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ export const TOPOLOGY_CLOSED = 'topologyClosed' as const;
3030
/** @internal */
3131
export const TOPOLOGY_DESCRIPTION_CHANGED = 'topologyDescriptionChanged' as const;
3232
/** @internal */
33+
export const SERVER_SELECTION_STARTED = 'serverSelectionStarted' as const;
34+
/** @internal */
35+
export const SERVER_SELECTION_FAILED = 'serverSelectionFailed' as const;
36+
/** @internal */
37+
export const SERVER_SELECTION_SUCCEEDED = 'serverSelectionSucceeded' as const;
38+
/** @internal */
39+
export const WAITING_FOR_SUITABLE_SERVER = 'waitingForSuitableServer' as const;
40+
/** @internal */
3341
export const CONNECTION_POOL_CREATED = 'connectionPoolCreated' as const;
3442
/** @internal */
3543
export const CONNECTION_POOL_CLOSED = 'connectionPoolClosed' as const;

src/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ export {
147147
TopologyDescriptionChangedEvent,
148148
TopologyOpeningEvent
149149
} from './sdam/events';
150+
export {
151+
ServerSelectionEvent,
152+
ServerSelectionFailedEvent,
153+
ServerSelectionStartedEvent,
154+
ServerSelectionSucceededEvent,
155+
WaitingForSuitableServerEvent
156+
} from './sdam/server_selection_events';
150157
export { SrvPollingEvent } from './sdam/srv_polling';
151158

152159
// type only exports below, these are removed from emitted JS
@@ -303,9 +310,13 @@ export type {
303310
SERVER_HEARTBEAT_STARTED,
304311
SERVER_HEARTBEAT_SUCCEEDED,
305312
SERVER_OPENING,
313+
SERVER_SELECTION_FAILED,
314+
SERVER_SELECTION_STARTED,
315+
SERVER_SELECTION_SUCCEEDED,
306316
TOPOLOGY_CLOSED,
307317
TOPOLOGY_DESCRIPTION_CHANGED,
308-
TOPOLOGY_OPENING
318+
TOPOLOGY_OPENING,
319+
WAITING_FOR_SUITABLE_SERVER
309320
} from './constants';
310321
export type {
311322
AbstractCursorEvents,

src/mongo_logger.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import type {
1717
ConnectionPoolClearedEvent,
1818
ConnectionPoolClosedEvent,
1919
ConnectionPoolCreatedEvent,
20-
ConnectionPoolMonitoringEvent,
2120
ConnectionPoolReadyEvent,
2221
ConnectionReadyEvent
2322
} from './cmap/connection_pool_events';
@@ -41,9 +40,13 @@ import {
4140
SERVER_HEARTBEAT_STARTED,
4241
SERVER_HEARTBEAT_SUCCEEDED,
4342
SERVER_OPENING,
43+
SERVER_SELECTION_FAILED,
44+
SERVER_SELECTION_STARTED,
45+
SERVER_SELECTION_SUCCEEDED,
4446
TOPOLOGY_CLOSED,
4547
TOPOLOGY_DESCRIPTION_CHANGED,
46-
TOPOLOGY_OPENING
48+
TOPOLOGY_OPENING,
49+
WAITING_FOR_SUITABLE_SERVER
4750
} from './constants';
4851
import type {
4952
ServerClosedEvent,
@@ -52,6 +55,13 @@ import type {
5255
TopologyDescriptionChangedEvent,
5356
TopologyOpeningEvent
5457
} from './sdam/events';
58+
import type {
59+
ServerSelectionEvent,
60+
ServerSelectionFailedEvent,
61+
ServerSelectionStartedEvent,
62+
ServerSelectionSucceededEvent,
63+
WaitingForSuitableServerEvent
64+
} from './sdam/server_selection_events';
5565
import { HostAddress, parseUnsignedInteger } from './utils';
5666

5767
/** @internal */
@@ -335,6 +345,10 @@ type SDAMLoggableEvent =
335345

336346
/** @internal */
337347
export type LoggableEvent =
348+
| ServerSelectionStartedEvent
349+
| ServerSelectionFailedEvent
350+
| ServerSelectionSucceededEvent
351+
| WaitingForSuitableServerEvent
338352
| CommandStartedEvent
339353
| CommandSucceededEvent
340354
| CommandFailedEvent
@@ -369,11 +383,16 @@ export function stringifyWithMaxLen(
369383
maxDocumentLength: number,
370384
options: EJSONOptions = {}
371385
): string {
372-
const ejson = EJSON.stringify(value, options);
386+
let strToTruncate: string;
387+
if (typeof value === 'function') {
388+
strToTruncate = value.toString();
389+
} else {
390+
strToTruncate = EJSON.stringify(value, options);
391+
}
373392

374-
return maxDocumentLength !== 0 && ejson.length > maxDocumentLength
375-
? `${ejson.slice(0, maxDocumentLength)}...`
376-
: ejson;
393+
return maxDocumentLength !== 0 && strToTruncate.length > maxDocumentLength
394+
? `${strToTruncate.slice(0, maxDocumentLength)}...`
395+
: strToTruncate;
377396
}
378397

379398
/** @internal */
@@ -385,6 +404,20 @@ function isLogConvertible(obj: Loggable): obj is LogConvertible {
385404
return objAsLogConvertible.toLog !== undefined && typeof objAsLogConvertible.toLog === 'function';
386405
}
387406

407+
function attachServerSelectionFields(
408+
log: Record<string, any>,
409+
serverSelectionEvent: ServerSelectionEvent,
410+
maxDocumentLength: number = DEFAULT_MAX_DOCUMENT_LENGTH
411+
) {
412+
const { selector, operation, topologyDescription, message } = serverSelectionEvent;
413+
log.selector = stringifyWithMaxLen(selector, maxDocumentLength);
414+
log.operation = operation;
415+
log.topologyDescription = stringifyWithMaxLen(topologyDescription, maxDocumentLength);
416+
log.message = message;
417+
418+
return log;
419+
}
420+
388421
function attachCommandFields(
389422
log: Record<string, any>,
390423
commandEvent: CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent
@@ -402,10 +435,7 @@ function attachCommandFields(
402435
return log;
403436
}
404437

405-
function attachConnectionFields(
406-
log: Record<string, any>,
407-
event: ConnectionPoolMonitoringEvent | ServerOpeningEvent | ServerClosedEvent
408-
) {
438+
function attachConnectionFields(log: Record<string, any>, event: any) {
409439
const { host, port } = HostAddress.fromString(event.address).toHostPort();
410440
log.serverHost = host;
411441
log.serverPort = port;
@@ -441,6 +471,22 @@ function defaultLogTransform(
441471
let log: Omit<Log, 's' | 't' | 'c'> = Object.create(null);
442472

443473
switch (logObject.name) {
474+
case SERVER_SELECTION_STARTED:
475+
log = attachServerSelectionFields(log, logObject, maxDocumentLength);
476+
return log;
477+
case SERVER_SELECTION_FAILED:
478+
log = attachServerSelectionFields(log, logObject, maxDocumentLength);
479+
log.failure = logObject.failure.message;
480+
return log;
481+
case SERVER_SELECTION_SUCCEEDED:
482+
log = attachServerSelectionFields(log, logObject, maxDocumentLength);
483+
log.serverHost = logObject.serverHost;
484+
log.serverPort = logObject.serverPort;
485+
return log;
486+
case WAITING_FOR_SUITABLE_SERVER:
487+
log = attachServerSelectionFields(log, logObject, maxDocumentLength);
488+
log.remainingTimeMS = logObject.remainingTimeMS;
489+
return log;
444490
case COMMAND_STARTED:
445491
log = attachCommandFields(log, logObject);
446492
log.message = 'Command started';

src/operations/execute_operation.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ async function executeOperationAsync<
151151
selector = readPreference;
152152
}
153153

154-
const server = await topology.selectServerAsync(selector, { session });
154+
const server = await topology.selectServerAsync(selector, {
155+
session,
156+
operationName: operation.commandName
157+
});
155158

156159
if (session == null) {
157160
// No session also means it is not retryable, early exit
@@ -251,7 +254,10 @@ async function retryOperation<
251254
}
252255

253256
// select a new server, and attempt to retry the operation
254-
const server = await topology.selectServerAsync(selector, { session });
257+
const server = await topology.selectServerAsync(selector, {
258+
session,
259+
operationName: operation.commandName
260+
});
255261

256262
if (isWriteOperation && !supportsRetryableWrites(server)) {
257263
throw new MongoUnexpectedServerResponseError(

src/sdam/server_selection_events.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { HostAddress } from '.././utils';
2+
import {
3+
SERVER_SELECTION_FAILED,
4+
SERVER_SELECTION_STARTED,
5+
SERVER_SELECTION_SUCCEEDED,
6+
WAITING_FOR_SUITABLE_SERVER
7+
} from '../constants';
8+
import { type ReadPreference } from '../read_preference';
9+
import { type ServerSelector } from './server_selection';
10+
import type { TopologyDescription } from './topology_description';
11+
12+
/**
13+
* The base export class for all logs published from server selection
14+
* @internal
15+
* @category Log Type
16+
*/
17+
export abstract class ServerSelectionEvent {
18+
/** String representation of the selector being used to select the server.
19+
* Defaults to 'custom selector' for application-provided custom selector case.
20+
*/
21+
selector: string | ReadPreference | ServerSelector;
22+
/** The name of the operation for which a server is being selected. */
23+
operation: string;
24+
/** The current topology description. */
25+
topologyDescription: TopologyDescription;
26+
27+
/** @internal */
28+
abstract name:
29+
| typeof SERVER_SELECTION_STARTED
30+
| typeof SERVER_SELECTION_SUCCEEDED
31+
| typeof SERVER_SELECTION_FAILED
32+
| typeof WAITING_FOR_SUITABLE_SERVER;
33+
34+
abstract message: string;
35+
36+
/** @internal */
37+
constructor(
38+
selector: string | ReadPreference | ServerSelector,
39+
topologyDescription: TopologyDescription,
40+
operation: string
41+
) {
42+
this.selector = selector;
43+
this.operation = operation;
44+
this.topologyDescription = topologyDescription;
45+
}
46+
}
47+
48+
/**
49+
* An event published when server selection starts
50+
* @internal
51+
* @category Event
52+
*/
53+
export class ServerSelectionStartedEvent extends ServerSelectionEvent {
54+
/** @internal */
55+
name = SERVER_SELECTION_STARTED;
56+
message = 'Server selection started';
57+
58+
/** @internal */
59+
constructor(
60+
selector: string | ReadPreference | ServerSelector,
61+
topologyDescription: TopologyDescription,
62+
operation: string
63+
) {
64+
super(selector, topologyDescription, operation);
65+
}
66+
}
67+
68+
/**
69+
* An event published when a server selection fails
70+
* @internal
71+
* @category Event
72+
*/
73+
export class ServerSelectionFailedEvent extends ServerSelectionEvent {
74+
/** @internal */
75+
name = SERVER_SELECTION_FAILED;
76+
message = 'Server selection failed';
77+
/** Representation of the error the driver will throw regarding server selection failing. */
78+
failure: Error;
79+
80+
/** @internal */
81+
constructor(
82+
selector: string | ReadPreference | ServerSelector,
83+
topologyDescription: TopologyDescription,
84+
error: Error,
85+
operation: string
86+
) {
87+
super(selector, topologyDescription, operation);
88+
this.failure = error;
89+
}
90+
}
91+
92+
/**
93+
* An event published when server selection succeeds
94+
* @internal
95+
* @category Event
96+
*/
97+
export class ServerSelectionSucceededEvent extends ServerSelectionEvent {
98+
/** @internal */
99+
name = SERVER_SELECTION_SUCCEEDED;
100+
message = 'Server selection succeeded';
101+
/** The hostname, IP address, or Unix domain socket path for the selected server. */
102+
serverHost: string;
103+
/** The port for the selected server. Optional; not present for Unix domain sockets. When the user does not specify a port and the default (27017) is used, the driver SHOULD include it here. */
104+
serverPort: number | undefined;
105+
106+
/** @internal */
107+
constructor(
108+
selector: string | ReadPreference | ServerSelector,
109+
topologyDescription: TopologyDescription,
110+
address: string,
111+
operation: string
112+
) {
113+
super(selector, topologyDescription, operation);
114+
const { host, port } = HostAddress.fromString(address).toHostPort();
115+
this.serverHost = host;
116+
this.serverPort = port;
117+
}
118+
}
119+
120+
/**
121+
* An event published when server selection is waiting for a suitable server to become available
122+
* @internal
123+
* @category Event
124+
*/
125+
export class WaitingForSuitableServerEvent extends ServerSelectionEvent {
126+
/** @internal */
127+
name = WAITING_FOR_SUITABLE_SERVER;
128+
message = 'Waiting for suitable server to become available';
129+
/** The remaining time left until server selection will time out. */
130+
remainingTimeMS: number;
131+
132+
/** @internal */
133+
constructor(
134+
selector: string | ReadPreference | ServerSelector,
135+
topologyDescription: TopologyDescription,
136+
remainingTimeMS: number,
137+
operation: string
138+
) {
139+
super(selector, topologyDescription, operation);
140+
this.remainingTimeMS = remainingTimeMS;
141+
}
142+
}

0 commit comments

Comments
 (0)