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
15 changes: 12 additions & 3 deletions pymongo/_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,12 +446,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``.
spans. Formatting mirrors ``record_exception``. Returns the computed
``exception.type`` value so callers (e.g. ``error.type``) can reuse it
without recomputing.
"""
module = type(exc).__module__
qualname = type(exc).__qualname__
Expand All @@ -462,6 +464,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 @@ -473,10 +476,16 @@ 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:
# Server error: error.type mirrors db.response.status_code, per spec.
span.set_attribute("db.response.status_code", str(code))
span.set_attribute("error.type", str(code))
else:
# Non-server error (e.g. network failure): fall back to the
# exception's class name, since there's no server error code to report.
span.set_attribute("error.type", exception_type)
span.set_status(Status(StatusCode.ERROR, description=failure.get("errmsg")))
span.end()

Expand Down
61 changes: 61 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,62 @@ 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 there is no error
# code to report: the spec 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 here, by design:
# one is the span attribute, the other the exception event's.
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 the blocked command replies, so again
# there is no server error code and error.type is the class name.
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