ConduitMcp.Telemetry (ConduitMCP v0.11.0)

Copy Markdown View Source

Telemetry integration for ConduitMCP.

ConduitMCP uses the :telemetry library for instrumentation. This module documents all telemetry events emitted by the library and provides helper functions for setting up event handlers.

Events

All events are prefixed with [:conduit_mcp].

Request Events

[:conduit_mcp, :request, :stop]

Emitted when an MCP request completes, whether successfully or with an error.

Measurements:

Metadata:

  • :method (String.t | nil) - The MCP method that was called (e.g., "initialize", "tools/list", "tools/call"); nil when the request's "method" is missing or not a string

  • :server_module (module) - The MCP server module handling the request
  • :status (:ok | :error) - Whether the request succeeded or failed

  • :era (:modern | :legacy) - The request's protocol era (see ConduitMcp.Era)

  • :protocol_version (String.t | nil) - The modern request's _meta protocol version, or the legacy session's negotiated version; nil when neither is known

  • :traceparent, :tracestate, :baggage (String.t | nil) - W3C trace context from the request's _meta, already validated (traceparent well-formed, tracestate/baggage at most 512 bytes of printable ASCII); nil when absent or invalid

Example:

:telemetry.attach(
  "log-mcp-requests",
  [:conduit_mcp, :request, :stop],
  fn _event, measurements, metadata, _config ->
    duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)

    IO.inspect(%{
      method: metadata.method,
      status: metadata.status,
      duration_ms: duration_ms
    }, label: "MCP Request")
  end,
  nil
)

Tool Execution Events

[:conduit_mcp, :tool, :execute]

Emitted when a tool is executed via the tools/call method.

Measurements:

  • :duration (integer) - Tool execution duration in native time units

Metadata:

  • :tool_name (String.t | nil) - Name of the tool that was executed; nil when the client's "name" is missing or not a string

  • :server_module (module) - The MCP server module
  • :status (:ok | :error) - Whether the tool executed successfully (an MRTR input_required round counts as :ok)

Example:

:telemetry.attach(
  "track-tool-performance",
  [:conduit_mcp, :tool, :execute],
  fn _event, measurements, metadata, _config ->
    duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)

    if duration_ms > 1000 do
      Logger.warning("Slow tool execution: #{metadata.tool_name} (#{duration_ms}ms)")
    end
  end,
  nil
)

Resource Events

[:conduit_mcp, :resource, :read]

Emitted when a resource is read via the resources/read method.

Measurements:

  • :duration (integer) - Resource read duration in native time units

Metadata:

  • :uri (String.t | nil) - URI of the resource that was read; nil when the client's "uri" is missing or not a string

  • :server_module (module) - The MCP server module
  • :status (:ok | :error) - Whether the read succeeded or failed (an MRTR input_required round counts as :ok)

Example:

:telemetry.attach(
  "track-resource-reads",
  [:conduit_mcp, :resource, :read],
  fn _event, measurements, metadata, _config ->
    duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
    Logger.info("Resource read: uri=#{metadata.uri} duration=#{duration_ms}ms")
  end,
  nil
)

Prompt Events

[:conduit_mcp, :prompt, :get]

Emitted when a prompt is retrieved via the prompts/get method.

Measurements:

  • :duration (integer) - Prompt retrieval duration in native time units

Metadata:

  • :prompt_name (String.t | nil) - Name of the prompt that was retrieved; nil when the client's "name" is missing or not a string

  • :server_module (module) - The MCP server module
  • :status (:ok | :error) - Whether the prompt retrieval succeeded (an MRTR input_required round counts as :ok)

Example:

:telemetry.attach(
  "track-prompt-usage",
  [:conduit_mcp, :prompt, :get],
  fn _event, _measurements, metadata, _config ->
    Logger.info("Prompt requested: #{metadata.prompt_name}")
  end,
  nil
)

MRTR Events

[:conduit_mcp, :mrtr, :input_required]

Emitted when a modern-era tools/call, resources/read or prompts/get answers with an InputRequiredResult (see "Multi round-trip requests (MRTR)" in ConduitMcp.Server) — after the input requests passed the client-capability check and the state was sealed. Not emitted for a rejected round (-32021, -32603).

Measurements:

  • :count (integer) - Number of input requests in the result

Metadata:

  • :method (String.t) - "tools/call", "resources/read" or "prompts/get"
  • :server_module (module) - The MCP server module
  • :round (pos_integer) - Which input_required round of the exchange this is: 1 for a first request, n + 1 for a retry whose verified requestState came from round n

Neither the input requests, the state nor the requestState token is included.

Subscription Events

A modern subscriptions/listen stream (see ConduitMcp.Subscriptions) emits these instead of [:conduit_mcp, :request, :stop]: its duration is how long the stream stayed open, which would swamp a latency metric. A listen refused before its stream opened (HTTP 406 or 503) emits neither.

[:conduit_mcp, :subscription, :start]

Emitted once the stream's SSE response has started, just before the acknowledgment is written.

Measurements:

  • :system_time (integer) - System.system_time/0 at the start
  • :resource_subscriptions (integer) - Number of resource URIs honoured

Metadata:

  • :filters ([atom]) - The filter fields honoured, a subset of :tools_list_changed, :prompts_list_changed, :resources_list_changed, :resource_subscriptions and :task_ids (ConduitMcp.Ext.Tasks enabled only)
  • :server_module (module) - The MCP server module

[:conduit_mcp, :subscription, :stop]

Emitted when the stream ends.

Measurements:

  • :duration (integer) - How long the stream was open, in native time units

Metadata:

  • :filters ([atom]) - As on :start
  • :server_module (module) - The MCP server module
  • :reason (atom) - :client_closed (the client went away), :max_lifetime (:max_stream_lifetime reached), :token_expired (the bearer token's exp passed), :idle_timeout (:listen_idle_timeout without a notification) or :superseded (a newer listen of the same principal took its place at :max_listens_per_principal). Every reason but :client_closed ended with notifications/cancelled and the graceful-closure result.

[:conduit_mcp, :subscription, :drop]

Emitted by ConduitMcp.Subscriptions.publish/2 for each stream it skips because the stream's mailbox already holds ConduitMcp.Subscriptions.max_queue/0 messages (a client that stopped reading). That stream misses the notification; the others still get it.

Measurements:

  • :message_queue_len (integer) - The stream's mailbox length

Metadata:

  • :topic (atom) - The topic kind: :tools, :prompts, :resources, :resource or :task (never a URI, task id or principal)

Neither event carries the resource URIs, the listen request id or the principal.

Rate Limiting Events

[:conduit_mcp, :rate_limit, :check]

Emitted when a rate limit check is performed.

Measurements:

  • :duration (integer) - Rate limit check duration in native time units

Metadata:

  • :key (String.t) - The rate limit key (e.g., IP address or user ID)
  • :status (:allow | :deny) - Whether the request was allowed or denied

  • :count (integer) - Current request count (only when status is :allow)
  • :retry_after (integer) - Seconds until the limit resets (only when status is :deny)

Example:

:telemetry.attach(
  "track-rate-limits",
  [:conduit_mcp, :rate_limit, :check],
  fn _event, _measurements, metadata, _config ->
    if metadata.status == :deny do
      Logger.warning("Rate limited: key=#{metadata.key}")
    end
  end,
  nil
)

Message Rate Limiting Events

[:conduit_mcp, :message_rate_limit, :check]

Emitted when a message-level rate limit check is performed.

Measurements:

  • :duration (integer) - Rate limit check duration in native time units

Metadata:

  • :key (String.t) - The rate limit key (e.g., "msg:user:123" or "msg:127.0.0.1")
  • :status (:allow | :deny) - Whether the message was allowed or denied

  • :method (String.t | nil) - The MCP method being rate limited

  • :count (integer) - Current message count (only when status is :allow)
  • :retry_after (integer) - Seconds until the limit resets (only when status is :deny)

Example:

:telemetry.attach(
  "track-message-rate-limits",
  [:conduit_mcp, :message_rate_limit, :check],
  fn _event, _measurements, metadata, _config ->
    if metadata.status == :deny do
      Logger.warning("Message rate limited: key=#{metadata.key} method=#{metadata.method}")
    end
  end,
  nil
)

Authentication Events

[:conduit_mcp, :auth, :verify]

Emitted when authentication verification is performed.

Measurements:

  • :duration (integer) - Authentication verification duration in native time units

Metadata:

  • :strategy (:bearer_token | :api_key | :function | :oauth) - Auth strategy used (the deprecated :custom strategy reports :function)

  • :status (:ok | :error) - Whether authentication succeeded

  • :reason (atom) - Failure reason, only present when status is :error. Always an atom, never verifier or token text: :invalid_credential / :invalid_return from ConduitMcp.Plugs.Auth; for :oauth, one of the set in the "Telemetry" section of ConduitMcp.Plugs.OAuth

Example:

:telemetry.attach(
  "track-auth-failures",
  [:conduit_mcp, :auth, :verify],
  fn _event, _measurements, metadata, _config ->
    if metadata.status == :error do
      Logger.warning("Auth failed: strategy=#{metadata.strategy} reason=#{inspect(metadata.reason)}")
    end
  end,
  nil
)

Cancellation Events

[:conduit_mcp, :request, :cancelled]

Emitted when a notifications/cancelled is recorded for a request in flight in the caller's scope (see ConduitMcp.Cancellation).

Measurements:

  • :count (integer) - Always 1

Metadata:

  • :request_id (String.t | integer) - The cancelled request's JSON-RPC id

  • :scope (String.t) - The caller's cancellation scope ("session:...", "principal:..." or "ip:...")
  • :reason (String.t | nil) - The client's reason, clamped and stripped of control characters. Client-supplied: never use it as a metric tag.

[:conduit_mcp, :cancellation, :cleanup]

Emitted on every ConduitMcp.Cancellation.cleanup/1 pass.

Measurements:

  • :removed (integer) - Expired cancellation rows deleted
  • :in_flight_removed (integer) - In-flight rows of dead request processes deleted

Metadata: none (%{}).

Janitor Events

Emitted after each periodic sweep of the tables ConduitMcp.Application owns.

[:conduit_mcp, :session, :cleanup]

From ConduitMcp.Session.Janitor sweeping expired sessions.

Measurements: :removed (integer) - Sessions evicted.

Metadata: :store (module) - The session store swept.

[:conduit_mcp, :cancellation, :janitor]

From the cancellation janitor (a ConduitMcp.Session.Janitor over ConduitMcp.Cancellation). Its :removed is the same count the run's [:conduit_mcp, :cancellation, :cleanup] reports, so count runs with it rather than summing both.

Measurements: :removed (integer) - Cancellation rows evicted.

Metadata: :store (module) - ConduitMcp.Cancellation.

[:conduit_mcp, :tasks, :cleanup]

From ConduitMcp.Tasks.Janitor sweeping terminal tasks.

Measurements: :removed (integer) - Tasks evicted.

Metadata: none (%{}).

Client Events

Emitted by ConduitMcp.Client. Metadata comes from a fixed allowlist and never carries _meta, tokens or raw error data, nor tool arguments or results unless the host opts in to payload capture (below). Every client event's metadata has:

  • :client (term) - the client's :name
  • :server_id (String.t) - the client's :server_id
  • :transport (atom) - the transport kind (:stdio, :http, :in_process)
  • :era (:modern | :legacy | nil) - the negotiated era; nil before the first handshake

The request, tool_call and input_request spans, policy.decision and the hook events are emitted in the calling process, so tracing bridges nest them under the caller's span, and tool.rejected in the process that fetched the tool list (a caller, or a cache refresh task). The other client events are emitted inside the client's connection process: their handlers must be fast.

[:conduit_mcp, :client, :request, :start | :stop | :exception]

One span per request sent (each round of a multi-round-trip call is its own request).

Measurements: :start has :monotonic_time and :system_time; :stop and :exception have :duration (native units) and :monotonic_time.

Metadata: the common keys, plus :method (String.t). :stop adds :status (:ok | :error), :error_kind (a ConduitMcp.Client.Error kind or nil) and :request_id (integer, or nil if never sent). :exception (a raise in an :on_progress callback) adds :kind.

[:conduit_mcp, :client, :tool_call, :start | :stop | :exception]

One span per ConduitMcp.Client.call_tool/4, around all its rounds.

Measurements: as for request; :stop adds :rounds (integer).

Metadata: the common keys, plus :tool_name (String.t, sanitized). :stop adds :status, :error_kind and :is_error (boolean, or nil when no result came back). With payload capture on, :start adds :arguments and :stop adds :result; with it off (the default) neither key is present.

Payload capture

Off by default (TL-2). A client started with telemetry: [capture_payloads: [redact: &MyApp.redact/1, max_bytes: 4_096]] adds :arguments to :start and :result to :stop. :redact is required (capture_payloads: true is rejected when the client starts) and runs in the calling process, before any handler sees the value:

  • :arguments - redact.(args), where args are the arguments the caller passed to ConduitMcp.Client.call_tool/4 (on a governed client, before pre-hooks: the span opens before governance runs);
  • :result - redact.(map), where map is the returned ConduitMcp.Client.ToolResult as %{"content" => ..., "isError" => ..., "structuredContent" => ...} (no _meta; after post-hooks and shaping on a governed client), or nil when the call returned an error or a task.

Each value is what redact returned when it JSON-encodes within :max_bytes (default 4 096); {:truncated, byte_size} when it is larger (never a partial value); :redaction_failed when redact raises, throws or exits, or returns something that cannot be JSON-encoded. A ConduitMcp.Client.Secret refuses to encode, so one left in place by redact yields :redaction_failed, never the secret. No hash or digest of a payload is ever emitted: low-entropy arguments are brute-forceable from one.

[:conduit_mcp, :client, :connection, :transition]

The connection changed state (see ConduitMcp.Client.Connection).

Measurements: :system_time.

Metadata: the common keys, plus :from (atom, nil at start), :to (atom), :reason (atom - a closed-set tag of the cause, nil when none) and :attempt (integer - consecutive reconnect attempts).

[:conduit_mcp, :client, :request, :cancel]

The connection cancelled an in-flight request.

Measurements: :count (always 1).

Metadata: the common keys, plus :method, :request_id and :reason (:timeout | :caller_down | :caller_timeout).

[:conduit_mcp, :client, :response, :dropped]

A response arrived for a request that is no longer pending (late, or cancelled).

Measurements: :count (always 1).

Metadata: the common keys, plus :request_id (integer, or nil for an id the client never issued).

[:conduit_mcp, :client, :notification]

A server notification reached the connection.

Measurements: :count (always 1).

Metadata: the common keys, plus :method (String.t, sanitized; server-supplied, so never use it as a metric tag).

[:conduit_mcp, :client, :log, :dropped]

A server notifications/message was dropped: the connection's :log_rate_limit (50 per second, bursts of 100 by default) was spent. Entries within the budget reach subscribers and handle_log/3 with every string sanitized and the level clamped to the RFC 5424 set.

Measurements: :count (always 1).

Metadata: the common keys.

[:conduit_mcp, :ext_tasks, :cleanup]

From the ConduitMcp.Ext.Tasks janitor (a ConduitMcp.Session.Janitor, started only when the experimental tasks extension is enabled) evicting expired tasks.

Measurements: :removed (integer) - Tasks evicted.

Metadata: :store (module) - The ConduitMcp.Ext.Tasks.Store swept.

[:conduit_mcp, :client, :cache, :refresh, :start | :stop]

The connection refreshed one cached list (ConduitMcp.Client.Cache): on demand, or after a list_changed notification. The fetch itself runs in a task; these events bracket it.

Measurements: :start has :monotonic_time and :system_time; :stop has :duration (native units), :monotonic_time, :count (items kept), :pages and :rejected (tools dropped at intake); the counts are 0 when the refresh failed.

Metadata: the common keys, plus :kind (:tools:prompts
:resources:resource_templates) and :trigger (:demand

:list_changed). :stop adds :status (:ok | :error) and :error_kind.

[:conduit_mcp, :client, :tool, :rejected]

A tool a server listed was dropped at intake (see ConduitMcp.Client, "Listing and the cache").

Measurements: :count (always 1).

Metadata: the common keys, plus :tool_name (String.t | nil - the name with every character outside [A-Za-z0-9_-] replaced by _; server-supplied, never a metric tag) and :reason (:invalid_name | :invalid_definition | :duplicate_name | :invalid_x_mcp_header, or :collision when a ConduitMcp.Client.Hub or ConduitMcp.Client.Adapters toolset left the tool out because its exposed name collided).

[:conduit_mcp, :client, :tool, :definition_changed]

A refreshed tool list changed the definition (the ConduitMcp.Client.Fingerprint) of a tool the client already knew.

Measurements: :count (always 1).

Metadata: the common keys, plus :tool_name (String.t, sanitized; server-supplied, never a metric tag).

[:conduit_mcp, :client, :policy, :decision]

ConduitMcp.Client.Pipeline decided a governed tool call, resource read or prompt get (ConduitMcp.Client.Policy), after any approval.

Measurements: :count (always 1).

Metadata: the common keys, plus:

  • :target (:tool | :resource | :prompt)

  • :tool_name (String.t, sanitized; tools only; never a metric tag)
  • :decision (:allow | :ask | :deny) - the policy's decision

  • :source (:deny | :ask | :allow | :pin | :default)

  • :reason (atom) - a ConduitMcp.Client.Policy.Decision reason, or :no_policy when a {:named, name} policy is not stored
  • :rule_id (term) - the deciding rule's id, nil for the pin and the default (operator-chosen, so not a metric tag)
  • :approval (:grant | :once | :always | :denied | :halted | :failed | nil) - how an :ask was settled: a live grant, the approver's answer, or :failed (a raise, exit, timeout or bad return); nil when the decision was not :ask

  • :principal (term) - the call's :principal, only when given

[:conduit_mcp, :client, :hook, :stop]

A ConduitMcp.Client.Hooks hook returned within its contract.

Measurements: :duration (native units) and :monotonic_time.

Metadata: the common keys, plus :hook_id (the hook's :id), :kind (the hook event: :pre_tool_call | :post_tool_call | :tool_call_failed | :on_tools_listed | :on_resource_read | :on_prompt_get), :status (:cont | :rewrite | :deny | :halt | :recover) and, for tool hooks, :tool_name (sanitized).

[:conduit_mcp, :client, :hook, :exception]

A hook raised, threw, exited, timed out or returned outside its contract; the call failed closed, the result was withheld, or (with on_error: :ignore) the hook was skipped. What the hook raised is never included.

Measurements: as for hook.stop.

Metadata: as for hook.stop, with :reason (:raise | :exit | :timeout | :bad_return) instead of :status.

[:conduit_mcp, :client, :input_request, :start | :stop | :exception]

One span per server-to-client input request the client's ConduitMcp.Client.Handler answers (sampling/createMessage, elicitation/create, roots/list), in either era: in the calling process for a modern multi-round-trip input request, in the connection's handler task for a legacy server request. The span covers the safety checks and the handler callback; neither the request's params nor the answer is included.

Measurements: as for request.

Metadata: the common keys, plus :method (one of the three methods above, or nil for any other). :stop adds :status (:ok when an answer is sent, :error when an error is); :exception (the handler raised, threw or exited) adds :kind.

[:conduit_mcp, :client, :transport, :mailbox]

The connection's mailbox holds more than 1 000 messages: it is not keeping up with what the server sends (a stdio port or an HTTP stream has no back-pressure). Sampled once every 64 transport messages, so a backlog emits it repeatedly until it drains.

Measurements: :message_queue_len (integer).

Metadata: the common keys.

Hub and Adapter Events

Emitted by ConduitMcp.Client.Hub and ConduitMcp.Client.Adapters. The hub events carry :hub (the hub's :name) instead of the client keys above; adapter.strict_fallback carries :client (the hub's or client's name).

[:conduit_mcp, :client, :hub, :server_failed]

A hub server's client exited (it crashed, or its supervisor gave up). Emitted in the hub's manager process, which starts it again after a jittered backoff.

Measurements: :count (always 1) and :delay (milliseconds until the restart).

Metadata: :hub, :server_id (String.t), :attempt (integer - consecutive failures) and :reason (atom - a closed-set tag of the exit reason, :other when it is not an atom or tagged tuple).

[:conduit_mcp, :client, :hub, :server_restarted]

The hub started a failed server's client again.

Measurements: :count (always 1).

Metadata: :hub, :server_id and :attempt.

[:conduit_mcp, :client, :hub, :collision]

Two or more tools mangled to the same exposed name, so none of them is offered (HB-1). Emitted in the caller that built the toolset, once per distinct tool list (each tool also gets a tool.rejected with reason :collision).

Measurements: :count (the number of colliding tools).

Metadata: :hub, :exposed_name (String.t) and :colliders ([{server_id, tool_name}], tool names sanitized; never a metric tag).

[:conduit_mcp, :client, :adapter, :strict_fallback]

ConduitMcp.Client.Adapters.tools/3 sent an OpenAI tool with "strict": false because its schema cannot be converted to strict mode (strict: :auto).

Measurements: :count (always 1).

Metadata: :client, :provider (:openai), :api (:chat | :responses), :tool_name (the exposed name; never a metric tag) and :reason (:unsupported_keyword, or the strict-mode limit exceeded: :max_properties | :max_depth | :max_chars | :max_enum_values | :max_enum_chars).

[:conduit_mcp, :client, :oauth, op, :start | :stop | :exception]

The client's OAuth work (ConduitMcp.Client.OAuth), one span per operation, op one of:

  • :discovery - Protected Resource Metadata and authorization-server metadata fetched and validated;
  • :registration - the client identity chosen (pre-registered, CIMD, or a dynamic registration request);
  • :token_exchange - an authorization code redeemed, or a client-credentials token requested;
  • :refresh - a refresh-token grant;
  • :step_up - a 403 insufficient_scope handled (emitted in the connection process).

The other spans run in the process doing the work: an HTTP request process, the connection's authorization task, or the caller of ConduitMcp.Client.OAuth.

Measurements: as for request.

Metadata: never a token, code, state, verifier, secret, URL or issuer:

  • :client (term) - the client's :name, nil for a standalone ConduitMcp.Client.OAuth.Config
  • :server_id (String.t | nil) - the client's :server_id

  • :principal (term) - the configured :principal (host-supplied; nil when unused)

:token_exchange adds :grant (:authorization_code | :client_credentials). :stop adds :status (:ok | :error) and :reason (a ConduitMcp.Client.OAuth.Error reason atom, or nil); :registration's :stop adds :method (:pre_registered | :cimd | :dcr | nil). :exception adds :kind.

[:conduit_mcp, :client, :oauth, :authorization_required]

The user must authorize: the client is about to call on_authorization_required (emitted in the connection process, once per episode, and for each step-up).

Measurements: :count (always 1).

Metadata: :client, :server_id, :principal as above, and :reason (:unauthorized | :step_up).

Key reference

Every event with the exact keys it carries. "Always" keys are in every emission; "Sometimes" keys only where the event's section says. No emission carries a key outside these two columns: a test pins each emitted key set to this table, which is generated from the same list as events/0.

EventMeasurementsMetadata (always)Metadata (sometimes)
[:conduit_mcp, :request, :stop]:duration:method, :server_module, :status, :era, :protocol_version, :traceparent, :tracestate, :baggage-
[:conduit_mcp, :tool, :execute]:duration:tool_name, :server_module, :status-
[:conduit_mcp, :resource, :read]:duration:uri, :server_module, :status-
[:conduit_mcp, :prompt, :get]:duration:prompt_name, :server_module, :status-
[:conduit_mcp, :rate_limit, :check]:duration:key, :status:count, :retry_after
[:conduit_mcp, :message_rate_limit, :check]:duration:key, :status, :method:count, :retry_after
[:conduit_mcp, :auth, :verify]:duration:strategy, :status:reason
[:conduit_mcp, :request, :cancelled]:count:request_id, :scope, :reason-
[:conduit_mcp, :cancellation, :cleanup]:removed, :in_flight_removed--
[:conduit_mcp, :cancellation, :janitor]:removed:store-
[:conduit_mcp, :session, :cleanup]:removed:store-
[:conduit_mcp, :tasks, :cleanup]:removed--
[:conduit_mcp, :mrtr, :input_required]:count:method, :server_module, :round-
[:conduit_mcp, :client, :request, :start]:monotonic_time, :system_time:client, :server_id, :transport, :era, :method-
[:conduit_mcp, :client, :request, :stop]:duration, :monotonic_time:client, :server_id, :transport, :era, :method, :status, :error_kind, :request_id-
[:conduit_mcp, :client, :request, :exception]:duration, :monotonic_time:client, :server_id, :transport, :era, :method, :kind-
[:conduit_mcp, :client, :tool_call, :start]:monotonic_time, :system_time:client, :server_id, :transport, :era, :tool_name:arguments
[:conduit_mcp, :client, :tool_call, :stop]:duration, :monotonic_time, :rounds:client, :server_id, :transport, :era, :tool_name, :status, :error_kind, :is_error:result
[:conduit_mcp, :client, :tool_call, :exception]:duration, :monotonic_time:client, :server_id, :transport, :era, :tool_name, :kind-
[:conduit_mcp, :client, :input_request, :start]:monotonic_time, :system_time:client, :server_id, :transport, :era, :method-
[:conduit_mcp, :client, :input_request, :stop]:duration, :monotonic_time:client, :server_id, :transport, :era, :method, :status-
[:conduit_mcp, :client, :input_request, :exception]:duration, :monotonic_time:client, :server_id, :transport, :era, :method, :kind-
[:conduit_mcp, :client, :connection, :transition]:system_time:client, :server_id, :transport, :era, :from, :to, :reason, :attempt-
[:conduit_mcp, :client, :request, :cancel]:count:client, :server_id, :transport, :era, :method, :request_id, :reason-
[:conduit_mcp, :client, :response, :dropped]:count:client, :server_id, :transport, :era, :request_id-
[:conduit_mcp, :client, :notification]:count:client, :server_id, :transport, :era, :method-
[:conduit_mcp, :client, :log, :dropped]:count:client, :server_id, :transport, :era-
[:conduit_mcp, :client, :transport, :mailbox]:message_queue_len:client, :server_id, :transport, :era-
[:conduit_mcp, :subscription, :start]:system_time, :resource_subscriptions:filters, :server_module-
[:conduit_mcp, :subscription, :stop]:duration:filters, :server_module, :reason-
[:conduit_mcp, :subscription, :drop]:message_queue_len:topic-
[:conduit_mcp, :ext_tasks, :cleanup]:removed:store-
[:conduit_mcp, :client, :cache, :refresh, :start]:monotonic_time, :system_time:client, :server_id, :transport, :era, :kind, :trigger-
[:conduit_mcp, :client, :cache, :refresh, :stop]:duration, :monotonic_time, :count, :pages, :rejected:client, :server_id, :transport, :era, :kind, :trigger, :status, :error_kind-
[:conduit_mcp, :client, :tool, :rejected]:count:client, :server_id, :transport, :era, :tool_name, :reason-
[:conduit_mcp, :client, :tool, :definition_changed]:count:client, :server_id, :transport, :era, :tool_name-
[:conduit_mcp, :client, :policy, :decision]:count:client, :server_id, :transport, :era, :target, :decision, :source, :reason, :rule_id, :approval:tool_name, :principal
[:conduit_mcp, :client, :hook, :stop]:duration, :monotonic_time:client, :server_id, :transport, :era, :hook_id, :kind, :status:tool_name
[:conduit_mcp, :client, :hook, :exception]:duration, :monotonic_time:client, :server_id, :transport, :era, :hook_id, :kind, :reason:tool_name
[:conduit_mcp, :client, :hub, :server_failed]:count, :delay:hub, :server_id, :attempt, :reason-
[:conduit_mcp, :client, :hub, :server_restarted]:count:hub, :server_id, :attempt-
[:conduit_mcp, :client, :hub, :collision]:count:hub, :exposed_name, :colliders-
[:conduit_mcp, :client, :adapter, :strict_fallback]:count:client, :provider, :api, :tool_name, :reason-
[:conduit_mcp, :client, :oauth, :discovery, :start]:monotonic_time, :system_time:client, :server_id, :principal:grant
[:conduit_mcp, :client, :oauth, :discovery, :stop]:duration, :monotonic_time:client, :server_id, :principal, :status, :reason:grant, :method
[:conduit_mcp, :client, :oauth, :discovery, :exception]:duration, :monotonic_time:client, :server_id, :principal, :kind:grant
[:conduit_mcp, :client, :oauth, :registration, :start]:monotonic_time, :system_time:client, :server_id, :principal:grant
[:conduit_mcp, :client, :oauth, :registration, :stop]:duration, :monotonic_time:client, :server_id, :principal, :status, :reason:grant, :method
[:conduit_mcp, :client, :oauth, :registration, :exception]:duration, :monotonic_time:client, :server_id, :principal, :kind:grant
[:conduit_mcp, :client, :oauth, :token_exchange, :start]:monotonic_time, :system_time:client, :server_id, :principal:grant
[:conduit_mcp, :client, :oauth, :token_exchange, :stop]:duration, :monotonic_time:client, :server_id, :principal, :status, :reason:grant, :method
[:conduit_mcp, :client, :oauth, :token_exchange, :exception]:duration, :monotonic_time:client, :server_id, :principal, :kind:grant
[:conduit_mcp, :client, :oauth, :refresh, :start]:monotonic_time, :system_time:client, :server_id, :principal:grant
[:conduit_mcp, :client, :oauth, :refresh, :stop]:duration, :monotonic_time:client, :server_id, :principal, :status, :reason:grant, :method
[:conduit_mcp, :client, :oauth, :refresh, :exception]:duration, :monotonic_time:client, :server_id, :principal, :kind:grant
[:conduit_mcp, :client, :oauth, :step_up, :start]:monotonic_time, :system_time:client, :server_id, :principal:grant
[:conduit_mcp, :client, :oauth, :step_up, :stop]:duration, :monotonic_time:client, :server_id, :principal, :status, :reason:grant, :method
[:conduit_mcp, :client, :oauth, :step_up, :exception]:duration, :monotonic_time:client, :server_id, :principal, :kind:grant
[:conduit_mcp, :client, :oauth, :authorization_required]:count:client, :server_id, :principal, :reason-

Common Use Cases

Logging Request Activity

defmodule MyApp.MCPTelemetry do
  require Logger

  def attach_handlers do
    events = [
      [:conduit_mcp, :request, :stop],
      [:conduit_mcp, :tool, :execute],
      [:conduit_mcp, :resource, :read],
      [:conduit_mcp, :prompt, :get],
      [:conduit_mcp, :rate_limit, :check],
      [:conduit_mcp, :message_rate_limit, :check],
      [:conduit_mcp, :auth, :verify]
    ]

    :telemetry.attach_many(
      "my-app-mcp-logger",
      events,
      &handle_event/4,
      nil
    )
  end

  def handle_event([:conduit_mcp, :request, :stop], measurements, metadata, _config) do
    Logger.info("MCP request",
      method: metadata.method,
      status: metadata.status,
      duration_ms: convert_duration(measurements.duration)
    )
  end

  def handle_event([:conduit_mcp, :tool, :execute], measurements, metadata, _config) do
    Logger.info("Tool executed",
      tool: metadata.tool_name,
      status: metadata.status,
      duration_ms: convert_duration(measurements.duration)
    )
  end

  def handle_event([:conduit_mcp, :resource, :read], measurements, metadata, _config) do
    Logger.info("Resource read",
      uri: metadata.uri,
      status: metadata.status,
      duration_ms: convert_duration(measurements.duration)
    )
  end

  def handle_event([:conduit_mcp, :prompt, :get], measurements, metadata, _config) do
    Logger.info("Prompt retrieved",
      prompt: metadata.prompt_name,
      status: metadata.status,
      duration_ms: convert_duration(measurements.duration)
    )
  end

  def handle_event([:conduit_mcp, :rate_limit, :check], measurements, metadata, _config) do
    Logger.info("Rate limit check",
      key: metadata.key,
      status: metadata.status,
      duration_ms: convert_duration(measurements.duration)
    )
  end

  def handle_event([:conduit_mcp, :message_rate_limit, :check], measurements, metadata, _config) do
    Logger.info("Message rate limit check",
      key: metadata.key,
      method: metadata.method,
      status: metadata.status,
      duration_ms: convert_duration(measurements.duration)
    )
  end

  def handle_event([:conduit_mcp, :auth, :verify], measurements, metadata, _config) do
    Logger.info("Auth verification",
      strategy: metadata.strategy,
      status: metadata.status,
      duration_ms: convert_duration(measurements.duration)
    )
  end

  defp convert_duration(native) do
    System.convert_time_unit(native, :native, :millisecond)
  end
end

Metrics Collection

defmodule MyApp.MCPMetrics do
  use Supervisor
  import Telemetry.Metrics

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def init(_opts) do
    children = [
      {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  defp metrics do
    [
      # Total requests by method and status
      counter("conduit_mcp.request.stop.count",
        tags: [:method, :status],
        description: "Total MCP requests"
      ),

      # Request duration distribution
      distribution("conduit_mcp.request.stop.duration",
        unit: {:native, :millisecond},
        tags: [:method],
        description: "MCP request duration"
      ),

      # Tool execution count
      counter("conduit_mcp.tool.execute.count",
        tags: [:tool_name, :status],
        description: "Tool execution count"
      ),

      # Tool duration summary
      summary("conduit_mcp.tool.execute.duration",
        unit: {:native, :millisecond},
        tags: [:tool_name],
        description: "Tool execution duration"
      ),

      # Resource read count
      counter("conduit_mcp.resource.read.count",
        tags: [:status],
        description: "Resource read operations"
      ),

      # Resource read duration
      distribution("conduit_mcp.resource.read.duration",
        unit: {:native, :millisecond},
        description: "Resource read duration"
      ),

      # Prompt retrieval count
      counter("conduit_mcp.prompt.get.count",
        tags: [:prompt_name, :status],
        description: "Prompt retrieval count"
      ),

      # Prompt retrieval duration
      summary("conduit_mcp.prompt.get.duration",
        unit: {:native, :millisecond},
        tags: [:prompt_name],
        description: "Prompt retrieval duration"
      ),

      # Rate limit check count
      counter("conduit_mcp.rate_limit.check.count",
        tags: [:status],
        description: "Rate limit checks"
      ),

      # Rate limit check duration
      distribution("conduit_mcp.rate_limit.check.duration",
        unit: {:native, :millisecond},
        description: "Rate limit check duration"
      ),

      # Message rate limit check count
      counter("conduit_mcp.message_rate_limit.check.count",
        tags: [:status, :method],
        description: "Message rate limit checks"
      ),

      # Message rate limit check duration
      distribution("conduit_mcp.message_rate_limit.check.duration",
        unit: {:native, :millisecond},
        description: "Message rate limit check duration"
      ),

      # Auth verification count
      counter("conduit_mcp.auth.verify.count",
        tags: [:strategy, :status],
        description: "Authentication attempts"
      ),

      # Auth verification duration
      distribution("conduit_mcp.auth.verify.duration",
        unit: {:native, :millisecond},
        tags: [:strategy],
        description: "Authentication verification duration"
      )
    ]
  end
end

Performance Alerts

:telemetry.attach(
  "mcp-performance-alerts",
  [:conduit_mcp, :tool, :execute],
  fn _event, %{duration: duration}, %{tool_name: tool}, _config ->
    duration_ms = System.convert_time_unit(duration, :native, :millisecond)

    cond do
      duration_ms > 5000 ->
        Logger.error("Critical: Tool #{tool} took #{duration_ms}ms")

      duration_ms > 1000 ->
        Logger.warning("Warning: Tool #{tool} took #{duration_ms}ms")

      true ->
        :ok
    end
  end,
  nil
)

See Also

Summary

Functions

Attaches default logging handlers for all ConduitMCP telemetry events.

Detaches the default telemetry handlers.

Returns all telemetry event names emitted by ConduitMCP, in the order of the moduledoc's "Key reference" table.

Functions

attach_default_handlers()

@spec attach_default_handlers() :: :ok | {:error, :already_exists}

Attaches default logging handlers for all ConduitMCP telemetry events.

This is a convenience function for quick setup during development. The handlers will log events at the :debug level.

Example

# In your application.ex
def start(_type, _args) do
  ConduitMcp.Telemetry.attach_default_handlers()
  # ... rest of supervision tree
end

To detach later:

ConduitMcp.Telemetry.detach_default_handlers()

detach_default_handlers()

@spec detach_default_handlers() :: :ok | {:error, :not_found}

Detaches the default telemetry handlers.

Example

ConduitMcp.Telemetry.detach_default_handlers()

events()

@spec events() :: [[:conduit_mcp, ...]]

Returns all telemetry event names emitted by ConduitMCP, in the order of the moduledoc's "Key reference" table.

Example

iex> [:conduit_mcp, :client, :tool_call, :stop] in ConduitMcp.Telemetry.events()
true