FastestMCP emits telemetry events for core runtime activity and creates OpenTelemetry spans for both server execution and connected-client operations.
That gives you two levels of observability:
:telemetryevents for direct Elixir instrumentation- OpenTelemetry spans and trace propagation for distributed tracing systems
Telemetry Events
FastestMCP emits events around operation and auth work.
Common events include:
[:fastest_mcp, :operation, :start][:fastest_mcp, :operation, :stop][:fastest_mcp, :operation, :exception][:fastest_mcp, :auth, :start][:fastest_mcp, :auth, :stop][:fastest_mcp, :auth, :exception]
Attach handlers using standard :telemetry APIs:
handler_id = "fastest-mcp-docs"
:telemetry.attach_many(
handler_id,
[
[:fastest_mcp, :operation, :start],
[:fastest_mcp, :operation, :stop],
[:fastest_mcp, :operation, :exception]
],
fn event, measurements, metadata, _config ->
IO.inspect({event, measurements, metadata})
end,
nil
)The metadata includes fields such as the MCP method and server name, which is usually enough to build metrics and dashboards.
OpenTelemetry Spans
FastestMCP creates SERVER spans around operations, INTERNAL spans around provider delegation, and CLIENT spans around connected-client operations.
That is useful when you want traces that connect:
- inbound HTTP or stdio requests
- auth work
- provider delegation
- tool, resource, or prompt execution
- Phoenix or application work that calls a remote MCP server
FastestMCP uses W3C trace context propagation through request metadata and headers, so trace context can move through the runtime without custom glue in every handler.
For connected clients, one public operation owns one CLIENT span across its complete lifetime, including asynchronous cancellation and timeouts. Tool-call MRTR continuations and task updates use bounded child spans; pagination and task polling annotate the aggregate operation rather than creating one span per page or poll. Cache hits remain visible as client spans while trace identifiers stay outside cache keys and cached values.
Client propagation injects only traceparent and tracestate into MCP
params._meta. Existing nested metadata is preserved. On the server, that MCP
metadata takes precedence over direct request metadata and transport headers,
so the immediate MCP client span remains the remote server span's parent.
What FastestMCP Traces For You
FastestMCP already handles:
- span naming from MCP method and component type
- attributes for server name, MCP method, component key, transport, session, request id, and GenAI/MCP semantic fields
- exception recording on failed spans
- trace context extraction and injection
- client cache-hit, pagination, task-poll, and task-status annotations
- generic error status for tool results carrying
isError: true
Resource URIs are recorded as attributes rather than embedded in span names, so
high-cardinality resource values do not fragment trace dashboards. Failed spans
also include error.type, and nil attributes are dropped before they reach the
OpenTelemetry exporter.
Arguments, content, structured results, arbitrary MCP metadata, Authorization headers, tokens, and credentials are never recorded as span attributes or events.
Most applications do not need to call the internal tracing helper module directly. The runtime instrumentation is the default.
Telemetry vs Logging
Use Logging for human-readable events.
Use telemetry when you need:
- counters
- durations
- traces
- monitoring integrations
- alerts and dashboards
Logging and telemetry complement each other, but they are not interchangeable.
Example Test
Telemetry is straightforward to verify in ExUnit:
:telemetry.attach_many(
"fastest-mcp-test",
[
[:fastest_mcp, :operation, :start],
[:fastest_mcp, :operation, :stop],
[:fastest_mcp, :operation, :exception]
],
fn event, measurements, metadata, pid ->
send(pid, {:telemetry, event, measurements, metadata})
end,
self()
)
assert %{status: "ok"} == FastestMCP.call_tool(server_name, "ok", %{})
assert_receive {:telemetry, [:fastest_mcp, :operation, :start], _, _}
assert_receive {:telemetry, [:fastest_mcp, :operation, :stop], %{duration: duration}, _}
assert duration > 0Why This Shape
FastestMCP does not make you choose between BEAM-native telemetry and modern trace pipelines.
The runtime emits standard :telemetry events for local Elixir integrations
and layers OpenTelemetry spans on top for tracing systems. That keeps the
runtime observable without forcing one instrumentation style on every user.