Skip to content

Commit d844557

Browse files
committed
PYTHON-5846 Consolidate server selection logging and retry logging into _telemetry.py
1 parent 3eefcf7 commit d844557

5 files changed

Lines changed: 152 additions & 146 deletions

File tree

pymongo/_telemetry.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
_COMMAND_LOGGER,
2828
_CONNECTION_LOGGER,
2929
_SDAM_LOGGER,
30+
_SERVER_SELECTION_LOGGER,
3031
_CommandStatusMessage,
3132
_ConnectionStatusMessage,
3233
_debug_log,
3334
_SDAMStatusMessage,
35+
_ServerSelectionStatusMessage,
3436
_verbose_connection_error_reason,
3537
)
3638
from pymongo.pool_shared import _ConnectionTelemetryInfo
@@ -543,3 +545,91 @@ def server_closed(self, address: _Address) -> None:
543545
serverHost=address[0],
544546
serverPort=address[1],
545547
)
548+
549+
550+
class _ServerSelectionTelemetry:
551+
"""Structured logging for server selection events.
552+
553+
The server selection spec defines only log entries, not APM events, so this
554+
class has no publish methods.
555+
556+
Construct once per :meth:`select_server` call.
557+
"""
558+
559+
__slots__ = (
560+
"_operation",
561+
"_operation_id",
562+
"_selector",
563+
"_topology_description",
564+
"_topology_id",
565+
)
566+
567+
def __init__(
568+
self,
569+
topology_id: Any,
570+
selector: Any,
571+
operation: str,
572+
operation_id: Optional[int],
573+
topology_description: Any,
574+
) -> None:
575+
self._topology_id = topology_id
576+
self._selector = selector
577+
self._operation = operation
578+
self._operation_id = operation_id
579+
self._topology_description = topology_description
580+
581+
@property
582+
def _should_log(self) -> bool:
583+
return _SERVER_SELECTION_LOGGER.isEnabledFor(logging.DEBUG)
584+
585+
def _emit_log(self, message: _ServerSelectionStatusMessage, **extra: Any) -> None:
586+
if self._should_log:
587+
_debug_log(
588+
_SERVER_SELECTION_LOGGER,
589+
message=message,
590+
clientId=self._topology_id,
591+
selector=self._selector,
592+
operation=self._operation,
593+
operationId=self._operation_id,
594+
topologyDescription=self._topology_description,
595+
**extra,
596+
)
597+
598+
def started(self) -> None:
599+
"""Emit the server selection STARTED log entry."""
600+
self._emit_log(_ServerSelectionStatusMessage.STARTED)
601+
602+
def waiting(self, remaining_time_ms: int) -> None:
603+
"""Emit the server selection WAITING log entry."""
604+
self._emit_log(_ServerSelectionStatusMessage.WAITING, remainingTimeMS=remaining_time_ms)
605+
606+
def failed(self, failure: str) -> None:
607+
"""Emit the server selection FAILED log entry."""
608+
self._emit_log(_ServerSelectionStatusMessage.FAILED, failure=failure)
609+
610+
def succeeded(self, server_host: str, server_port: Optional[int]) -> None:
611+
"""Emit the server selection SUCCEEDED log entry."""
612+
self._emit_log(
613+
_ServerSelectionStatusMessage.SUCCEEDED,
614+
serverHost=server_host,
615+
serverPort=server_port,
616+
)
617+
618+
619+
def log_command_retry(
620+
topology_id: Any,
621+
command_name: str,
622+
operation_id: Optional[int],
623+
attempt_number: int,
624+
is_write: bool,
625+
) -> None:
626+
"""Emit a command-retry log entry."""
627+
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
628+
op = "write" if is_write else "read"
629+
_debug_log(
630+
_COMMAND_LOGGER,
631+
message=f"Retrying {op} attempt number {attempt_number}",
632+
clientId=topology_id,
633+
commandName=command_name,
634+
operationId=operation_id,
635+
)

pymongo/asynchronous/mongo_client.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, TypeRegistry
5858
from bson.timestamp import Timestamp
5959
from pymongo import _csot, common, helpers_shared, periodic_executor
60+
from pymongo._telemetry import log_command_retry
6061
from pymongo.asynchronous import client_session, database, uri_parser
6162
from pymongo.asynchronous.change_stream import AsyncChangeStream, AsyncClusterChangeStream
6263
from pymongo.asynchronous.client_bulk import _AsyncClientBulk
@@ -90,8 +91,6 @@
9091
)
9192
from pymongo.logger import (
9293
_CLIENT_LOGGER,
93-
_COMMAND_LOGGER,
94-
_debug_log,
9594
_log_client_error,
9695
_log_or_warn,
9796
)
@@ -3014,12 +3013,12 @@ async def _write(self) -> T:
30143013
self._check_last_error()
30153014
self._retryable = False
30163015
if self._retrying:
3017-
_debug_log(
3018-
_COMMAND_LOGGER,
3019-
message=f"Retrying write attempt number {self._attempt_number}",
3020-
clientId=self._client._topology_id,
3021-
commandName=self._operation,
3022-
operationId=self._operation_id,
3016+
log_command_retry(
3017+
self._client._topology_id,
3018+
self._operation,
3019+
self._operation_id,
3020+
self._attempt_number,
3021+
is_write=True,
30233022
)
30243023
return await self._func(self._session, conn, self._retryable) # type: ignore
30253024
except PyMongoError as exc:
@@ -3043,12 +3042,12 @@ async def _read(self) -> T:
30433042
if self._retrying and not self._retryable and not self._always_retryable:
30443043
self._check_last_error()
30453044
if self._retrying:
3046-
_debug_log(
3047-
_COMMAND_LOGGER,
3048-
message=f"Retrying read attempt number {self._attempt_number}",
3049-
clientId=self._client._topology_settings._topology_id,
3050-
commandName=self._operation,
3051-
operationId=self._operation_id,
3045+
log_command_retry(
3046+
self._client._topology_settings._topology_id,
3047+
self._operation,
3048+
self._operation_id,
3049+
self._attempt_number,
3050+
is_write=False,
30523051
)
30533052
return await self._func(self._session, self._server, conn, read_pref) # type: ignore
30543053

pymongo/asynchronous/topology.py

Lines changed: 18 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from __future__ import annotations
1818

1919
import asyncio
20-
import logging
2120
import os
2221
import queue
2322
import random
@@ -30,7 +29,7 @@
3029
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
3130

3231
from pymongo import _csot, common, helpers_shared, periodic_executor
33-
from pymongo._telemetry import _SdamTelemetry
32+
from pymongo._telemetry import _SdamTelemetry, _ServerSelectionTelemetry
3433
from pymongo.asynchronous.client_session import _ServerSession, _ServerSessionPool
3534
from pymongo.asynchronous.monitor import MonitorBase, SrvMonitor
3635
from pymongo.asynchronous.pool import Pool
@@ -52,11 +51,6 @@
5251
_async_create_condition,
5352
_async_create_lock,
5453
)
55-
from pymongo.logger import (
56-
_SERVER_SELECTION_LOGGER,
57-
_debug_log,
58-
_ServerSelectionStatusMessage,
59-
)
6054
from pymongo.pool_options import PoolOptions
6155
from pymongo.server_description import ServerDescription
6256
from pymongo.server_selectors import (
@@ -107,14 +101,14 @@ class Topology:
107101
def __init__(self, topology_settings: TopologySettings):
108102
self._topology_id = topology_settings._topology_id
109103
self._listeners = topology_settings._pool_options._event_listeners
110-
self._publish_server = self._listeners is not None and self._listeners.enabled_for_server
111-
self._publish_tp = self._listeners is not None and self._listeners.enabled_for_topology
112104

113105
# Create events queue if there are publishers.
114106
self._events: queue.Queue[Any] | None = None
115107
self.__events_executor: Any = None
116108

117-
if self._publish_server or self._publish_tp:
109+
publish_server = self._listeners is not None and self._listeners.enabled_for_server
110+
publish_tp = self._listeners is not None and self._listeners.enabled_for_topology
111+
if publish_server or publish_tp:
118112
self._events = queue.Queue(maxsize=100)
119113

120114
self._sdam = _SdamTelemetry(self._topology_id, self._listeners, self._events)
@@ -152,7 +146,7 @@ def __init__(self, topology_settings: TopologySettings):
152146
self._max_cluster_time: Optional[ClusterTime] = None
153147
self._session_pool = _ServerSessionPool()
154148

155-
if self._publish_server or self._publish_tp:
149+
if self._sdam._publish_server or self._sdam._publish_tp:
156150
assert self._events is not None
157151
weak: weakref.ReferenceType[queue.Queue[Any]]
158152

@@ -285,17 +279,10 @@ async def _select_servers_loop(
285279
now = time.monotonic()
286280
end_time = now + timeout
287281
logged_waiting = False
288-
289-
if _SERVER_SELECTION_LOGGER.isEnabledFor(logging.DEBUG):
290-
_debug_log(
291-
_SERVER_SELECTION_LOGGER,
292-
message=_ServerSelectionStatusMessage.STARTED,
293-
selector=selector,
294-
operation=operation,
295-
operationId=operation_id,
296-
topologyDescription=self.description,
297-
clientId=self.description._topology_settings._topology_id,
298-
)
282+
ss = _ServerSelectionTelemetry(
283+
self._topology_id, selector, operation, operation_id, self.description
284+
)
285+
ss.started()
299286

300287
server_descriptions = self._description.apply_selector(
301288
selector,
@@ -309,32 +296,13 @@ async def _select_servers_loop(
309296
while not server_descriptions:
310297
# No suitable servers.
311298
if timeout == 0 or now > end_time:
312-
if _SERVER_SELECTION_LOGGER.isEnabledFor(logging.DEBUG):
313-
_debug_log(
314-
_SERVER_SELECTION_LOGGER,
315-
message=_ServerSelectionStatusMessage.FAILED,
316-
selector=selector,
317-
operation=operation,
318-
operationId=operation_id,
319-
topologyDescription=self.description,
320-
clientId=self.description._topology_settings._topology_id,
321-
failure=self._error_message(selector),
322-
)
299+
ss.failed(self._error_message(selector))
323300
raise ServerSelectionTimeoutError(
324301
f"{self._error_message(selector)}, Timeout: {timeout}s, Topology Description: {self.description!r}"
325302
)
326303

327304
if not logged_waiting:
328-
_debug_log(
329-
_SERVER_SELECTION_LOGGER,
330-
message=_ServerSelectionStatusMessage.WAITING,
331-
selector=selector,
332-
operation=operation,
333-
operationId=operation_id,
334-
topologyDescription=self.description,
335-
clientId=self.description._topology_settings._topology_id,
336-
remainingTimeMS=int(1000 * (end_time - time.monotonic())),
337-
)
305+
ss.waiting(int(1000 * (end_time - time.monotonic())))
338306
logged_waiting = True
339307

340308
await self._ensure_opened()
@@ -399,18 +367,9 @@ async def select_server(
399367
)
400368
if _csot.get_timeout():
401369
_csot.set_rtt(server.description.min_round_trip_time)
402-
if _SERVER_SELECTION_LOGGER.isEnabledFor(logging.DEBUG):
403-
_debug_log(
404-
_SERVER_SELECTION_LOGGER,
405-
message=_ServerSelectionStatusMessage.SUCCEEDED,
406-
selector=selector,
407-
operation=operation,
408-
operationId=operation_id,
409-
topologyDescription=self.description,
410-
clientId=self.description._topology_settings._topology_id,
411-
serverHost=server.description.address[0],
412-
serverPort=server.description.address[1],
413-
)
370+
_ServerSelectionTelemetry(
371+
self._topology_id, selector, operation, operation_id, self.description
372+
).succeeded(server.description.address[0], server.description.address[1])
414373
return server
415374

416375
async def select_server_by_address(
@@ -670,7 +629,7 @@ async def close(self) -> None:
670629
self._closed = True
671630

672631
# Publish only after releasing the lock.
673-
if self._publish_tp:
632+
if self._sdam._publish_tp:
674633
self._description = TopologyDescription(
675634
TOPOLOGY_TYPE.Unknown,
676635
{},
@@ -681,7 +640,7 @@ async def close(self) -> None:
681640
)
682641
self._sdam.topology_closed(old_td, self._description)
683642

684-
if self._publish_server or self._publish_tp:
643+
if self._sdam._publish_server or self._sdam._publish_tp:
685644
# Make sure the events executor thread is fully closed before publishing the remaining events
686645
self.__events_executor.close()
687646
await self.__events_executor.join(1)
@@ -722,7 +681,7 @@ async def _ensure_opened(self) -> None:
722681
await self._update_servers()
723682

724683
# Start or restart the events publishing thread.
725-
if self._publish_tp or self._publish_server:
684+
if self._sdam._publish_tp or self._sdam._publish_server:
726685
self.__events_executor.open()
727686

728687
# Start the SRV polling thread.
@@ -861,7 +820,7 @@ async def _update_servers(self) -> None:
861820
)
862821

863822
weak = None
864-
if self._publish_server and self._events is not None:
823+
if self._sdam._publish_server and self._events is not None:
865824
weak = weakref.ref(self._events)
866825
server = Server(
867826
server_description=sd,

pymongo/synchronous/mongo_client.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, TypeRegistry
5858
from bson.timestamp import Timestamp
5959
from pymongo import _csot, common, helpers_shared, periodic_executor
60+
from pymongo._telemetry import log_command_retry
6061
from pymongo.client_options import ClientOptions
6162
from pymongo.driver_info import DriverInfo
6263
from pymongo.errors import (
@@ -80,8 +81,6 @@
8081
)
8182
from pymongo.logger import (
8283
_CLIENT_LOGGER,
83-
_COMMAND_LOGGER,
84-
_debug_log,
8584
_log_client_error,
8685
_log_or_warn,
8786
)
@@ -3005,12 +3004,12 @@ def _write(self) -> T:
30053004
self._check_last_error()
30063005
self._retryable = False
30073006
if self._retrying:
3008-
_debug_log(
3009-
_COMMAND_LOGGER,
3010-
message=f"Retrying write attempt number {self._attempt_number}",
3011-
clientId=self._client._topology_id,
3012-
commandName=self._operation,
3013-
operationId=self._operation_id,
3007+
log_command_retry(
3008+
self._client._topology_id,
3009+
self._operation,
3010+
self._operation_id,
3011+
self._attempt_number,
3012+
is_write=True,
30143013
)
30153014
return self._func(self._session, conn, self._retryable) # type: ignore
30163015
except PyMongoError as exc:
@@ -3034,12 +3033,12 @@ def _read(self) -> T:
30343033
if self._retrying and not self._retryable and not self._always_retryable:
30353034
self._check_last_error()
30363035
if self._retrying:
3037-
_debug_log(
3038-
_COMMAND_LOGGER,
3039-
message=f"Retrying read attempt number {self._attempt_number}",
3040-
clientId=self._client._topology_settings._topology_id,
3041-
commandName=self._operation,
3042-
operationId=self._operation_id,
3036+
log_command_retry(
3037+
self._client._topology_settings._topology_id,
3038+
self._operation,
3039+
self._operation_id,
3040+
self._attempt_number,
3041+
is_write=False,
30433042
)
30443043
return self._func(self._session, self._server, conn, read_pref) # type: ignore
30453044

0 commit comments

Comments
 (0)