Notre avis
Ce guide présente les bonnes pratiques pour implémenter une journalisation structurée NDJSON dans une application Rust TUI, en mettant l'accent sur des événements larges, un riche contexte et un coût nul lorsque désactivé.
Points forts
- Encourage des événements riches en contexte pour faciliter le diagnostic après incident.
- Préconise une identification claire des événements avec les champs event et kind.
- Inclut des recommandations précises sur le timing et les métriques de performance.
- Assure un impact nul sur les performances quand la journalisation est désactivée.
Limites
- Spécifique à l'architecture de Grove, difficile à réutiliser telle quelle dans d'autres projets.
- Nécessite une discipline d'équipe pour maintenir la cohérence des événements.
- Ne couvre pas l'analyse ni la visualisation des logs.
Utilisez cette approche lorsque vous concevez un système de journalisation structurée pour le débogage post-mortem d'applications TUI ou à forte réactivité.
Ne l'utilisez pas si vous avez besoin d'une journalisation en temps réel avec des alertes, ou si votre application n'a pas de cycle événementiel clair.
Analyse de sécurité
SûrThe skill provides documentation about logging best practices in a Rust TUI application. It does not instruct or involve any execution of commands, external data access, or system changes. All allowed tools are read-only (Read, Grep, Glob). No risky operations are described.
Aucun point d'attention détecté
Exemples
How do I add a new debug record event for a file watcher update in Grove? I need to log the changed path, timestamp, and duration.Review the logging code in src/event_log.rs and src/tui.rs to ensure all events follow the wide event principle and include proper timing fields.I'm adding a new preview polling feature. Show me how to add NDJSON logging with capture_ms and output_bytes fields using LogEvent.name: logging-best-practices description: > Debug record logging best practices for Grove's Rust TUI. Covers how to add new events to the NDJSON debug record system, wide event design, frame-level data capture, and when to log what. Use when adding new log events, reviewing logging code, or designing instrumentation for new features. Trigger phrases: "how to log", "add logging", "add event", "debug record", "event log", "instrumentation", "telemetry". allowed-tools: Read, Grep, Glob
Grove Logging Best Practices
Architecture Overview
Grove uses a structured NDJSON event log toggled by --debug-record. When
enabled, every frame render, state change, input, tmux command, and polling
event is written to .grove/debug-record-{app_start_ts}-{pid}.jsonl. When
disabled, a NullEventLogger discards all events at zero cost.
Key files:
src/event_log.rs--Eventstruct,EventLoggertrait,FileEventLogger,NullEventLoggersrc/tui.rs-- all.event_log.log(...)call sites,log_frame_render()src/main.rs----debug-recordflag parsing, path generation
Core Principles
1. Wide Events Per Boundary (CRITICAL)
Each log event should be a single, context-rich JSON object capturing everything relevant to that moment. Don't scatter multiple narrow log lines across a code path. Instead, build up context and emit one event at the boundary.
Good: one frame/rendered event with seq, hash, mode, focus, degradation,
dimensions, input queue state, and the full frame buffer.
Bad: separate events for "frame started", "frame mode is X", "frame focus is Y", "frame done".
2. Event Identity: event + kind (CRITICAL)
Every event has two classification fields:
event: the category (e.g.,frame,tick,input,tmux_cmd,error)kind: the specific occurrence (e.g.,rendered,scheduled,interactive_key_received)
This pair is the event's identity. Choose names that read naturally as
event/kind: frame/rendered, tick/scheduled, input/interactive_forwarded.
Use LogEvent::new("event", "kind") to create events. The timestamp is
captured automatically.
3. Rich Data Fields (CRITICAL)
Include high-cardinality fields (session names, frame hashes, input sequence numbers) and high-dimensionality (many fields per event). The debug record exists for post-hoc diagnosis of issues you haven't anticipated yet.
Add data with .with_data(key, value) or .with_data_fields(vec![...]):
self.event_log.log(
LogEvent::new("preview_poll", "capture_completed")
.with_data("session", Value::from(session.as_str()))
.with_data("capture_ms", Value::from(capture_ms))
.with_data("changed", Value::from(changed))
.with_data("output_bytes", Value::from(output_bytes))
.with_data("total_ms", Value::from(total_ms)),
);
4. Timing at Boundaries (CRITICAL)
Measure and log durations for operations that touch external systems or do
significant work. Use Instant::now() before and after, then log the delta
as _ms suffixed fields.
Common patterns:
capture_ms/total_mson preview capturestmux_send_mson input forwardingduration_mson tmux command completiondraw_ms/view_ms/frame_log_mson frame renderingupdate_mson message handlinginput_to_preview_ms/tmux_to_preview_msfor end-to-end input latency
5. Zero Cost When Disabled (HIGH)
All logging goes through self.event_log.log() which dispatches to either
FileEventLogger (writes to disk) or NullEventLogger (no-op). This means:
- Don't gate event construction behind
if self.debug_record_enabledchecks. TheNullEventLoggerhandles this. - Don't do expensive computation solely for logging. If you need to compute something expensive (like hashing frame lines), and it's only useful for logging, consider whether it should be part of the normal frame path.
- Frame line extraction in
log_frame_render()is an exception: it walks the terminal buffer and is only called when debug recording is active (guarded bydebug_record_start_ts.is_some()).
6. Sequence Numbers for Correlation (HIGH)
Use monotonic sequence numbers to correlate events across the pipeline.
Input events use seq (from self.input_seq_counter) to trace a keystroke
from receipt through action selection, tmux forwarding, and preview update.
Frames use seq (from self.frame_render_seq) for ordering.
When adding a new pipeline, assign a counter and propagate seq through
all related events so they can be joined during analysis.
7. Consistent Field Names (HIGH)
Reuse existing field names when logging the same concept:
| Concept | Field name | Type |
|---|---|---|
| Tmux session | session | string |
| Workspace name | workspace | string |
| Input sequence | seq | u64 |
| Duration | *_ms suffix | u64 |
| Success/failure | ok | bool |
| Error message | error or message | string |
| Queue state | pending_depth | usize |
| Queue age | oldest_pending_age_ms | u64 |
| Boolean state | changed, repeat, cursor_visible | bool |
Event Categories
Frame events (frame/*)
Logged per frame render. Two events per frame:
-
frame/rendered-- full frame state (only when--debug-recordis active):seq,frame_lines,frame_hash,width,height,line_count,non_empty_line_count,degradation,mode,focus,selected_workspace,interactive_session,sidebar_width_pct,preview_offset,preview_auto_scroll,output_changing,pending_input_depth,oldest_pending_input_seq,oldest_pending_input_age_ms,app_start_ts -
frame/timing-- performance metrics (always logged):draw_ms,view_ms,frame_log_ms,degradation,pending_depth
Tick events (tick/*)
Polling lifecycle: scheduled, retained, processed, skipped,
interactive_debounce_scheduled. Always include pending_depth and
oldest_pending_age_ms on scheduling events.
Input events (input/*)
Full input pipeline: interactive_key_received, interactive_action_selected,
interactive_forwarded, interactive_input_to_preview,
interactive_inputs_coalesced. Always include seq and session.
Tmux events (tmux_cmd/*)
Command lifecycle: execute (with command string), completed (with
command, duration_ms, ok, and error if failed).
State/mode events
Low-frequency state transitions: state_change/selection_changed,
state_change/focus_changed, mode_change/mode_changed,
mode_change/interactive_entered, mode_change/interactive_exited.
Error events (error/*)
Failures that need diagnosis: error/tmux_error. Include enough context
to reproduce (command, session, error message).
Adding a New Event
-
Choose
eventcategory andkindname. Check existing events in this document andsrc/tui.rsto avoid duplicating a category. -
Build the event at the boundary (completion of the operation, not the start):
self.event_log.log(
LogEvent::new("my_category", "my_kind")
.with_data("relevant_field", Value::from(value))
.with_data("duration_ms", Value::from(elapsed_ms)),
);
-
Include timing if the operation is non-trivial. Capture
Instant::now()before the operation and compute the delta after. -
Include queue/pipeline state if the event is part of a flow (
pending_depth,seq). -
Update the event type reference table in
.agents/skills/grove-debug/SKILL.mdso the debug analyzer skill knows about the new event.
Anti-Patterns
-
Narrow events: Don't emit "started" / "finished" pairs when a single completion event with
duration_mssuffices. Exception: operations that may never complete (liketmux_cmd/executepaired withcompleted). -
Missing context: Don't log an error without the operation that caused it. Include the command string, session name, and relevant state.
-
Unstructured strings: Never
eprintln!()for diagnostic info. Useself.event_log.log()with structured data. The onlyeprintln!in the codebase is for the debug record path at startup. -
Logging in hot loops without gating: Frame line extraction is expensive. If you add similarly expensive logging, guard it behind
self.debug_record_start_ts.is_some()likelog_frame_render()does. -
Inconsistent field names: Don't use
elapsedwhen the convention isduration_ms. Don't usetarget_sessionwhen the convention issession. -
Missing sequence numbers: If the event is part of an input or frame pipeline, include
seqso events can be correlated during analysis.
Frame Data: What and Why
The frame/rendered event captures the full terminal buffer because:
- Visual bugs (flicker, blank frames, layout corruption) can only be diagnosed by seeing exactly what was rendered
- Frame hash enables change detection without comparing full content
non_empty_line_countcatches blank frame regressionsdegradationtracks frame budget healthpending_input_depth+oldest_pending_input_age_msreveal input queue pressure at render time
This is intentionally heavy data. It's gated behind --debug-record and
only used during active debugging sessions.
Expert Next.js App Router
Developpement
Un skill qui transforme Claude en expert Next.js App Router.
Générateur de README
Developpement
Crée des README.md professionnels et complets pour vos projets.
Rédacteur de Documentation API
Developpement
Génère de la documentation API complète au format OpenAPI/Swagger.