Logging Guidelines
Capsa uses the tracing crate for structured logging in library and daemon code, with println!/eprintln! reserved for user-facing CLI output.
When to Use Each Mechanism
| Mechanism | When to Use |
|---|---|
println! | User-facing CLI output (command results, prompts, formatted data) |
eprintln! | CLI errors/warnings shown to user, test skip messages |
tracing | All internal diagnostics, library code, daemon processes |
Special Cases
- Guest-side code (
capsa-sandbox-init,capsa-sandbox-agent): Useprintln!/eprintln!only. These run in minimal VM environments where tracing setup adds unnecessary complexity. - Build scripts: Use
println!for cargo directives only. - Tests: Use
eprintln!for skip messages (e.g.,eprintln!("Skipping: no /dev/kvm")).
Log Levels
| Level | When to Use | Example |
|---|---|---|
error! | Unrecoverable failures, bugs, data corruption | error!("Failed to create VM: {}", e) |
warn! | Recoverable errors, degraded operation, unexpected but handled | warn!("Connection reset, retrying") |
info! | Service lifecycle, significant state changes, config summary | info!("VM started") |
debug! | Flow tracing, connection lifecycle, operational details | debug!("NAT: {} -> {}", src, dst) |
trace! | Hot paths, packet-level, verbose internals | trace!("vCPU exit: {:?}", exit) |
Rule of thumb: If you'd want to see it in production logs by default, use info!. If only when debugging, use debug! or trace!.
Initializing Tracing
All binaries that use tracing should follow this pattern:
rust,ignore
use tracing_subscriber::EnvFilter;
fn init_logging() {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"))
)
.with_writer(std::io::stderr)
.init();
}Key points:
- Default to
infolevel (avoid debug/trace spam by default) - Write to stderr (stdout is for program output)
- Respect
RUST_LOGenvironment variable for override
Structured Logging
Use structured fields for machine-parseable data:
rust,ignore
// Preferred: structured fields + human message
tracing::warn!(port = port, error = %e, "Failed to bind listener");
tracing::debug!(src = %guest_addr, dst = %remote_addr, "NAT connection");
// Avoid: embedding data only in message
tracing::warn!("Failed to bind listener on port {}: {}", port, e);Field Naming Conventions
| Field | Usage |
|---|---|
error, err | Error values (use % for Display: error = %e) |
port, addr, path | Network/filesystem identifiers |
vm_id, conn_id | Resource identifiers |
Use snake_case for all field names.
Subprocess Logging
Capsa spawns subprocesses (e.g., capsa-vmm on macOS and optionally on Linux). Follow these guidelines:
Subprocess Output Routing
- Subprocesses should write logs to stderr, never stdout
- The parent process can capture stderr and either:
- Forward it to its own tracing (preferred for integration)
- Pass it through to the user's stderr (simpler)
- Stdout should be reserved for structured IPC (if any)
Log Correlation
When spawning subprocesses, include identifiers to correlate logs:
rust,ignore
// Parent spawns child with VM ID in environment
cmd.env("CAPSA_VM_ID", vm_id);
// Child includes VM ID in logs
let vm_id = std::env::var("CAPSA_VM_ID").unwrap_or_default();
tracing::info!(vm_id = %vm_id, "Subprocess started");Error Propagation
- Subprocesses should exit with non-zero status on fatal errors
- Log the error before exiting so it's captured
- Parent should check exit status AND captured stderr for diagnostics
Current Subprocess Behavior
| Subprocess | Logging | Output |
|---|---|---|
capsa-vmm | tracing to stderr | Respects RUST_LOG |
capsa-sandbox-init | println to stdout | Visible on VM console |
capsa-sandbox-agent | println to stdout | Visible on VM console |
What NOT to Log
- Sensitive data (credentials, tokens, keys)
- High-frequency events at
info!level or above (usedebug!ortrace!) - Redundant information already in structured fields
Adding Tracing to a Crate
Add the dependency (it's in the workspace):
toml[dependencies] tracing = { workspace = true }For binaries, also add:
toml[dependencies] tracing-subscriber = { version = "0.3", features = ["env-filter"] }Library crates should NOT initialize tracing - that's the binary's responsibility.