Skip to content

Commit 0d874c1

Browse files
committed
docs: clarify OpenTelemetry spans for resources and prompts
1 parent 9bdc03d commit 0d874c1

3 files changed

Lines changed: 50 additions & 4 deletions

File tree

docs/run/opentelemetry.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ call `MCPServer(...)`.
1111
```
1212

1313
That is a complete, traced server. Call `search_books` and a span is created for it. The same is
14-
true for the low-level `Server`: the tracing lives on both.
14+
true when a client reads `catalog://featured` or renders `reading_prompt`. The low-level `Server`
15+
is traced too.
1516

1617
## What you get
1718

18-
Every inbound message becomes a `SERVER` span named after the method and its target. So a
19-
`tools/call` for `search_books` is the span `tools/call search_books`, and a bare `tools/list`
19+
Every inbound message becomes a `SERVER` span named after the method, plus a target for named
20+
operations. So a `tools/call` for `search_books` is the span `tools/call search_books`,
21+
a `prompts/get` for `reading_prompt` is `prompts/get reading_prompt`, and a bare `tools/list`
2022
is just `tools/list`.
2123

2224
Each span carries a few attributes:
@@ -31,13 +33,25 @@ OpenTelemetry's [GenAI semantic conventions](https://opentelemetry.io/docs/specs
3133
* `gen_ai.operation.name`, set to `"execute_tool"`.
3234
* `gen_ai.tool.name`, set to the tool being called.
3335

34-
A `prompts/get` span gets `gen_ai.prompt.name` in the same spirit. The list methods carry no
36+
A `prompts/get` span gets `gen_ai.prompt.name` in the same spirit. `resources/read` spans are
37+
created by the same middleware and carry the common `mcp.*` attributes; the SDK does not add the
38+
resource URI to the span name or to a `gen_ai.*` attribute today. The list methods carry no
3539
`gen_ai.*` keys, because there is nothing to name.
3640

3741
!!! tip
3842
Those GenAI attributes are the reason a tracing UI groups your tool calls the way it groups
3943
any other agent's. You get that grouping for free, with no extra code.
4044

45+
## Adding your own detail
46+
47+
The SDK span wraps the request handler. That means a tool, resource, or prompt function runs with
48+
the SDK-created span already current.
49+
50+
If you need more detail than the default attributes provide, create child spans inside your handler
51+
or in your own middleware. For example, a resource handler can add a child span for a database read,
52+
and a prompt handler can add one for template assembly. Those spans nest under `resources/read` or
53+
`prompts/get`, so a tracing backend still shows one connected request.
54+
4155
## It costs nothing until you want it
4256

4357
Here is the part that makes "on by default" a comfortable default.

docs_src/opentelemetry/tutorial001.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@
77
def search_books(query: str) -> str:
88
"""Search the catalog by title or author."""
99
return f"Found 3 books matching {query!r}."
10+
11+
12+
@mcp.resource("catalog://featured")
13+
def featured_books() -> str:
14+
"""The featured books shelf."""
15+
return "Dune\nThe Left Hand of Darkness\nA Wizard of Earthsea"
16+
17+
18+
@mcp.prompt()
19+
def reading_prompt(topic: str) -> str:
20+
"""Create a reading recommendation prompt."""
21+
return f"Recommend one book about {topic}."

tests/docs_src/test_opentelemetry.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ async def test_a_plain_server_is_traced_with_no_extra_code(capfire: CaptureLogfi
2424
assert attributes["gen_ai.tool.name"] == "search_books"
2525

2626

27+
async def test_resources_and_prompts_are_traced_at_the_request_level(capfire: CaptureLogfire) -> None:
28+
"""tutorial001: resource reads and prompt renders use request-level SERVER spans."""
29+
async with Client(tutorial001.mcp) as client:
30+
await client.read_resource("catalog://featured")
31+
await client.get_prompt("reading_prompt", {"topic": "Dune"})
32+
33+
spans = {s["name"]: s for s in capfire.exporter.exported_spans_as_dict()}
34+
35+
resource_attributes = spans["resources/read"]["attributes"]
36+
assert resource_attributes["mcp.method.name"] == "resources/read"
37+
assert "gen_ai.operation.name" not in resource_attributes
38+
assert "gen_ai.prompt.name" not in resource_attributes
39+
assert "gen_ai.tool.name" not in resource_attributes
40+
41+
prompt_attributes = spans["prompts/get reading_prompt"]["attributes"]
42+
assert prompt_attributes["mcp.method.name"] == "prompts/get"
43+
assert prompt_attributes["gen_ai.prompt.name"] == "reading_prompt"
44+
assert "gen_ai.operation.name" not in prompt_attributes
45+
46+
2747
async def test_client_and_server_share_one_trace(capfire: CaptureLogfire) -> None:
2848
"""When both sides run the SDK, the client and server spans land in one trace (SEP-414)."""
2949
async with Client(tutorial001.mcp, mode="legacy") as client:

0 commit comments

Comments
 (0)