Visitar URL original
PYTHON-5947 Test error.type class-name fallback for network errors · mongodb/mongo-python-driver@5583231 · GitHub
Skip to content

Commit 5583231

Browse files
committed
PYTHON-5947 Test error.type class-name fallback for network errors
Covers the two non-server-error paths where there is no server error code to mirror: a closed connection (ConnectionFailure) and a socket timeout (NetworkTimeout). Both assert error.type is the exception's qualified class name and that db.response.status_code is absent.
1 parent 45533be commit 5583231

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

‎test/asynchronous/test_otel.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
from pymongo.errors import (
3333
ClientBulkWriteException,
3434
ConfigurationError,
35+
ConnectionFailure,
3536
InvalidOperation,
37+
NetworkTimeout,
3638
OperationFailure,
3739
ServerSelectionTimeoutError,
3840
)
@@ -66,6 +68,11 @@ def _tracing_opts() -> _otel.TracingOptions:
6668
return {"enabled": True, "query_text_max_length": 0}
6769

6870

71+
def _qualified_name(exc_type: type) -> str:
72+
"""Format an exception class the way the spans do: ``module.QualName``."""
73+
return f"{exc_type.__module__}.{exc_type.__qualname__}"
74+
75+
6976
@unittest.skipUnless(_HAS_OTEL_TEST_DEPS, "opentelemetry-sdk is not installed")
7077
class TestOTelOperationSpanPrimitives(unittest.TestCase):
7178
"""Unit tests for the pymongo._otel operation-span primitives."""
@@ -498,8 +505,62 @@ async def test_failure_records_exception_and_status_code(self):
498505
span = spans[0]
499506
self.assertEqual(span.status.status_code, trace.StatusCode.ERROR)
500507
self.assertIn("db.response.status_code", span.attributes)
508+
# For a server error the spec has error.type mirror the status code.
509+
self.assertEqual(span.attributes["error.type"], span.attributes["db.response.status_code"])
501510
self.assertTrue(any(event.name == "exception" for event in span.events))
502511

512+
@async_client_context.require_failCommand_fail_point
513+
async def test_error_type_is_exception_class_name_for_connection_failure(self):
514+
# A closed connection produces no server reply, so there is no error
515+
# code to report: the spec falls back to the exception's class name.
516+
client = await self.async_rs_or_single_client(tracing={"enabled": True}, retryReads=False)
517+
fail_command = {
518+
"configureFailPoint": "failCommand",
519+
"mode": {"times": 1},
520+
"data": {"failCommands": ["find"], "closeConnection": True},
521+
}
522+
async with self.fail_point(fail_command):
523+
self.exporter.clear()
524+
with self.assertRaises(ConnectionFailure) as ctx:
525+
await client[self.db.name].test.find_one({})
526+
527+
spans = [s for s in self.spans() if s.attributes.get("db.command.name") == "find"]
528+
self.assertEqual(len(spans), 1)
529+
attrs = spans[0].attributes
530+
self.assertNotIn("db.response.status_code", attrs)
531+
self.assertEqual(attrs["error.type"], _qualified_name(type(ctx.exception)))
532+
# error.type and exception.type carry the same value here, by design:
533+
# one is the span attribute, the other the exception event's.
534+
self.assertEqual(attrs["error.type"], attrs["exception.type"])
535+
536+
@async_client_context.require_failCommand_blockConnection
537+
async def test_error_type_is_exception_class_name_for_network_timeout(self):
538+
# socketTimeoutMS trips before the blocked command replies, so again
539+
# there is no server error code and error.type is the class name.
540+
client = await self.async_rs_or_single_client(
541+
tracing={"enabled": True}, socketTimeoutMS=200, retryReads=False
542+
)
543+
fail_command = {
544+
"configureFailPoint": "failCommand",
545+
"mode": {"times": 1},
546+
"data": {
547+
"failCommands": ["find"],
548+
"blockConnection": True,
549+
"blockTimeMS": 1000,
550+
},
551+
}
552+
async with self.fail_point(fail_command):
553+
self.exporter.clear()
554+
with self.assertRaises(NetworkTimeout) as ctx:
555+
await client[self.db.name].test.find_one({})
556+
557+
spans = [s for s in self.spans() if s.attributes.get("db.command.name") == "find"]
558+
self.assertEqual(len(spans), 1)
559+
attrs = spans[0].attributes
560+
self.assertNotIn("db.response.status_code", attrs)
561+
self.assertEqual(attrs["error.type"], _qualified_name(NetworkTimeout))
562+
self.assertIsInstance(ctx.exception, NetworkTimeout)
563+
503564
async def test_tracing_disabled_by_default(self):
504565
client = await self.async_rs_or_single_client()
505566
self.exporter.clear()

‎test/test_otel.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
from pymongo.errors import (
3333
ClientBulkWriteException,
3434
ConfigurationError,
35+
ConnectionFailure,
3536
InvalidOperation,
37+
NetworkTimeout,
3638
OperationFailure,
3739
ServerSelectionTimeoutError,
3840
)
@@ -66,6 +68,11 @@ def _tracing_opts() -> _otel.TracingOptions:
6668
return {"enabled": True, "query_text_max_length": 0}
6769

6870

71+
def _qualified_name(exc_type: type) -> str:
72+
"""Format an exception class the way the spans do: ``module.QualName``."""
73+
return f"{exc_type.__module__}.{exc_type.__qualname__}"
74+
75+
6976
@unittest.skipUnless(_HAS_OTEL_TEST_DEPS, "opentelemetry-sdk is not installed")
7077
class TestOTelOperationSpanPrimitives(unittest.TestCase):
7178
"""Unit tests for the pymongo._otel operation-span primitives."""
@@ -498,8 +505,62 @@ def test_failure_records_exception_and_status_code(self):
498505
span = spans[0]
499506
self.assertEqual(span.status.status_code, trace.StatusCode.ERROR)
500507
self.assertIn("db.response.status_code", span.attributes)
508+
# For a server error the spec has error.type mirror the status code.
509+
self.assertEqual(span.attributes["error.type"], span.attributes["db.response.status_code"])
501510
self.assertTrue(any(event.name == "exception" for event in span.events))
502511

512+
@client_context.require_failCommand_fail_point
513+
def test_error_type_is_exception_class_name_for_connection_failure(self):
514+
# A closed connection produces no server reply, so there is no error
515+
# code to report: the spec falls back to the exception's class name.
516+
client = self.rs_or_single_client(tracing={"enabled": True}, retryReads=False)
517+
fail_command = {
518+
"configureFailPoint": "failCommand",
519+
"mode": {"times": 1},
520+
"data": {"failCommands": ["find"], "closeConnection": True},
521+
}
522+
with self.fail_point(fail_command):
523+
self.exporter.clear()
524+
with self.assertRaises(ConnectionFailure) as ctx:
525+
client[self.db.name].test.find_one({})
526+
527+
spans = [s for s in self.spans() if s.attributes.get("db.command.name") == "find"]
528+
self.assertEqual(len(spans), 1)
529+
attrs = spans[0].attributes
530+
self.assertNotIn("db.response.status_code", attrs)
531+
self.assertEqual(attrs["error.type"], _qualified_name(type(ctx.exception)))
532+
# error.type and exception.type carry the same value here, by design:
533+
# one is the span attribute, the other the exception event's.
534+
self.assertEqual(attrs["error.type"], attrs["exception.type"])
535+
536+
@client_context.require_failCommand_blockConnection
537+
def test_error_type_is_exception_class_name_for_network_timeout(self):
538+
# socketTimeoutMS trips before the blocked command replies, so again
539+
# there is no server error code and error.type is the class name.
540+
client = self.rs_or_single_client(
541+
tracing={"enabled": True}, socketTimeoutMS=200, retryReads=False
542+
)
543+
fail_command = {
544+
"configureFailPoint": "failCommand",
545+
"mode": {"times": 1},
546+
"data": {
547+
"failCommands": ["find"],
548+
"blockConnection": True,
549+
"blockTimeMS": 1000,
550+
},
551+
}
552+
with self.fail_point(fail_command):
553+
self.exporter.clear()
554+
with self.assertRaises(NetworkTimeout) as ctx:
555+
client[self.db.name].test.find_one({})
556+
557+
spans = [s for s in self.spans() if s.attributes.get("db.command.name") == "find"]
558+
self.assertEqual(len(spans), 1)
559+
attrs = spans[0].attributes
560+
self.assertNotIn("db.response.status_code", attrs)
561+
self.assertEqual(attrs["error.type"], _qualified_name(NetworkTimeout))
562+
self.assertIsInstance(ctx.exception, NetworkTimeout)
563+
503564
def test_tracing_disabled_by_default(self):
504565
client = self.rs_or_single_client()
505566
self.exporter.clear()

0 commit comments

Comments
 (0)