# Telemetry

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:

- `:telemetry` events 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:

```elixir
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](logging.md) 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:

```elixir
: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 > 0
```

## Why 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.

## Related Guides

- [Logging](logging.md)
- [Testing](testing.md)
- [Middleware](middleware.md)
