Telemetry Events
This page is the canonical schema for both the OTEL telemetry exported by
ftui-runtime/telemetry and the local JSONL evidence sink used for E2E
capture and replay. It also documents semantic input events, which are the
high-level events the gesture recognizer produces.
Telemetry is opt-in. It requires the ftui-runtime/telemetry Cargo
feature plus OTEL_EXPORTER_OTLP_ENDPOINT. With the feature off, this code
and its dependencies are excluded from the build. See
/reference/feature-flags and
/reference/env-vars.
Goals
- Define a stable, explicit event schema for OTEL spans/events.
- Establish conservative default redaction of sensitive data.
- Enable consumers to build dashboards and alerts.
- Provide clear semantics for user-supplied fields.
Non-goals: performance-oriented micro-tracing, complete UI-state reconstruction from telemetry, real-time streaming without batching.
OTEL Event Categories
Runtime Phase Events
High-level spans for the Elm/Bubbletea runtime loop.
| Span Name | Description | Fields |
|---|---|---|
ftui.program.init | Model initialization | model_type, cmd_count |
ftui.program.update | Single update cycle | msg_type, duration_us, cmd_type |
ftui.program.view | View rendering | duration_us, widget_count |
ftui.program.subscriptions | Subscription management | active_count, started, stopped |
Render Pipeline Events
Spans for the render kernel (buffer, diff, presenter).
| Span Name | Description | Fields |
|---|---|---|
ftui.render.frame | Complete frame cycle | width, height, duration_us |
ftui.render.diff | Buffer diff computation | changes_count, rows_skipped, duration_us |
ftui.render.present | ANSI emission | bytes_written, runs_count, duration_us |
ftui.render.flush | Output flush | duration_us, sync_mode |
ftui.reflow.apply | Resize application outcome | width, height, debounce_ms, latency_ms, rate_hz |
ftui.reflow.placeholder | Resize placeholder shown | width, height, rate_hz |
Decision Events
Point-in-time events for auditable decisions.
| Event Name | Description | Fields |
|---|---|---|
ftui.decision.degradation | Degradation level change | degradation_level, reason_code, budget_remaining |
ftui.decision.fallback | Capability fallback | capability, fallback_to, reason_code |
ftui.decision.resize | Resize handling decision | strategy, debounce_active, coalesced, same_size, width, height, rate_hz |
ftui.decision.screen_mode | Screen mode selection | mode, ui_height, anchor |
Runtime Mode Contract Extensions
For the runtime-performance lane, degradation and fallback events are not complete unless they also explain the user-visible service mode:
ftui.decision.degradation— must carryruntime_mode,runtime_mode_before,runtime_mode_after,pressure_class,degradation_level,queue_depth,queue_capacity,queue_high_water,coalescing_state,coalesced_count,deferred_count,dropped_count,reason_code,strict_guarantees,degraded_behaviors,recovery_target,signal_surface, andwork_disposition. If it closes a degraded interval, it must also carryrecovery_completed,recovery_latency_ms, anddegraded_interval_ms.ftui.decision.fallback— must carryruntime_mode,rollback_required,operator_action, andreason_code. If the fallback changes active service quality, it must also carrypressure_class, any activedegradation_level, andwork_disposition.
Input Events
Spans for input processing (redacted by default).
| Span Name | Description | Fields |
|---|---|---|
ftui.input.event | Input event processing | event_type (no content!) |
ftui.input.macro | Macro playback | macro_id, event_count |
Field Schema
Common Fields (All Spans)
service.name string - From OTEL_SERVICE_NAME or "ftui-runtime"
service.version string - FrankenTUI version
telemetry.sdk string - "ftui-telemetry"
host.arch string - Target architecture
process.pid int - Process IDDuration Fields
All duration fields use microseconds (us) for precision:
duration_us u64 - Elapsed time in microsecondsDecision Evidence Fields
Decision events include structured evidence:
decision.rule string - Rule/heuristic applied
decision.inputs string - JSON-serialized input state (redacted)
decision.action string - Chosen action
decision.confidence f32 - Confidence score (0.0-1.0) if applicableRedaction Policy
Principles
- Conservative by default — err on the side of not emitting.
- No PII — never emit user input content, file paths, or secrets.
- Structural only — emit types and counts, not values.
- Opt-in detail — verbose fields require explicit configuration.
Never Emit (Hard Redaction)
| Category | Examples |
|---|---|
| User input content | Key characters, text buffer contents, passwords |
| File paths | Log files, config paths, temp files |
| Environment variables | Beyond OTEL_* and FTUI_* prefixes |
| Memory addresses | Pointer values, buffer addresses |
| Process arguments | Command-line arguments |
| User identifiers | Usernames, home directories |
Conditionally Emit (Soft Redaction)
These are omitted by default but can be enabled via FTUI_TELEMETRY_VERBOSE=true:
| Category | When Enabled |
|---|---|
| Widget types | Full widget type names |
| Message types | Model::Message enum variants |
| Command types | Cmd enum variants |
| Capability details | Full terminal capability report |
Always Emit (No Redaction)
| Category | Examples |
|---|---|
| Counts | Widget count, change count, event count |
| Durations | All timing measurements |
| Dimensions | Buffer width/height, UI height |
| Enum variants | Screen mode, degradation level |
| Boolean flags | Mouse enabled, sync available |
User-Supplied Field Handling
Custom Span Attributes
Applications may attach custom attributes via tracing:
tracing::info_span!("my_operation", custom.field = "value");Policy:
- Prefix requirement: custom fields MUST use a namespace prefix
(e.g.,
app.,custom.). - No automatic redaction: the application is responsible for not emitting sensitive data.
- Pass-through: custom fields are passed to the OTEL exporter unchanged.
Custom Events
tracing::info!(target: "app.audit", action = "user_action");Policy:
- Filtered by target: only targets matching
app.*orcustom.*are exported. - Rate limiting: custom events are subject to the same batching as built-in events.
- Documentation: applications should document their custom event schemas.
Schema Versioning
All telemetry includes a schema version:
ftui.schema_version string - Semantic version (e.g., "1.0.0")| Level | Pattern | Rule |
|---|---|---|
| Patch | 1.0.x | Additive only, no breaking changes |
| Minor | 1.x.0 | New fields, deprecated fields still emitted |
| Major | x.0.0 | Breaking changes, old fields may be removed |
Current version: 1.0.0 (initial stable schema).
Invariants
- Redaction completeness — no user input content escapes to telemetry.
- Schema stability — breaking changes require a major version bump.
- Duration precision — all durations use microseconds.
- Deterministic field set — same operation produces same field names.
- Bounded cardinality — enum-typed fields have known cardinality.
Failure Modes
| Scenario | Behavior |
|---|---|
| Serialization error | Log warning, omit event |
| Field value overflow | Saturate to max value |
| Unknown field type | Stringify with .to_string() |
| Custom field collision | Prefix with app. |
JSONL Evidence Sink
Deterministic decision logs are emitted via the local JSONL evidence sink. This is distinct from OTEL spans and is intended for E2E capture + replay.
Configuration (see crates/ftui-runtime/src/evidence_sink.rs):
use ftui_runtime::{EvidenceSinkConfig, EvidenceSinkDestination, ProgramConfig};
let config = ProgramConfig::default()
.with_evidence_sink(
EvidenceSinkConfig::default()
.with_enabled(true)
.with_destination(EvidenceSinkDestination::file("evidence.jsonl"))
.with_flush_on_write(true),
);Notes:
- Default is disabled.
- Destination:
StdoutorFile(path). - Flush-on-write defaults to true (recommended for tests).
- Resize decision JSONL requires
CoalescerConfig::with_logging(true). - BOCPD JSONL requires
BocpdConfig::with_logging(true).
Each line is a single JSON object; field sets are event-specific. The following sections enumerate them.
Event: diff_decision
Required fields:
run_id(string)event_idx(u64)strategy(Full|DirtyRows|FullRedraw)cost_full,cost_dirty,cost_redrawposterior_mean,posterior_variance,alpha,betadirty_rows,total_rows,total_cellsspan_count,span_coverage_pct,max_span_lenfallback_reason,scan_cost_estimatetile_used,tile_fallbacktile_w,tile_h,tile_size,tiles_x,tiles_ydirty_tiles,dirty_tile_count,dirty_cells,dirty_tile_ratio,dirty_cell_ratioscanned_tiles,skipped_tiles,skipped_tile_counttile_scan_cells_estimate,sat_build_cost_estbayesian_enabled,dirty_rows_enabled
Runtime defaults (RuntimeDiffConfig): bayesian_enabled = true,
dirty_rows_enabled = true, reset_on_resize = true,
reset_on_invalidation = true.
Example:
{"event":"diff_decision","run_id":"diff-4242","event_idx":12,"strategy":"DirtyRows","cost_full":4800.0,"cost_dirty":1200.0,"cost_redraw":0.0,"posterior_mean":0.036,"posterior_variance":0.00034,"alpha":3.5,"beta":92.5,"dirty_rows":10,"total_rows":40,"total_cells":4800,"span_count":6,"span_coverage_pct":12.5,"max_span_len":18,"fallback_reason":"none","scan_cost_estimate":600,"tile_used":true,"tile_fallback":"none","tile_w":16,"tile_h":16,"tile_size":256,"tiles_x":5,"tiles_y":3,"dirty_tiles":2,"dirty_tile_count":2,"dirty_cells":40,"dirty_tile_ratio":0.133,"dirty_cell_ratio":0.008,"scanned_tiles":2,"skipped_tiles":13,"skipped_tile_count":13,"tile_scan_cells_estimate":512,"sat_build_cost_est":4800,"bayesian_enabled":true,"dirty_rows_enabled":true}Notes:
span_coverage_pctis a 0–100 percentage of total cells covered by spans (including full rows).fallback_reasonvalues:none,no_spans,no_dirty_rows,span_overflow,full_strategy,full_redraw.
Event: config (resize coalescer)
Required fields:
steady_delay_ms,burst_delay_ms,hard_deadline_msburst_enter_rate,burst_exit_ratecooldown_frames,rate_window_sizelogging_enabled
Event: decision (resize coalescer)
Required fields:
idx,elapsed_ms,dt_ms,event_rateregime(steady|burst)action(apply|apply_forced|apply_immediate|coalesce|skip_same_size)pending_w,pending_h,applied_w,applied_htime_since_render_ms,coalesce_ms,forced
Event: decision_evidence (resize coalescer)
Required fields:
log_bayes_factor(float or"inf")regime_contribution,timing_contribution,rate_contributionexplanation
Event: bocpd
Schema versioned (schema_version: "bocpd-v1"). Required fields:
p_burst,log_bf,obs_ms,regimell_steady,ll_burstrunlen_mean,runlen_var,runlen_mode,runlen_p95,runlen_taildelay_ms(nullable),forced_deadline(nullable)n_obs
Example:
{"schema_version":"bocpd-v1","event":"bocpd","p_burst":0.7321,"log_bf":1.204,"obs_ms":18.0,"regime":"burst","ll_steady":0.001234,"ll_burst":0.056789,"runlen_mean":12.4,"runlen_var":9.1,"runlen_mode":9,"runlen_p95":21,"runlen_tail":0.042,"delay_ms":40,"forced_deadline":false,"n_obs":64}Event: allocation_budget_config
Required fields: alpha, mu_0, sigma_sq, cusum_k, cusum_h, lambda,
window_size.
Event: allocation_budget_evidence
Required fields: frame, x, residual, cusum_plus, cusum_minus,
e_value, alert (bool).
Event: mode_transition
For runtime-performance tracking. Required fields:
run_id,event_idxruntime_moderuntime_mode_before/runtime_mode_after— one ofhealthy,stressed,degraded,recoveredpressure_class— one ofsteady_state,input_backpressure,mixed_workload,shutdown_pressure,capability_fallbackdegradation_levelqueue_depth,queue_capacity,queue_high_watercoalescing_state,coalesced_count,deferred_count,dropped_countreason_code,strict_guarantees,degraded_behaviors,recovery_target,signal_surface,work_disposition
When mode_transition closes a degraded interval, it must also include:
recovery_completed,recovery_latency_ms,degraded_interval_ms
Event: recovery_complete
Required fields:
- all fields required by
mode_transitionwhen it closes a degraded interval pending_work_drained
Semantic Input Events
Separately from telemetry, ftui-core also emits semantic input events —
high-level gestures derived from raw terminal input. Your Model::update
receives both the raw Event stream and the SemanticEvent stream; you
choose which to consume per widget.
Architecture
Terminal Input
│
▼
Event (ftui-core) ─ Key, Mouse, Resize, Focus, Paste, Tick
│
▼
GestureRecognizer ─ Click detection, drag tracking, chord sequences
│
▼
SemanticEvent ─ Click, DoubleClick, DragStart, DragEnd, Chord, …
│
▼
Model.update() ─ receives both streamsSemanticEvent variants
pub enum SemanticEvent {
// Mouse gestures
Click { pos: Position, button: MouseButton },
DoubleClick { pos: Position, button: MouseButton },
TripleClick { pos: Position, button: MouseButton },
LongPress { pos: Position, duration: Duration },
// Drag gestures
DragStart { pos: Position, button: MouseButton },
DragMove { start: Position, current: Position, delta: (i16, i16) },
DragEnd { start: Position, end: Position },
DragCancel,
// Keyboard gestures
Chord { sequence: Vec<ChordKey> },
// Touch-like gestures
Swipe { direction: SwipeDirection, distance: u16, velocity: f32 },
}When to Use Which
| Scenario | Raw | Semantic |
|---|---|---|
| Text input field | ✓ | |
| Button click | ✓ | |
| Double-click to edit | ✓ | |
| Drag to reorder items | ✓ | |
| Scroll wheel handling | ✓ | |
| Key repeat for movement | ✓ | |
| Long-press context menu | ✓ | |
| Vim-like key sequences | ✓ | |
| Custom gesture detection | ✓ | |
| Game-like input | ✓ |
Gesture Config Defaults
GestureConfig {
multi_click_timeout: Duration::from_millis(300),
long_press_threshold: Duration::from_millis(500),
drag_threshold: 3, // cells
chord_timeout: Duration::from_millis(1000),
swipe_velocity_threshold: 50.0, // cells/sec
click_tolerance: 1, // cells
}Gesture Invariants
- Drag sequences are well-formed. Every
DragStartis followed by zero or moreDragMoveevents, ending with exactly oneDragEndorDragCancel. - Click and Drag are mutually exclusive. A mouse-down/mouse-up sequence produces either click events OR drag events, never both.
- Click multiplicity is monotonic. Within a multi-click window, you see
Click→DoubleClick→TripleClickin order. - Chords are non-empty. A
Chordevent always contains at least two keys. - Focus loss cancels drags. Losing window focus emits
DragCancelif a drag was in progress.
Failure Modes
| Failure | Cause | Result |
|---|---|---|
| Chord timeout | Keys pressed too slowly | No chord emitted; raw keys pass through |
| Focus loss mid-drag | Window deactivated | DragCancel emitted |
| Escape mid-drag | User pressed Escape | DragCancel emitted |
| Click outside threshold | Movement beyond click_tolerance | Resets to single click |
See Also
How to wire up OTLP export and trace-parent propagation.
Runtime telemetryThe JSONL ledger of runtime decisions.
Evidence sinkThe GestureRecognizer state machine that produces semantic events.
Raw event types and the DoS-hardened input parser.
Events and inputOTEL_* and FTUI_TELEMETRY_VERBOSE configuration.