بنقرة واحدة
tracing-best-practices
// TRIGGER when writing, modifying, or reviewing any Rust code that involves tracing, logging, spans, events,
// TRIGGER when writing, modifying, or reviewing any Rust code that involves tracing, logging, spans, events,
Inspect uniclipboard logs from BOTH the macOS host and the mounted Windows peer when debugging cross-platform sync, pairing, transfer, or daemon issues. Use whenever the user asks to "check logs", "see what's happening on both sides", or describes a symptom that involves the Windows peer (e.g. "Windows didn't receive...", "Mac sent but...", pairing/transfer/sync failures during dual-side dev).
Push the current branch and open a GitHub pull request against `main`. Use when the user says "create PR", "open PR", "make a PR", "提 PR", "开 PR", "打 PR", or otherwise asks to publish their committed work for review. Before pushing, the skill (1) verifies the current branch name actually reflects the change and proposes a rename if it doesn't, and (2) scans the PR's diff against `docs-site/` for docs that look stale and lists update candidates for the user to approve.
Trigger the prepare-release workflow on GitHub Actions. Supports optional arguments for version, bump type, and channel. Defaults to bump=patch, channel=stable.
Push the macOS working-tree changes to the Windows peer's repo via rsync over SSH. Use when the user wants to "sync to win", "push my changes to windows", "mirror the working tree", or otherwise propagate uncommitted edits from this Mac to the paired Windows machine for cross-platform testing of uniclipboard. Replaces the older SMB-mount strategy.
Meta-skill for evolving other skills. Invoke after resolving a real-world issue to extract lessons and merge them into the referenced SKILL.md. Usage - /skill-evolve @path/to/SKILL.md
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
| name | tracing-best-practices |
| description | TRIGGER when writing, modifying, or reviewing any Rust code that involves tracing, logging, spans, events, |
This skill MUST be followed whenever you:
#[instrument] attributestracing::info!, tracing::debug!, tracing::warn!, tracing::error!, tracing::trace! eventsinfo_span!, debug_span!, etc.)tokio::spawn or any async task spawningtracing records three types of structured data — NOT print text:
trace_id, device_id, session_id, state, elapsed_ms)#[instrument] alone only creates a span; tracing_subscriber::fmt only outputs events. A span with no events inside it produces zero log output, making the function invisible in logs.Applies to: Tauri commands, CLI handlers, HTTP/IPC handlers, background task entries, scheduled task entries.
#[instrument(
name = "cmd.get_clipboard_items",
level = "info",
skip(runtime),
fields(trace_id = %uuid::Uuid::new_v4())
)]
pub async fn get_clipboard_items(runtime: State<'_, AppRuntime>) -> Result<Vec<Item>, String> {
// ...
}
Requirements:
trace_id or equivalent request identifiersession_id, device_id, space_id)Applies to: start_join_space, submit_passphrase, persist_entry, handle_event, sync_once, etc.
#[instrument(
name = "space_access.submit_passphrase",
level = "info",
skip(self, passphrase),
fields(session_id = %self.session_id, device_id = %self.device_id, state = ?self.state)
)]
async fn submit_passphrase(&mut self, passphrase: String) -> Result<()> {
// Span name reflects business action, not technical detail
}
Applies to: DB read/write, file I/O, network send/receive, subprocess calls, encryption boundaries, WebSocket/libp2p/relay/RPC.
// CORRECT - Propagate current span
tokio::spawn(task().in_current_span());
// CORRECT - Create specific span for spawned work
let span = tracing::info_span!("pairing_session", session_id = %session_id);
tokio::spawn(task.instrument(span));
Long-lived background loops MUST create child spans or events per iteration. Channel/callback boundaries MUST re-attach message IDs into new span context.
.entered() in async functionsEnteredSpan contains *mut () → not Send → holding it across .await makes the future non-Send → tokio::spawn / JoinSet::spawn will fail to compile.
#[instrument] (preferred)Best for: standalone async functions where parameters can be skipped.
#[instrument(skip_all, fields(session_id = %session_id, peer_id = %peer_id))]
async fn handle_message(session_id: &str, peer_id: &str, msg: Message) -> Result<()> {
info!("received message"); // automatically under the span
do_something().await; // await-safe
}
.instrument(span)Best for: futures passed to tokio::spawn / JoinSet::spawn.
let span = tracing::info_span!("pairing.action_loop");
tasks.spawn(run_action_loop(rx, cancel).instrument(span));
async { }.instrument(span).awaitBest for: match arms, if-branches, or other blocks that need a local span with .await inside.
match event {
Event::Succeeded { session_id } => {
let span = info_span!("pairing.session", session_id = %session_id);
async {
info!(event = "succeeded"); // under the span
notify_peer().await; // await-safe
}.instrument(span).await;
}
}
// ❌ Compile error — EnteredSpan is not Send
let _guard = info_span!("my_span").entered();
something.await; // _guard held across await
// ❌ Same problem, different syntax
let span = info_span!("my_span");
let _guard = span.enter();
something.await; // _guard still held across await
#[instrument]hash_bytes, normalize_path, parse_header, to_png — no business semantics, no span. Adds noise.
Tight loops, per-poll/tick functions, per-item iteration helpers. Use sampled events or aggregate stats instead.
#[instrument] records params via Debug by default. Passwords, tokens, ciphertext, large blobs WILL leak unless explicitly skip()-ed.
#[instrument] Standard Template#[instrument(
name = "space_access.submit_passphrase", // Stable name — survives refactors
level = "info", // Explicit default level
skip(self, passphrase), // Skip sensitive/large params
fields(
session_id = %self.session_id, // Key business context
device_id = %self.device_id,
state = ?self.state
)
)]
async fn submit_passphrase(&mut self, passphrase: String) -> Result<()> { ... }
skip Rules — ALWAYS skip:self (unless Debug is very light and valuable)password, passphrase, token, secretArc<AppState> / runtime / container% vs ? Selection:%field (Display): stable, short, search-friendly fields?field (Debug): enums, struct summaries, diagnostic detailDo NOT automatically record full return values. Log key results as separate events:
tracing::info!(session_id = %session_id, result = "accepted", "space access completed");
#[instrument] Requires Events Inside the Function Body#[instrument] generates a span, not an event. tracing_subscriber::fmt only writes events to log output (console/JSON file). A function with only #[instrument] and no event macros (info!, debug!, etc.) produces zero log output — the function is completely invisible in logs.
Every #[instrument]-annotated function MUST emit at least one tracing event. Minimum pattern:
#[instrument(name = "api.search_query", level = "info", skip(state, params), fields(query = %params.query))]
async fn search_query_handler(state: State, params: Query) -> Result<Json<Response>, Error> {
// ... business logic ...
let result = do_search().await?;
// At minimum, log the outcome — this makes the function visible in logs
info!(total = result.total, "search completed");
Ok(Json(result))
}
For thin delegation functions (usecases that just call a port), a single debug! after the call is sufficient:
#[tracing::instrument(name = "usecase.index_entry.execute", skip(self, doc, postings), fields(entry_id = %doc.entry_id))]
pub async fn execute(&self, doc: SearchDocument, postings: Vec<SearchPosting>) -> Result<(), SearchError> {
self.search_index.index_entry(doc, postings).await?;
tracing::debug!("entry indexed successfully");
Ok(())
}
Events record facts within a span. Message MUST be short; fields carry the data.
tracing::info!(
session_id = %session_id,
peer_id = %peer_id,
attempt = retry_count,
"relay connection established"
);
MUST include: error_kind (stable category), error or source = ?err, retryability flag, key context IDs.
tracing::error!(
session_id = %session_id,
error_kind = "proof_verification_failed",
retryable = false,
error = %err,
"space access failed"
);
tracing::debug!(
session_id = %session_id,
event = "ReceivedProof",
from_state = ?old_state,
to_state = ?new_state,
"state transition"
);
Field names MUST be stable (once shipped, avoid renaming). Use snake_case.
| Field | Purpose |
|---|---|
trace_id | Cross-boundary request correlation |
request_id | Per-request identifier |
session_id | Pairing/space session |
task_id | Background task identifier |
device_id | Device identifier |
space_id | Space identifier |
peer_id | Network peer |
user_action | What the user triggered |
state / from_state / to_state | State machine context |
elapsed_ms | Duration |
retry_count | Retry attempts |
error_code | Business error code |
error_kind | Stable error classification |
error: human-readable summaryerror_kind: stable classificationerror_code: business error codesource: originating moduleretryable: whether retry makes senseWhen debugging, log: length, hash, summary, or object ID instead.
| Level | Meaning | Examples |
|---|---|---|
| ERROR | Unrecoverable, or main flow result affected | Decryption failed, DB corruption, illegal state transition |
| WARN | Abnormal but system continues, or fallback triggered | Relay failed -> switched backup, invalid config -> using default |
| INFO | Important business milestones (safe for production) | Space join success, pairing established, sync start/complete |
| DEBUG | Development/troubleshooting context | State machine event received, branch selection, retry parameters |
| TRACE | Ultra-fine internal behavior (local debugging only) | Per protocol frame, per loop iteration, per poll |
At least one error event MUST exist at the boundary where the error is discovered.
// WRONG - Same error logged at 5 stack levels
error!("failed: {}", err); // layer 1
error!("op failed: {}", err); // layer 2
error!("handler failed: {}", err); // layer 3
// CORRECT - Full detail at boundary, summary at top
// At discovery boundary:
tracing::error!(error_kind = "db_write_failed", error = %err, entry_id = %id, "persist failed");
// Upper layer: just propagate via ? or log only business outcome
Don't lump them with generic failures. They need separate classification for operational statistics.
State machines are the MOST important tracing target in this project.
#[instrument(
name = "space_access.handle_event",
level = "debug",
skip(self, event),
fields(session_id = %self.session_id, state = ?self.state)
)]
async fn handle_event(&mut self, event: Event) -> Result<()> { ... }
Fields: event, from_state, to_state, reason, session_id.
Never lump with generic failures.
Fields: trace_id, request_id, route/command, client/source, session_id.
GUI sends trace_id with each daemon request; daemon creates span with same ID. Connects "frontend click -> IPC -> usecase -> infra" into one trace.
Record length, type, or object ID only.
observability::init_tracing() / bootstrap::init_observability(). No module may independently initialize a subscriber.
info baseline, own crates at debug, noisy third-party crates at warn. Uses EnvFilter with directives.
tracing_appender::non_blocking)When writing or reviewing tracing code, verify:
| Check | Rule |
|---|---|
| Entry function has span? | MUST |
| Key usecase/orchestrator has span? | MUST |
#[instrument] function has at least one event inside? | MUST |
tokio::spawn propagates span? | MUST |
Sensitive params use skip()? | MUST |
| Errors have structured fields (not just string)? | MUST |
| Field names follow project convention? | MUST |
| Subscriber init centralized? | MUST |
| File output holds WorkerGuard? | MUST |
High-frequency function avoids needless #[instrument]? | SHOULD |
| Span names stable and business-oriented? | SHOULD |
info level safe for long-term production? | SHOULD |
| State machine transitions use structured events? | SHOULD |
| Same error repeated across stack layers? | FORBIDDEN |
| Secret/large payload in tracing output? | FORBIDDEN |
.entered() / .enter() held across .await in async? | FORBIDDEN |
#[instrument] with no events inside (silent span)? | FORBIDDEN |