Stop flagging snake_case is_error results as tool errors in OTel span (#2971)
This commit is contained in:
parent
1b1abf6ab6
commit
96bf22e57a
2 changed files with 12 additions and 5 deletions
|
|
@ -59,8 +59,12 @@ class OpenTelemetryMiddleware(ServerMiddleware[Any]):
|
||||||
span.set_status(StatusCode.ERROR, str(e))
|
span.set_status(StatusCode.ERROR, str(e))
|
||||||
raise
|
raise
|
||||||
if ctx.method == "tools/call":
|
if ctx.method == "tools/call":
|
||||||
|
# Tool errors are detected pre-serialization, so only shapes that reach the wire as an error
|
||||||
|
# count: the model, or the camelCase alias (`is_error` is dropped by the alias-only wire
|
||||||
|
# validation). A raw-dict `isError` is matched as a literal bool only - non-bool coercible
|
||||||
|
# values (1, "true") would serialize to an error but are rare enough to leave undetected.
|
||||||
match result:
|
match result:
|
||||||
case CallToolResult(is_error=True) | {"isError": True} | {"is_error": True}:
|
case CallToolResult(is_error=True) | {"isError": True}:
|
||||||
span.set_attribute("error.type", "tool_error")
|
span.set_attribute("error.type", "tool_error")
|
||||||
span.set_status(StatusCode.ERROR)
|
span.set_status(StatusCode.ERROR)
|
||||||
case _:
|
case _:
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,9 @@ async def test_tool_error_model_result_sets_error_type(server: SrvT, spans: Span
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_tool_error_snake_case_dict_result_sets_error_type(server: SrvT, spans: SpanCapture):
|
async def test_snake_case_dict_result_is_not_a_tool_error(server: SrvT, spans: SpanCapture):
|
||||||
|
# `is_error` is alias-only on the wire, so serialization drops it; the result reaches the
|
||||||
|
# client as a success and the span must not contradict that.
|
||||||
async def err_tool(ctx: Ctx, params: CallToolRequestParams) -> dict[str, Any]:
|
async def err_tool(ctx: Ctx, params: CallToolRequestParams) -> dict[str, Any]:
|
||||||
return {"content": [], "is_error": True}
|
return {"content": [], "is_error": True}
|
||||||
|
|
||||||
|
|
@ -100,11 +102,12 @@ async def test_tool_error_snake_case_dict_result_sets_error_type(server: SrvT, s
|
||||||
server.middleware.append(OpenTelemetryMiddleware())
|
server.middleware.append(OpenTelemetryMiddleware())
|
||||||
async with connected_runner(server) as (client, _):
|
async with connected_runner(server) as (client, _):
|
||||||
spans.clear()
|
spans.clear()
|
||||||
await client.send_raw_request("tools/call", {"name": "mytool", "arguments": {}})
|
result = await client.send_raw_request("tools/call", {"name": "mytool", "arguments": {}})
|
||||||
|
assert result == {"content": []}
|
||||||
[span] = [s for s in spans.finished() if s.kind == SpanKind.SERVER]
|
[span] = [s for s in spans.finished() if s.kind == SpanKind.SERVER]
|
||||||
assert span.attributes is not None
|
assert span.attributes is not None
|
||||||
assert span.attributes["error.type"] == "tool_error"
|
assert "error.type" not in span.attributes
|
||||||
assert span.status.status_code == StatusCode.ERROR
|
assert span.status.status_code == StatusCode.UNSET
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue