Snodo.Instrumentation is a dependency-free, opt-in event sink. It keeps observability outside protocol semantics and lets an application bridge the same events to :telemetry, OpenTelemetry, Logger, a metrics process, or a test collector.

A sink implements one callback:

defmodule MyApp.MCPTelemetry do
  @behaviour Snodo.Instrumentation

  @impl true
  def handle_event(name, measurements, metadata, _options) do
    :telemetry.execute(name, measurements, metadata)
  end
end

Configure the server runtime, subscription hub, and Tasks runner explicitly:

sink = {MyApp.MCPTelemetry, []}

{:ok, hub} =
  Snodo.Subscription.Hub.start_link(
    name: MyApp.SubscriptionHub,
    instrumentation: sink
  )

runtime =
  MyServer.runtime(
    instrumentation: sink,
    subscription_source: Snodo.Subscription.Hub.source(MyApp.SubscriptionHub)
  )

{:ok, runner} =
  Snodo.Extensions.Tasks.Runner.start_link(
    store: store_ref,
    instrumentation: sink
  )

The callback is synchronous, matching :telemetry.execute/3; applications should keep it fast and hand expensive work to another process. Callback faults are caught so they cannot change the observed operation's result. No event includes request params, auth data, work input, access values, task results/errors, or input responses.

Durations use the VM's native monotonic time unit. Convert them with System.convert_time_unit(duration, :native, desired_unit).

Event catalog

EventMeasurementsMetadata
[:snodo, :server, :dispatch, :start]system_timemethod, request_id, transport
[:snodo, :server, :dispatch, :stop]durationstart metadata plus outcome; errors add error_code
[:snodo, :server, :dispatch, :exception]durationstart metadata plus kind and bounded reason_class
[:snodo, :subscription, :open]current subscriptionsrequest_id, transport, sorted filter_keys
[:snodo, :subscription, :publish]matched, delivered, buffered, dropped, total queuedevent_kind; extension events add extension_id
[:snodo, :subscription, :overflow]dropped, total queuedpublish metadata plus overflow policy
[:snodo, :subscription, :complete]current subscriptions, total queuednone
[:snodo, :subscription, :close]remaining subscriptionsclassified reason
[:snodo, :tasks, :runner, :job, :start]current jobs, system_timetask_id, revision, source
[:snodo, :tasks, :runner, :job, :stop]duration, remaining jobstask_id, job outcome, store_outcome, release_outcome
[:snodo, :tasks, :store, :transition]durationtask_id, expected_revision, event_kind, authority, outcome

The names and bounded metadata are framework API. A particular metrics backend, aggregation policy, sampling policy, and task-ID cardinality policy remain application concerns.

The opt-in Tasks contention and soak harness consumes the job and transition events directly. Its exact lifecycle invariants illustrate one way to build correctness evidence without coupling the framework to a metrics dependency or a machine-specific latency threshold.