ExRatatui emits events for every runtime transition, render cycle, transport handshake, and session lifecycle change. You attach handlers — for logging, metrics, or OpenTelemetry tracing — and get observability without forking the runtime or wrapping your own timers around mount/1.
What gets emitted
Two categories, all prefixed with :ex_ratatui. Span events wrap something with a duration — mount/1, a render, a handle_event/2 call. Each span fires three telemetry events: :start when it begins, :stop when it ends (measurements include :duration), :exception if it raises. Single events mark a point in time — a dropped frame, a disconnect — and fire once with no paired stop.
| Event | Kind | Fires on | Metadata |
|---|---|---|---|
[:ex_ratatui, :runtime, :init] | span | mount/1 | :mod, :transport |
[:ex_ratatui, :runtime, :event] | span | Terminal event → handle_event/2 | :mod, :transport, :event |
[:ex_ratatui, :runtime, :update] | span | Info message → handle_info/2 (subscriptions, async results, user sends) | :mod, :transport, :msg |
[:ex_ratatui, :render, :frame] | span | Frame build + draw | :mod, :transport, :widget_count (on :stop) |
[:ex_ratatui, :transport, :connect] | span | Transport handshake at server start | :mod, :transport |
[:ex_ratatui, :session, :lifecycle, :open] | single | Session-backed runtime adopts a session | :mod, :transport, :width, :height |
[:ex_ratatui, :session, :lifecycle, :close] | single | Session-backed runtime releases its session (fires once per session even when the transport's own teardown closes the ref defensively) | :mod, :transport, :reason |
[:ex_ratatui, :render, :dropped] | single | Frame skipped (draw error, future backpressure) | :mod, :transport, :reason |
[:ex_ratatui, :transport, :disconnect] | single | Server terminate/2 | :mod, :transport, :reason |
Every event carries :mod and :transport in its metadata, so the same handler can tag frames by app module or filter by transport without fishing for the data elsewhere.
The split between :runtime, :event and :runtime, :update isn't arbitrary. Terminal input goes through :event; everything else — subscriptions firing, async command results, plain send/2 into the server — goes through :update. When you're asking "is my app slow because of keyboard handling or because my tick interval is doing too much?", that's the first place to look. :render, :frame's :stop event also adds :widget_count to its metadata, which makes a Telemetry.Metrics summary like "p99 frame build by widget count" a one-liner.
Quick start: log every event
ExRatatui ships a default logger that attaches one handler for every event:
# typically in your Application.start/2 or an iex session
ExRatatui.Telemetry.attach_default_logger()Every :stop and every single event now logs at :debug. Pass level: :info (or any Logger level) to bump the verbosity, or events: to restrict which events log:
ExRatatui.Telemetry.attach_default_logger(
level: :info,
events: [[:ex_ratatui, :render, :frame, :stop], [:ex_ratatui, :render, :dropped]]
)detach_default_logger/0 reverses it.
Telemetry.Metrics
telemetry_metrics converts :telemetry events into summaries, counters, and distributions. Wire the output into a reporter and you have dashboards in minutes.
An example:
def metrics do
[
# Render timing — the one metric you actually want to graph.
Telemetry.Metrics.summary("ex_ratatui.render.frame.stop.duration",
unit: {:native, :millisecond},
tags: [:mod, :transport]
),
# Frame-build cost by scene size — spot "my dashboard grew 3x and now drops frames".
Telemetry.Metrics.distribution("ex_ratatui.render.frame.stop.widget_count",
tags: [:mod]
),
# Dropped frames — should be near zero. Page on it.
Telemetry.Metrics.counter("ex_ratatui.render.dropped",
tags: [:transport, :reason]
),
# Handler latency — if a :runtime.event.stop summary grows, your
# handle_event/2 is doing too much.
Telemetry.Metrics.summary("ex_ratatui.runtime.event.stop.duration",
unit: {:native, :millisecond},
tags: [:mod]
),
# Session churn — SSH clients connecting/disconnecting.
Telemetry.Metrics.counter("ex_ratatui.transport.disconnect",
tags: [:transport, :reason]
)
]
endOpenTelemetry
opentelemetry_telemetry gives you the primitives to turn each :telemetry span into an OTel span: start_telemetry_span/4 when the :start event fires, end_telemetry_span/2 when :stop fires. The telemetry_span_context reference that :telemetry.span/3 puts on every event is what lets the pair find each other across handler invocations, so you don't have to thread any state yourself.
Attach one handler covering the suffixes of every span you care about. Configure your exporter (Jaeger, Honeycomb, Tempo) via :opentelemetry_exporter, and every ExRatatui span now lands in your trace UI.
The payoff is end-to-end traces: an SSH TUI that calls into your Phoenix backend shows the render span next to the HTTP span next to the DB span, all in one timeline.
Where to go next
- Debugging —
Runtime.enable_trace/2gives you an in-memory event log scoped to one server; telemetry gives you the system-wide view. Both have their place. - Performance — once metrics surface a slow render, this guide covers what to do about it.
ExRatatui.Telemetry— module docs with the helper API.telemetry/telemetry_metrics/opentelemetry_telemetry— upstream docs.