PYTHON-3835 Log informational message client-side based on detected environment (#1537)
This commit is contained in:
parent
b041ca5f7c
commit
266b3dd8e9
@ -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.
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user