유대선
프로젝트로
·기술 회고·1

Two-layer observability — Claude Code hooks for dev-time, Rust tracing for runtime

Split observability into two layers with different purposes: Claude Code hooks capture every Bash/Edit/Prompt/Stop during dev, Rust `tracing` captures PTY lifecycle on the user's machine. Codified as CLAUDE.md RULE #8.

Why two layers

The two layers answer different questions. Layer 1 answers "why did Claude make that decision?". Layer 2 answers "what actually happened on the user's machine?". Conflating them produces a log that's bad at both.

What landed

Rust init at app start (apps/desktop/src-tauri/src/lib.rs):

fn init_logging() {
    let log_dir = std::env::var("HOME")
        .map(|h| format!("{}/Library/Logs/DalkkakAI", h))
        .unwrap_or_else(|_| "./logs".to_string());
    let _ = std::fs::create_dir_all(&log_dir);
 
    let file_appender = tracing_appender::rolling::daily(&log_dir, "runtime.log");
    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
    Box::leak(Box::new(guard));  // worker thread lives as long as the app
 
    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(
            "info,appsdesktop=debug,appsdesktop_lib=debug"
        ));
 
    let _ = tracing_subscriber::fmt()
        .with_writer(non_blocking)
        .with_ansi(false)
        .with_target(true)
        .with_env_filter(filter)
        .try_init();
}

Commit 89a8c26. Rule codified as CLAUDE.md RULE #8.

Pattern

When you keep wanting to grep one log for two unrelated questions, that log is hiding a missing split. Build the two logs deliberately — separate filenames, separate writers, separate retention — and the question you're asking next will pick the right one.