Skip to content

Conversation

@strawgate
Copy link
Collaborator

Add comprehensive documentation for integrating OpenTelemetry with FastMCP for distributed tracing and observability.

Changes

  • New integration guide at docs/integrations/opentelemetry.mdx
  • Covers logging integration with LoggingHandler
  • Demonstrates span creation via custom middleware
  • Includes production OTLP export configuration
  • Provides complete working example
  • Updated related docs with OpenTelemetry references
  • Registered in docs.json under new Observability section
  • Added working example in examples/opentelemetry_example.py

Closes #1998

Generated with Claude Code

Add comprehensive documentation for integrating OpenTelemetry with FastMCP:
- New integration guide at docs/integrations/opentelemetry.mdx
- Covers logging integration with LoggingHandler
- Demonstrates span creation via custom middleware
- Includes production OTLP export configuration
- Provides complete working example

Also update related docs:
- Add OpenTelemetry reference in middleware.mdx
- Add tip about OpenTelemetry in logging.mdx
- Register doc in docs.json under new Observability section

Example code:
- examples/opentelemetry_example.py with working weather server

Closes #1998

Co-authored-by: William Easton <[email protected]>
@marvin-context-protocol marvin-context-protocol bot added the documentation Updates to docs, examples, or guides. Primary change is documentation-related. label Oct 4, 2025
Copy link
Collaborator

@chrisguidry chrisguidry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering why we wouldn't just bake the instrumentation directly into FastMCP and enable it by default? It's not harmful to have the no-op/non-recording spans if you're not exporting them anywhere. There may be a very slight performance effect, but I think it's pretty minimal. Worst case, we could give people an option to disable it if they didn't want OTEL.

Comment on lines +63 to +75
# Set up tracing
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(trace_provider)

# Set up logging
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
set_logger_provider(logger_provider)

# Attach OpenTelemetry to FastMCP's logger
fastmcp_logger = get_logger("my_server")
fastmcp_logger.addHandler(LoggingHandler(logger_provider=logger_provider))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would be considered "manual" instrumentation, which is always an option, but wouldn't most folks want something like the "Zero Code" version: https://opentelemetry.io/docs/zero-code/python/

I think we should at least point folks to this as the default, and then we can explain in more detail the manual approach.

I do think there are some additional envvars or CLI switches required to get logging setup with the zero code approach.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this a little more? What we would have to do to give folks the zero code experience?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sure! The OTEL libraries (at least for Python, but I think for most languages) make a pretty strong distinction between producing telemetry (opentelemetry-api) and exporting it (opentelemetry-sdk) and they offer these zero-code approaches where you are essentially wrapping your entrypoint with opentelemetry-instrument (in Python) so that your code doesn't need to set up any exporters. Their canonical example looks like this:

opentelemetry-instrument \
    --traces_exporter console,otlp \
    --metrics_exporter console \
    --service_name your-service-name \
    --exporter_otlp_endpoint 0.0.0.0:4317 \
    python myapp.py

What would happen here is that your myapp.py might be using opentelemetry-api to make spans and metrics, and then when you run the program this way, the spans get exported to whichever ones you've configured there on the CLI. This example is very similar in spirit to what you laid out here, setting up OTLP exporters and attaching them to the providers, it's just handled by opentelemetry-instrument.

So my proposal is just to point people to that link for how they'd run their fastmcp servers by, say, wrapping them like:

opentelemetry-instrument \
    --traces_exporter console,otlp \
    --metrics_exporter console \
    --service_name my-mcp-server \
    --exporter_otlp_endpoint 0.0.0.0:4317 \
    fastmcp run mymcpserver.py


### Basic Tracing Middleware

Create a middleware that emits spans for all MCP requests:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we just ship this middleware with FastMCP? I'd argue it should be on by default, even.

@strawgate
Copy link
Collaborator Author

strawgate commented Oct 12, 2025

@chrisguidry I had Claude generate this, I was mostly going to stick with logging for now

I thought the problem with emitting spans was going to be that we don't have a way to accept or propagate the trace id through MCP calls so the spans would all be uncorrelated

I'd be very happy to do more if we thought it was useful and sensible

@chrisguidry
Copy link
Collaborator

we don't have a way to accept or propagate the trace id through MCP calls

Yep, that's a great call, and I think it might need to be pushed lower into the base SDK? I would make an argument that even without trace propagation, there's still some value because you'd see all of the things your MCP server called out to, even if each tool call was a separate trace. Also, as soon as the community figures out client-side trace propagation, it would all "just work" (LOL).

Didn't mean to block your efforts to get great documentation in for sure!

@strawgate
Copy link
Collaborator Author

we don't have a way to accept or propagate the trace id through MCP calls

Yep, that's a great call, and I think it might need to be pushed lower into the base SDK? I would make an argument that even without trace propagation, there's still some value because you'd see all of the things your MCP server called out to, even if each tool call was a separate trace. Also, as soon as the community figures out client-side trace propagation, it would all "just work" (LOL).

Didn't mean to block your efforts to get great documentation in for sure!

I'm ok adding it to our client and server as experimental until the lower level sdk gets it together, would that be interesting for you?

@pamelafox
Copy link

I found this PR when trying to add otel instrumentation to a FastMCP server. I notice no mention of this package:
https://github.com/traceloop/openllmetry/blob/main/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py

Would that package not be recommended? Is that something you'd add to FastMCP itself?

@pamelafox
Copy link

Update: I brought in the middleware from your PR and got it working with two OTel-compliant platforms. I also added in StarletteMiddleware on the FastMCP's Starlette app instance, for route-level tracing too.
You can see my usage here:
https://github.com/Azure-Samples/python-mcp-demos/pull/10/files#diff-2f9b1ef748487a1f4ade8aee44108cafa9db539ad34132e96912588d96e609de

I think it'd be great if this was added to FastMCP itself.
You may want to add options to turn off sending tool arguments in the spans, for PII/size reasons.

@strawgate
Copy link
Collaborator Author

strawgate commented Dec 3, 2025

@pamelafox im not sure using middleware makes the most sense here but im glad it worked for your use-case

@strawgate
Copy link
Collaborator Author

/marvin let's update the PR to bake the instrumentation right in

@marvin-context-protocol
Copy link
Contributor

marvin-context-protocol bot commented Dec 3, 2025

Claude finished @strawgate's task —— View job


Baking OpenTelemetry instrumentation into FastMCP ✓

Successfully integrated OpenTelemetry instrumentation directly into FastMCP core!

Completed Tasks

  • Review current OpenTelemetry documentation and examples
  • Review FastMCP architecture and middleware system
  • Design integration approach (built-in vs optional)
  • Implement built-in OTel middleware
  • Add optional dependencies for opentelemetry-api
  • Update documentation
  • Update examples
  • Create tests for the middleware
  • Run development workflow (uv sync, prek, pytest)
  • Commit and push changes

What Was Implemented

Built-in OpenTelemetry Middleware (src/fastmcp/server/middleware/opentelemetry.py):

  • Automatic span creation for all MCP operations:
    • Tool calls (tool.{name})
    • Resource reads (resource.read)
    • Prompt retrievals (prompt.{name})
    • List operations (tools.list, resources.list, etc.)
  • Graceful degradation: becomes no-op when OpenTelemetry not installed
  • Configuration options:
    • enabled: explicitly enable/disable (default: True)
    • include_arguments: control inclusion of sensitive data (default: True)
    • max_argument_length: prevent oversized spans (default: 500)
    • tracer_name: custom tracer naming (default: "fastmcp")

Optional Dependency:

  • Added fastmcp[opentelemetry] optional dependency
  • Installs opentelemetry-api>=1.20.0 and opentelemetry-sdk>=1.20.0

Updated Documentation:

  • Rewrote docs/integrations/opentelemetry.mdx to highlight built-in support
  • Added Quick Start section showing simple usage
  • Updated all examples to use built-in middleware
  • Documented configuration options and best practices

Tests:

  • Comprehensive test suite with 11 tests covering:
    • No-op behavior without OpenTelemetry installed
    • Configuration options
    • All MCP operation types
    • Error handling and propagation

Usage

Users can now simply add the middleware:

from fastmcp import FastMCP
from fastmcp.server.middleware.opentelemetry import OpenTelemetryMiddleware

mcp = FastMCP("My Server")
mcp.add_middleware(OpenTelemetryMiddleware())

If OpenTelemetry isn't installed, the middleware safely becomes a no-op. To enable full instrumentation:

pip install fastmcp[opentelemetry]

All validation checks pass ✓

--- | Branch: claude/issue-1998-20251004-0217

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Walkthrough

Adds OpenTelemetry observability across docs, examples, and server middleware. Inserts an "Observability" group under Integrations in docs/docs.json and adds docs/integrations/opentelemetry.mdx. Updates docs/servers/logging.mdx and docs/servers/middleware.mdx with pointers to the OpenTelemetry guide. Adds an example examples/opentelemetry_example.py demonstrating setup, exporters, and middleware wiring. Introduces OpenTelemetryMiddleware at src/fastmcp/server/middleware/opentelemetry.py with tracing hooks, optional argument capture, context propagation, and configuration options.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Draft] Document OpenTelemetry integration' clearly summarizes the main change - adding documentation for OpenTelemetry integration with FastMCP.
Description check ✅ Passed The description covers all required sections: clear summary of changes, comprehensive list of modifications, issue closure, and AI generation disclosure. Follows the template structure adequately.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #1998 by providing OpenTelemetry documentation with logging integration, span creation via middleware, production OTLP export guidance, and working examples.
Out of Scope Changes check ✅ Passed All changes are directly scoped to documenting OpenTelemetry integration: documentation files, example code, docs.json registration, and middleware implementation. No extraneous changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/issue-1998-20251004-0217

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
examples/opentelemetry_example.py (1)

121-213: Example tools and error handling are appropriate as a “happy path” demo

The three tools (get_weather, get_forecast, convert_temperature) demonstrate logging at different levels and basic validation without overcomplicating control flow. The explicit error messages and simple ValueError usage are fine here, and I’d ignore Ruff’s stylistic TRY*** suggestions for this educational example.

No changes needed from my side.

Based on learnings, keeping this as a clear happy‑path example without extra defensive layers is preferable.

Also applies to: 220-234

🧹 Nitpick comments (1)
docs/integrations/opentelemetry.mdx (1)

21-112: Surface zero-code instrumentation as the recommended starting point and clarify logging API stability

Two important additions to strengthen this guide:

  1. Lead with opentelemetry-instrument as the default approach
    The official OpenTelemetry Python documentation promotes opentelemetry-instrument as the zero-code instrumentation path. Add a brief section before "Logging Integration" that explains this is the recommended way to get started, then clarify that the rest of this page covers manual/fine-grained programmatic setup for cases where you need custom configuration. Include a link to OpenTelemetry's zero-code Python guide. This helps readers quickly find the simplest path without having to read the entire manual setup.

  2. Add forward-compatibility note for logging APIs
    The official OpenTelemetry Python documentation explicitly states the Logs API and SDK are still under active development. Add a note near the logging setup code (e.g., under "Basic Setup" or at the start of "Logging Integration") stating: "OpenTelemetry's Python Logs API is currently under development. If import paths change in future versions, refer to the official OpenTelemetry Python logs documentation." This prevents the guide from becoming stale as the logging signal stabilizes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee63405 and c6d6fb4.

📒 Files selected for processing (5)
  • docs/docs.json (1 hunks)
  • docs/integrations/opentelemetry.mdx (1 hunks)
  • docs/servers/logging.mdx (1 hunks)
  • docs/servers/middleware.mdx (1 hunks)
  • examples/opentelemetry_example.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/servers/logging.mdx
  • docs/integrations/opentelemetry.mdx
  • docs/servers/middleware.mdx
🧠 Learnings (1)
📚 Learning: 2025-11-03T17:36:13.363Z
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 2355
File: docs/clients/client.mdx:226-246
Timestamp: 2025-11-03T17:36:13.363Z
Learning: In FastMCP documentation, prefer showing the happy path in onboarding examples without over-explaining edge cases or adding defensive checks, as this reduces cognitive burden for new users learning the API.

Applied to files:

  • docs/integrations/opentelemetry.mdx
🧬 Code graph analysis (1)
examples/opentelemetry_example.py (2)
src/fastmcp/server/middleware/middleware.py (2)
  • Middleware (80-208)
  • MiddlewareContext (48-64)
src/fastmcp/utilities/logging.py (1)
  • get_logger (14-26)
🪛 Ruff (0.14.7)
examples/opentelemetry_example.py

92-92: Consider moving this statement to an else block

(TRY300)


164-164: Avoid specifying long messages outside the exception class

(TRY003)


198-198: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (3)
docs/servers/logging.mdx (1)

10-14: OpenTelemetry cross-link in logging docs looks good

The added sentence clearly routes readers from client logging to the broader OpenTelemetry observability guide and matches the new /integrations/opentelemetry path. No changes needed.

docs/servers/middleware.mdx (1)

596-600: Logging middleware Tip cleanly surfaces OpenTelemetry for production

The new intro sentence and Tip succinctly explain what the logging middleware does and point production users to the OpenTelemetry guide. This is consistent with the rest of the middleware docs and the new Observability section.

docs/docs.json (1)

216-222: Observability navigation group is well-placed and consistent

Adding an “Observability” group with integrations/opentelemetry under Integrations makes the new guide discoverable and matches the page path. No issues here.

Comment on lines 491 to 500
```python
from fastmcp.server.middleware.timing import TimingMiddleware
from fastmcp.server.middleware.logging import LoggingMiddleware

# Order matters: error handling first, then tracing, then logging
mcp.add_middleware(ErrorHandlingMiddleware())
mcp.add_middleware(OpenTelemetryMiddleware()) # Your custom middleware
mcp.add_middleware(TimingMiddleware()) # Built-in timing
mcp.add_middleware(LoggingMiddleware()) # Built-in logging
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Import for ErrorHandlingMiddleware missing in integration example

In the “Integration with FastMCP Middleware” example, you call ErrorHandlingMiddleware() but only import TimingMiddleware and LoggingMiddleware. As written, that snippet won’t run.

I’d suggest adding the import to keep the example copy‑pasteable:

-from fastmcp.server.middleware.timing import TimingMiddleware
-from fastmcp.server.middleware.logging import LoggingMiddleware
+from fastmcp.server.middleware.timing import TimingMiddleware
+from fastmcp.server.middleware.logging import LoggingMiddleware
+from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```python
from fastmcp.server.middleware.timing import TimingMiddleware
from fastmcp.server.middleware.logging import LoggingMiddleware
# Order matters: error handling first, then tracing, then logging
mcp.add_middleware(ErrorHandlingMiddleware())
mcp.add_middleware(OpenTelemetryMiddleware()) # Your custom middleware
mcp.add_middleware(TimingMiddleware()) # Built-in timing
mcp.add_middleware(LoggingMiddleware()) # Built-in logging
```
from fastmcp.server.middleware.timing import TimingMiddleware
from fastmcp.server.middleware.logging import LoggingMiddleware
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
# Order matters: error handling first, then tracing, then logging
mcp.add_middleware(ErrorHandlingMiddleware())
mcp.add_middleware(OpenTelemetryMiddleware()) # Your custom middleware
mcp.add_middleware(TimingMiddleware()) # Built-in timing
mcp.add_middleware(LoggingMiddleware()) # Built-in logging
🤖 Prompt for AI Agents
In docs/integrations/opentelemetry.mdx around lines 491 to 500 the example uses
ErrorHandlingMiddleware() but does not import it; add an import for
ErrorHandlingMiddleware alongside the other middleware imports at the top of the
snippet (e.g., from fastmcp.server.middleware.error import
ErrorHandlingMiddleware) so the example is copy-paste runnable.

Comment on lines +40 to +58
# Define service metadata
resource = Resource(
attributes={
"service.name": "fastmcp-weather-server",
"service.version": "1.0.0",
"deployment.environment": "development",
}
)

# Configure tracing
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(trace_provider)

# Configure logging
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
set_logger_provider(logger_provider)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the FastMCP logging utilities and check for logging configuration
find . -name "*.py" -type f | head -20

Repository: jlowin/fastmcp

Length of output: 995


🏁 Script executed:

# Search for logging configuration patterns
rg -n "basicConfig|setLevel|get_logger" --type=py | head -30

Repository: jlowin/fastmcp

Length of output: 2532


🏁 Script executed:

# Look at the opentelemetry_example.py file directly
fd "opentelemetry_example.py" -type f

Repository: jlowin/fastmcp

Length of output: 228


🏁 Script executed:

# Read the logging utilities to understand get_logger implementation
cat -n src/fastmcp/utilities/logging.py

Repository: jlowin/fastmcp

Length of output: 8842


🏁 Script executed:

# Read the opentelemetry_example.py file
cat -n examples/opentelemetry_example.py

Repository: jlowin/fastmcp

Length of output: 9292


Set logger level so INFO logs reach OpenTelemetry

The OTEL logging pipeline and LoggingHandler wiring look solid, but get_logger("weather") doesn't set a level here. Python's default effective level is WARNING, so your many logger.info(...) calls won't reach the OTEL handler.

Add logger.setLevel(logging.INFO) before attaching the handler:

+import logging
...
 logger = get_logger("weather")
+logger.setLevel(logging.INFO)
 logger.addHandler(LoggingHandler(logger_provider=logger_provider))

Also applies to: 110-113

🤖 Prompt for AI Agents
In examples/opentelemetry_example.py around lines 40-58 (and also apply same
change at 110-113), the logger created with get_logger("weather") does not have
an explicit level so INFO messages are blocked by the default WARNING level; set
the logger's level to logging.INFO (i.e., call logger.setLevel(logging.INFO))
before attaching any OpenTelemetry LoggingHandler so INFO logs are emitted to
the OTEL pipeline, and repeat the same change where the logger is created at
lines 110-113.

- Create OpenTelemetryMiddleware with automatic span creation for all MCP operations
- Add opentelemetry as optional dependency (pip install fastmcp[opentelemetry])
- Gracefully degrades to no-op when OpenTelemetry not installed
- Configuration options for privacy (include_arguments) and performance (max_argument_length)
- Update documentation to reflect built-in support
- Update example to use built-in middleware
- Add comprehensive tests

Co-authored-by: William Easton <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
examples/opentelemetry_example.py (1)

65-67: Set logger level to ensure INFO logs reach OpenTelemetry.

Without an explicit level, Python's default effective level is WARNING, so the logger.info(...) calls in the tool functions won't reach the OpenTelemetry handler. Add logger.setLevel(logging.INFO) after getting the logger.

+import logging
+
 # Attach OpenTelemetry to FastMCP's logger
 logger = get_logger("weather")
+logger.setLevel(logging.INFO)
 logger.addHandler(LoggingHandler(logger_provider=logger_provider))
🧹 Nitpick comments (3)
src/fastmcp/server/middleware/opentelemetry.py (3)

126-137: Consider more specific return type annotation.

The return type dict could be more specific as dict[str, Any] for better type checking.

-    def _create_span_attributes(self, context: MiddlewareContext, **extra: Any) -> dict:
+    def _create_span_attributes(self, context: MiddlewareContext, **extra: Any) -> dict[str, Any]:

139-141: Type annotations could be more specific to match base class.

The base Middleware class uses specific types like MiddlewareContext[mt.CallToolRequestParams] and CallNext[mt.CallToolRequestParams, ToolResult]. While Any works, matching the base class signatures would improve type safety.


185-187: Remove unnecessary f-string prefix.

The span name f"resource.read" contains no placeholders, so the f prefix is extraneous.

         with self.tracer.start_as_current_span(  # type: ignore[union-attr]
-            f"resource.read", attributes=span_attributes
+            "resource.read", attributes=span_attributes
         ) as span:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6d6fb4 and 8ecce7a.

⛔ Files ignored due to path filters (3)
  • pyproject.toml is excluded by none and included by none
  • tests/server/middleware/test_opentelemetry_middleware.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and included by none
📒 Files selected for processing (3)
  • docs/integrations/opentelemetry.mdx (1 hunks)
  • examples/opentelemetry_example.py (1 hunks)
  • src/fastmcp/server/middleware/opentelemetry.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/integrations/opentelemetry.mdx
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.py: Python source code must be version ≥3.10 with full type annotations
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Never use bare except in code - be specific with exception types

Files:

  • src/fastmcp/server/middleware/opentelemetry.py
🧠 Learnings (2)
📚 Learning: 2025-11-03T17:36:13.363Z
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 2355
File: docs/clients/client.mdx:226-246
Timestamp: 2025-11-03T17:36:13.363Z
Learning: In FastMCP documentation, prefer showing the happy path in onboarding examples without over-explaining edge cases or adding defensive checks, as this reduces cognitive burden for new users learning the API.

Applied to files:

  • docs/integrations/opentelemetry.mdx
📚 Learning: 2025-11-26T21:52:08.947Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: docs/.cursor/rules/mintlify.mdc:0-0
Timestamp: 2025-11-26T21:52:08.947Z
Learning: Applies to docs/**/*.mdx : Always include realistic error handling in code examples in MDX documentation

Applied to files:

  • docs/integrations/opentelemetry.mdx
🧬 Code graph analysis (1)
src/fastmcp/server/middleware/opentelemetry.py (1)
src/fastmcp/server/middleware/middleware.py (3)
  • CallNext (43-44)
  • Middleware (80-208)
  • MiddlewareContext (48-64)
🪛 Ruff (0.14.7)
examples/opentelemetry_example.py

119-119: Avoid specifying long messages outside the exception class

(TRY003)


153-153: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/middleware/opentelemetry.py

164-164: Consider moving this statement to an else block

(TRY300)


186-186: f-string without any placeholders

Remove extraneous f prefix

(F541)


191-191: Consider moving this statement to an else block

(TRY300)


221-221: Consider moving this statement to an else block

(TRY300)


243-243: Consider moving this statement to an else block

(TRY300)


265-265: Consider moving this statement to an else block

(TRY300)


287-287: Consider moving this statement to an else block

(TRY300)


309-309: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (14)
docs/integrations/opentelemetry.mdx (7)

1-5: LGTM!

Frontmatter is correctly structured with required title and description fields, plus an appropriate icon.


21-57: LGTM!

The Quick Start section provides a clear, minimal example showing the happy path. The graceful degradation note and installation options are well-documented.


154-199: LGTM!

The middleware configuration options and traced operations are well-documented and align with the implementation in opentelemetry.py.


412-427: LGTM!

The middleware integration example correctly imports only the middleware that is used. The execution order explanation is helpful for understanding the observability pipeline.


234-286: LGTM!

The complete example demonstrates a production-ready setup combining logging and tracing with realistic tool implementation.


387-406: LGTM!

The security guidance with the redaction example is practical and demonstrates a clear pattern for avoiding sensitive data in traces.


70-72: This review comment is unnecessary. The documentation already contains a clear stability warning in a <Note> component (line 50-56) that explicitly states: "Tracing and metrics are stable, while logging is in active development." This note appears immediately before the Logging Integration section and adequately informs users that the logging API is under active development and subject to change. The underscore-prefixed imports are the correct and documented approach for using OpenTelemetry Python logging at this time.

Likely an incorrect or invalid review comment.

src/fastmcp/server/middleware/opentelemetry.py (2)

42-49: LGTM!

The optional dependency handling is well-implemented - catches ImportError specifically and sets a module-level flag for graceful degradation.


227-313: LGTM!

All list operation handlers follow a consistent pattern with proper span lifecycle management, attribute recording, and exception handling.

examples/opentelemetry_example.py (5)

39-56: LGTM!

The OpenTelemetry configuration follows best practices with batch processors and proper resource attributes for service identification.


77-101: LGTM!

The get_weather tool demonstrates proper logging integration with the OpenTelemetry pipeline using realistic mock data.


104-133: LGTM!

The get_forecast tool demonstrates input validation with appropriate error handling and logging at multiple levels (INFO for normal operations, WARNING for invalid input).


136-168: LGTM!

The convert_temperature tool demonstrates comprehensive logging at different levels (DEBUG, INFO, ERROR) and proper input validation with unit conversion logic.


175-189: LGTM!

The main block provides helpful context for users running the example, with clear guidance about production deployment.

@strawgate
Copy link
Collaborator Author

strawgate commented Dec 3, 2025

/marvin while creating spans in middleware is great and all, we also want to push the context information into response _meta fields and then when we receive a result with _meta trace id or whatever, we can use that to set the trace id for the context. That way someone calling us over a weird protocol like SSE can still propagate context for distributed traces

@marvin-context-protocol
Copy link
Contributor

marvin-context-protocol bot commented Dec 3, 2025

Claude finished @strawgate's task —— View job


Adding trace context propagation to OpenTelemetry middleware ✓

Successfully implemented trace context propagation through MCP _meta fields for cross-protocol observability!

Completed Tasks

  • Review current OpenTelemetry middleware implementation
  • Understand MCP protocol metadata structure
  • Implement trace context injection into response metadata
  • Implement trace context extraction from request metadata
  • Update tests for context propagation
  • Update documentation
  • Run validation workflow (uv sync, prek, pytest)
  • Commit and push changes

Implementation Summary

Added trace context propagation to OpenTelemetryMiddleware using W3C Trace Context standard:

Key Features:

  • New parameter: propagate_context (default: True) enables/disables trace context propagation
  • Automatic extraction: Reads traceparent and tracestate from request _meta fields
  • Automatic injection: Writes current trace context to response _meta fields
  • Universal support: Works across all MCP operations (tools, resources, prompts, list operations)
  • Standard format: Uses W3C Trace Context for interoperability with existing tools

Changes Made:

  • Modified src/fastmcp/server/middleware/opentelemetry.py:
    • Added context extraction and injection helper methods
    • Updated all operation handlers to use parent context when available
    • Added W3C TraceContextTextMapPropagator for standard format
  • Added 5 new tests in tests/server/middleware/test_opentelemetry_middleware.py
  • Updated docs/integrations/opentelemetry.mdx with configuration examples

Benefits:

  • Enables distributed tracing across protocols without HTTP headers (SSE, stdio, etc.)
  • Automatically links spans when clients provide trace context in requests
  • Allows downstream trace propagation to other MCP servers
  • Gracefully degrades when trace context isn't provided

Example Usage:

from fastmcp import FastMCP
from fastmcp.server.middleware.opentelemetry import OpenTelemetryMiddleware

mcp = FastMCP("My Server")
mcp.add_middleware(OpenTelemetryMiddleware(propagate_context=True))

All validation checks pass ✓

--- | Branch: claude/issue-1998-20251004-0217

Enable distributed tracing across protocols that don't support HTTP headers (like SSE) by propagating W3C Trace Context through MCP _meta fields.

- Add propagate_context parameter (default: True) to OpenTelemetryMiddleware
- Implement _extract_trace_context() to read traceparent/tracestate from request metadata
- Implement _inject_trace_context() to write trace context to response metadata
- Update all operation handlers to extract parent context and inject into results
- Add comprehensive test coverage for context propagation
- Update documentation with examples and configuration details

This enables trace continuity across MCP calls, allowing clients to link server spans to their traces and propagate context downstream.

Co-authored-by: William Easton <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/fastmcp/server/middleware/opentelemetry.py (3)

56-135: Minor typing nit: add explicit -> None to __init__

Everything here looks solid; to fully align with the “full type annotations” guideline, it’d be good to add an explicit return type to __init__.

You can minimally do:

-    def __init__(
-        self,
-        tracer_name: str = "fastmcp",
-        enabled: bool = True,
-        include_arguments: bool = True,
-        max_argument_length: int = 500,
-        propagate_context: bool = True,
-    ):
+    def __init__(
+        self,
+        tracer_name: str = "fastmcp",
+        enabled: bool = True,
+        include_arguments: bool = True,
+        max_argument_length: int = 500,
+        propagate_context: bool = True,
+    ) -> None:

334-435: List operations don’t currently propagate trace context in responses

For the various on_list_* methods you’re intentionally only recording counts and status and returning result unchanged, with a comment noting that list operations don’t get context injection. That’s totally fine for an initial cut and keeps behavior simple.

If/when you want full propagation parity with single-object responses (e.g., for SSE-style clients relying on _meta), you may eventually want to design a convention for attaching trace context to list responses (either via an envelope object or by populating per-item _meta fields). That would require a broader API decision, so treating this as a follow-up rather than changing it now makes sense.


143-221: Refactor exception logging to use exc_info=True for better debuggability

The trace extraction/injection helpers are well-isolated and correctly guarded. Catching Exception here is appropriate to keep telemetry from breaking requests. However, the logging can be improved by including the full stack trace for easier diagnosis:

In _extract_trace_context:

-        except Exception as e:
-            logger.debug(f"Failed to extract trace context from metadata: {e}")
+        except Exception:
+            logger.debug(
+                "Failed to extract trace context from metadata",
+                exc_info=True,
+            )

In _inject_trace_context:

-        except Exception as e:
-            logger.debug(f"Failed to inject trace context into metadata: {e}")
+        except Exception:
+            logger.debug(
+                "Failed to inject trace context into metadata",
+                exc_info=True,
+            )

This preserves the "don't let instrumentation crash the app" property while making issues easier to diagnose. The OpenTelemetry API usage (dict carrier with named carrier= argument, trace.get_current_span() pattern) is correct for versions ≥1.20.0.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ecce7a and a9b6e71.

⛔ Files ignored due to path filters (1)
  • tests/server/middleware/test_opentelemetry_middleware.py is excluded by none and included by none
📒 Files selected for processing (2)
  • docs/integrations/opentelemetry.mdx (1 hunks)
  • src/fastmcp/server/middleware/opentelemetry.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/integrations/opentelemetry.mdx
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.py: Python source code must be version ≥3.10 with full type annotations
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Never use bare except in code - be specific with exception types

Files:

  • src/fastmcp/server/middleware/opentelemetry.py
🧬 Code graph analysis (1)
src/fastmcp/server/middleware/opentelemetry.py (2)
src/fastmcp/server/middleware/middleware.py (3)
  • CallNext (43-44)
  • Middleware (80-208)
  • MiddlewareContext (48-64)
src/fastmcp/server/context.py (3)
  • Context (100-716)
  • debug (393-407)
  • info (409-423)
🪛 Ruff (0.14.7)
src/fastmcp/server/middleware/opentelemetry.py

171-171: Do not catch blind exception: Exception

(BLE001)


217-217: Do not catch blind exception: Exception

(BLE001)


354-354: Consider moving this statement to an else block

(TRY300)


379-379: Consider moving this statement to an else block

(TRY300)


406-406: Consider moving this statement to an else block

(TRY300)


431-431: Consider moving this statement to an else block

(TRY300)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Updates to docs, examples, or guides. Primary change is documentation-related.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document logging with OpenTelemetry

4 participants