|
32 | 32 | from pymongo.errors import ( |
33 | 33 | ClientBulkWriteException, |
34 | 34 | ConfigurationError, |
| 35 | + ConnectionFailure, |
35 | 36 | InvalidOperation, |
| 37 | + NetworkTimeout, |
36 | 38 | OperationFailure, |
37 | 39 | ServerSelectionTimeoutError, |
38 | 40 | ) |
@@ -66,6 +68,11 @@ def _tracing_opts() -> _otel.TracingOptions: |
66 | 68 | return {"enabled": True, "query_text_max_length": 0} |
67 | 69 |
|
68 | 70 |
|
| 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 | + |
69 | 76 | @unittest.skipUnless(_HAS_OTEL_TEST_DEPS, "opentelemetry-sdk is not installed") |
70 | 77 | class TestOTelOperationSpanPrimitives(unittest.TestCase): |
71 | 78 | """Unit tests for the pymongo._otel operation-span primitives.""" |
@@ -498,8 +505,62 @@ async def test_failure_records_exception_and_status_code(self): |
498 | 505 | span = spans[0] |
499 | 506 | self.assertEqual(span.status.status_code, trace.StatusCode.ERROR) |
500 | 507 | 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"]) |
501 | 510 | self.assertTrue(any(event.name == "exception" for event in span.events)) |
502 | 511 |
|
| 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 | + |
503 | 564 | async def test_tracing_disabled_by_default(self): |
504 | 565 | client = await self.async_rs_or_single_client() |
505 | 566 | self.exporter.clear() |
|
0 commit comments