PYTHON-3835 Log informational message client-side based on detected environment (#1537)

This commit is contained in:
Noah Stapp 2024-03-07 14:53:09 -08:00 committed by GitHub
parent b041ca5f7c
commit 266b3dd8e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 130 additions and 2 deletions

View File

@ -32,6 +32,10 @@ PyMongo 4.7 brings a number of improvements including:
- Fixed a bug appearing in Python 3.12 where "RuntimeError: can't create new thread at interpreter shutdown"
could be written to stderr when a MongoClient's thread starts as the python interpreter is shutting down.
- Added a warning when connecting to DocumentDB and CosmosDB clusters.
For more information regarding feature compatibility and support please visit
`mongodb.com/supportability/documentdb <https://mongodb.com/supportability/documentdb>`_ and
`mongodb.com/supportability/cosmosdb <https://mongodb.com/supportability/cosmosdb>`_.
- Added the :attr:`pymongo.monitoring.ConnectionCheckedOutEvent.duration`,
:attr:`pymongo.monitoring.ConnectionCheckOutFailedEvent.duration`, and
:attr:`pymongo.monitoring.ConnectionReadyEvent.duration` properties.

View File

@ -16,6 +16,7 @@ from __future__ import annotations
import enum
import logging
import os
import warnings
from typing import Any
from bson import UuidRepresentation, json_util
@ -71,6 +72,7 @@ _JSON_OPTIONS = JSONOptions(uuid_representation=UuidRepresentation.STANDARD)
_COMMAND_LOGGER = logging.getLogger("pymongo.command")
_CONNECTION_LOGGER = logging.getLogger("pymongo.connection")
_SERVER_SELECTION_LOGGER = logging.getLogger("pymongo.serverSelection")
_CLIENT_LOGGER = logging.getLogger("pymongo.client")
_VERBOSE_CONNECTION_ERROR_REASONS = {
ConnectionClosedReason.POOL_CLOSED: "Connection pool was closed",
ConnectionCheckOutFailedReason.POOL_CLOSED: "Connection pool was closed",
@ -94,6 +96,14 @@ def _info_log(logger: logging.Logger, **fields: Any) -> None:
logger.info(LogMessage(**fields))
def _log_or_warn(logger: logging.Logger, message: str) -> None:
if logger.isEnabledFor(logging.INFO):
logger.info(message)
else:
# stacklevel=4 ensures that the warning is for the user's code.
warnings.warn(message, UserWarning, stacklevel=4)
class LogMessage:
__slots__ = ("_kwargs", "_redacted")

View File

@ -86,6 +86,7 @@ from pymongo.errors import (
WriteConcernError,
)
from pymongo.lock import _HAS_REGISTER_AT_FORK, _create_lock, _release_locks
from pymongo.logger import _CLIENT_LOGGER, _log_or_warn
from pymongo.monitoring import ConnectionClosedReason
from pymongo.operations import _Op
from pymongo.read_preferences import ReadPreference, _ServerMode
@ -789,6 +790,10 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
if not seeds:
raise ConfigurationError("need to specify at least one host")
for hostname in [node[0] for node in seeds]:
if _detect_external_db(hostname):
break
# Add options with named keyword arguments to the parsed kwarg options.
if type_registry is not None:
keyword_opts["type_registry"] = type_registry
@ -2487,6 +2492,31 @@ def _after_fork_child() -> None:
client._after_fork()
def _detect_external_db(entity: str) -> bool:
"""Detects external database hosts and logs an informational message at the INFO level."""
entity = entity.lower()
cosmos_db_hosts = [".cosmos.azure.com"]
document_db_hosts = [".docdb.amazonaws.com", ".docdb-elastic.amazonaws.com"]
for host in cosmos_db_hosts:
if entity.endswith(host):
_log_or_warn(
_CLIENT_LOGGER,
"You appear to be connected to a CosmosDB cluster. For more information regarding feature "
"compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb",
)
return True
for host in document_db_hosts:
if entity.endswith(host):
_log_or_warn(
_CLIENT_LOGGER,
"You appear to be connected to a DocumentDB cluster. For more information regarding feature "
"compatibility and support please visit https://www.mongodb.com/supportability/documentdb",
)
return True
return False
if _HAS_REGISTER_AT_FORK:
# This will run in the same thread as the fork was called.
# If we fork in a critical region on the same thread, it should break.

View File

@ -20,6 +20,7 @@ import contextlib
import copy
import datetime
import gc
import logging
import os
import re
import signal
@ -33,6 +34,8 @@ from typing import Iterable, Type, no_type_check
from unittest import mock
from unittest.mock import patch
import pytest
from pymongo.operations import _Op
sys.path[0:0] = [""]
@ -99,7 +102,7 @@ from pymongo.errors import (
ServerSelectionTimeoutError,
WriteConcernError,
)
from pymongo.mongo_client import MongoClient
from pymongo.mongo_client import MongoClient, _detect_external_db
from pymongo.monitoring import ServerHeartbeatListener, ServerHeartbeatStartedEvent
from pymongo.pool import _METADATA, DOCKER_ENV_PATH, ENV_VAR_K8S, Connection, PoolOptions
from pymongo.read_preferences import ReadPreference
@ -126,6 +129,10 @@ class ClientUnitTest(unittest.TestCase):
def tearDownClass(cls):
cls.client.close()
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog):
self._caplog = caplog
def test_keyword_arg_defaults(self):
client = MongoClient(
socketTimeoutMS=None,
@ -546,6 +553,78 @@ class ClientUnitTest(unittest.TestCase):
with self.assertRaisesRegex(ConfigurationError, expected):
MongoClient(**{typo: "standard"}) # type: ignore[arg-type]
@patch("pymongo.srv_resolver._SrvResolver.get_hosts")
def test_detected_environment_logging(self, mock_get_hosts):
normal_hosts = [
"normal.host.com",
"host.cosmos.azure.com",
"host.docdb.amazonaws.com",
"host.docdb-elastic.amazonaws.com",
]
srv_hosts = ["mongodb+srv://<test>:<test>@" + s for s in normal_hosts]
multi_host = (
"host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com"
)
with self.assertLogs("pymongo", level="INFO") as cm:
for host in normal_hosts:
MongoClient(host)
for host in srv_hosts:
mock_get_hosts.return_value = [(host, 1)]
MongoClient(host)
MongoClient(multi_host)
logs = [record.message for record in cm.records if record.name == "pymongo.client"]
self.assertEqual(len(logs), 7)
@patch("pymongo.srv_resolver._SrvResolver.get_hosts")
def test_detected_environment_warning(self, mock_get_hosts):
with self._caplog.at_level(logging.WARN):
normal_hosts = [
"host.cosmos.azure.com",
"host.docdb.amazonaws.com",
"host.docdb-elastic.amazonaws.com",
]
srv_hosts = ["mongodb+srv://<test>:<test>@" + s for s in normal_hosts]
multi_host = (
"host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com"
)
for host in normal_hosts:
with self.assertWarns(UserWarning):
MongoClient(host)
for host in srv_hosts:
mock_get_hosts.return_value = [(host, 1)]
with self.assertWarns(UserWarning):
MongoClient(host)
with self.assertWarns(UserWarning):
MongoClient(multi_host)
def test_detect_external_db(self):
hosts = [
"normalhost.com",
"host.cosmos.AZURE.com",
"host.docdb.amazonaws.com",
"host.docdb-elastic.amazonaws.com",
]
with self.assertLogs("pymongo", level="INFO") as cm:
for host in hosts:
_detect_external_db(host)
logs = [record.message for record in cm.records if record.name == "pymongo.client"]
self.assertEqual(len(logs), 3)
self.assertEqual(
logs[0],
"You appear to be connected to a CosmosDB cluster. For more information regarding feature "
"compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb",
)
self.assertEqual(
logs[1],
"You appear to be connected to a DocumentDB cluster. For more information regarding feature "
"compatibility and support please visit https://www.mongodb.com/supportability/documentdb",
)
self.assertEqual(
logs[2],
"You appear to be connected to a DocumentDB cluster. For more information regarding feature "
"compatibility and support please visit https://www.mongodb.com/supportability/documentdb",
)
class TestClient(IntegrationTest):
def test_multiple_uris(self):

View File

@ -28,7 +28,12 @@ from test import unittest
from bson.binary import JAVA_LEGACY
from pymongo import ReadPreference
from pymongo.errors import ConfigurationError, InvalidURI
from pymongo.uri_parser import parse_uri, parse_userinfo, split_hosts, split_options
from pymongo.uri_parser import (
parse_uri,
parse_userinfo,
split_hosts,
split_options,
)
class TestURI(unittest.TestCase):