Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions pymongo/_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,14 @@ def end_command_span_success(span: Optional[Span], reply: _DocumentOut) -> None:
span.end()


def _set_exception_attributes(span: Span, exc: BaseException) -> None:
def _set_exception_attributes(span: Span, exc: BaseException) -> str:
"""Set exception.type/exception.message/exception.stacktrace span attributes.

``record_exception`` attaches these to an "exception" *event* only, but the
spec requires them as span *attributes* too, for both command and operation
spans. Formatting mirrors ``record_exception``.

:return: The ``exception.type`` value.
"""
module = type(exc).__module__
qualname = type(exc).__qualname__
Expand All @@ -428,6 +430,7 @@ def _set_exception_attributes(span: Span, exc: BaseException) -> None:
"exception.stacktrace",
"".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
)
return exception_type


def end_command_span_failure(
Expand All @@ -439,10 +442,14 @@ def end_command_span_failure(
if span is None:
return
span.record_exception(exc)
_set_exception_attributes(span, exc)
exception_type = _set_exception_attributes(span, exc)
code = failure.get("code")
if code is not None:
span.set_attribute("db.response.status_code", str(code))
span.set_attribute("error.type", str(code))
else:
# A network failure gets no server reply, so there is no code to report.
span.set_attribute("error.type", exception_type)
span.set_status(Status(StatusCode.ERROR, description=failure.get("errmsg")))
span.end()

Expand Down
59 changes: 59 additions & 0 deletions test/asynchronous/test_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
from pymongo.errors import (
ClientBulkWriteException,
ConfigurationError,
ConnectionFailure,
InvalidOperation,
NetworkTimeout,
OperationFailure,
ServerSelectionTimeoutError,
)
Expand Down Expand Up @@ -66,6 +68,11 @@ def _tracing_opts() -> _otel.TracingOptions:
return {"enabled": True, "query_text_max_length": 0}


def _qualified_name(exc_type: type) -> str:
"""Format an exception class the way the spans do: ``module.QualName``."""
return f"{exc_type.__module__}.{exc_type.__qualname__}"


@unittest.skipUnless(_HAS_OTEL_TEST_DEPS, "opentelemetry-sdk is not installed")
class TestOTelOperationSpanPrimitives(unittest.TestCase):
"""Unit tests for the pymongo._otel operation-span primitives."""
Expand Down Expand Up @@ -498,8 +505,60 @@ async def test_failure_records_exception_and_status_code(self):
span = spans[0]
self.assertEqual(span.status.status_code, trace.StatusCode.ERROR)
self.assertIn("db.response.status_code", span.attributes)
# For a server error the spec has error.type mirror the status code.
self.assertEqual(span.attributes["error.type"], span.attributes["db.response.status_code"])
self.assertTrue(any(event.name == "exception" for event in span.events))

@async_client_context.require_failCommand_fail_point
async def test_error_type_is_exception_class_name_for_connection_failure(self):
# A closed connection produces no server reply, so error.type falls back
# to the exception's class name.
client = await self.async_rs_or_single_client(tracing={"enabled": True}, retryReads=False)
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 1},
"data": {"failCommands": ["find"], "closeConnection": True},
}
async with self.fail_point(fail_command):
self.exporter.clear()
with self.assertRaises(ConnectionFailure) as ctx:
await client[self.db.name].test.find_one({})

spans = [s for s in self.spans() if s.attributes.get("db.command.name") == "find"]
self.assertEqual(len(spans), 1)
attrs = spans[0].attributes
self.assertNotIn("db.response.status_code", attrs)
self.assertEqual(attrs["error.type"], _qualified_name(type(ctx.exception)))
# error.type and exception.type carry the same value on this path.
self.assertEqual(attrs["error.type"], attrs["exception.type"])

@async_client_context.require_failCommand_blockConnection
async def test_error_type_is_exception_class_name_for_network_timeout(self):
# socketTimeoutMS trips before any reply, so again no server error code.
client = await self.async_rs_or_single_client(
tracing={"enabled": True}, socketTimeoutMS=200, retryReads=False
)
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 1},
"data": {
"failCommands": ["find"],
"blockConnection": True,
"blockTimeMS": 1000,
},
}
async with self.fail_point(fail_command):
self.exporter.clear()
with self.assertRaises(NetworkTimeout) as ctx:
await client[self.db.name].test.find_one({})

spans = [s for s in self.spans() if s.attributes.get("db.command.name") == "find"]
self.assertEqual(len(spans), 1)
attrs = spans[0].attributes
self.assertNotIn("db.response.status_code", attrs)
self.assertEqual(attrs["error.type"], _qualified_name(NetworkTimeout))
self.assertIsInstance(ctx.exception, NetworkTimeout)

async def test_tracing_disabled_by_default(self):
client = await self.async_rs_or_single_client()
self.exporter.clear()
Expand Down
267 changes: 267 additions & 0 deletions test/open_telemetry/operation/error_type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
{
"description": "error_type",
"schemaVersion": "1.27",
"createEntities": [
{
"client": {
"id": "client0",
"useMultipleMongoses": false,
"uriOptions": {
"retryReads": false
},
"observeTracingMessages": {
"enableCommandPayload": false
}
}
},
{
"client": {
"id": "failPointClient",
"useMultipleMongoses": false
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "operation-error-type"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "test"
}
}
],
"initialData": [
{
"collectionName": "test",
"databaseName": "operation-error-type",
"documents": []
}
],
"tests": [
{
"description": "error.type matches db.response.status_code for a server error",
"operations": [
{
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "failPointClient",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"find"
],
"errorCode": 8
}
}
}
},
{
"name": "find",
"object": "collection0",
"arguments": {
"filter": {
"x": 1
}
},
"expectError": {
"isError": true
}
}
],
"expectTracingMessages": [
{
"client": "client0",
"ignoreExtraSpans": true,
"spans": [
{
"name": "find operation-error-type.test",
"attributes": {
"db.system.name": "mongodb",
"db.namespace": "operation-error-type",
"db.collection.name": "test",
"db.operation.name": "find",
"db.operation.summary": "find operation-error-type.test",
"exception.message": {
"$$type": "string"
},
"exception.type": {
"$$type": "string"
},
"exception.stacktrace": {
"$$type": "string"
},
"error.type": {
"$$exists": false
}
},
"nested": [
{
"name": "find",
"attributes": {
"db.system.name": "mongodb",
"db.namespace": "operation-error-type",
"db.collection.name": "test",
"db.command.name": "find",
"network.transport": "tcp",
"db.response.status_code": "8",
"error.type": "8",
"exception.message": {
"$$type": "string"
},
"exception.type": {
"$$type": "string"
},
"exception.stacktrace": {
"$$type": "string"
},
"server.address": {
"$$type": "string"
},
"server.port": {
"$$type": [
"long",
"string"
]
},
"db.query.summary": "find operation-error-type.test",
"db.mongodb.server_connection_id": {
"$$type": [
"int",
"long"
]
},
"db.mongodb.driver_connection_id": {
"$$type": [
"int",
"long"
]
}
}
}
]
}
]
}
]
},
{
"description": "error.type falls back to the exception class name for a non-server error",
"operations": [
{
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "failPointClient",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"find"
],
"closeConnection": true
}
}
}
},
{
"name": "find",
"object": "collection0",
"arguments": {
"filter": {
"x": 1
}
},
"expectError": {
"isError": true
}
}
],
"expectTracingMessages": [
{
"client": "client0",
"ignoreExtraSpans": true,
"spans": [
{
"name": "find operation-error-type.test",
"attributes": {
"db.system.name": "mongodb",
"db.namespace": "operation-error-type",
"db.collection.name": "test",
"db.operation.name": "find",
"db.operation.summary": "find operation-error-type.test",
"exception.message": {
"$$type": "string"
},
"exception.type": {
"$$type": "string"
},
"exception.stacktrace": {
"$$type": "string"
},
"error.type": {
"$$exists": false
}
},
"nested": [
{
"name": "find",
"attributes": {
"db.system.name": "mongodb",
"db.namespace": "operation-error-type",
"db.collection.name": "test",
"db.command.name": "find",
"network.transport": "tcp",
"db.response.status_code": {
"$$exists": false
},
"error.type": {
"$$type": "string"
},
"exception.message": {
"$$type": "string"
},
"exception.type": {
"$$type": "string"
},
"exception.stacktrace": {
"$$type": "string"
},
"server.address": {
"$$type": "string"
},
"server.port": {
"$$type": [
"long",
"string"
]
},
"db.query.summary": "find operation-error-type.test",
"db.mongodb.driver_connection_id": {
"$$type": [
"int",
"long"
]
}
}
}
]
}
]
}
]
}
]
}
Loading
Loading