//! Tracing-based logging setup. //! //! TUI applications cannot write logs to stdout/stderr without //! corrupting the terminal, so logs go to a file. The path comes //! from the CLI (`--log-file`) or the `RDBMS_PLAYGROUND_LOG_FILE` //! environment variable; if neither is set we default to //! `~/.rdbms-playground/playground.log` and create directories as //! needed. //! //! ## Level conventions (X1 — `requirements.md`) //! //! Instrumentation across the tree follows a consistent level //! discipline so the default `info` filter stays quiet and //! `RDBMS_PLAYGROUND_LOG=debug` (or `=trace`) is a rich, layered //! diagnostic stream. The env filter (`RDBMS_PLAYGROUND_LOG`, //! full `EnvFilter` syntax) controls this independently of the //! file path above; the default is `info`. //! //! - **`error!`** — unrecoverable failure (fatal persistence, a //! panic-equivalent). The process is going down or a command is //! hard-failing. //! - **`warn!`** — recoverable failure or a fallback taken (a //! snapshot couldn't be staged, a `PRAGMA` couldn't be restored, //! an integrity check rolled a rebuild back). //! - **`info!`** — low-volume lifecycle, visible by default: db //! worker start/exit, project create/open, "logging initialised". //! - **`debug!`** — the bulk of instrumentation, one line per //! *executed* command and the decision points within it (executor //! entry with key params, autofill/cascade summaries, the //! rebuild-table primitive, persistence writes, render-mode //! choice). Off by default. //! - **`trace!`** — hot paths only: per-keystroke parsing //! (`dsl::parser`), per-key input handling (`app`), per-refresh //! table reads. A firehose; never on except when debugging that //! specific layer. //! //! Rule of thumb for new code: a loop logs a single summary count, //! never per-iteration at `debug`/`info`. Logs are developer-facing, //! so naming the engine (SQLite/PRAGMA) is fine here even though the //! "no engine name" rule (ADR-0002) forbids it in user-facing strings. use std::fs::{File, OpenOptions, create_dir_all}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; const DEFAULT_LOG_DIR: &str = ".rdbms-playground"; const DEFAULT_LOG_FILE: &str = "playground.log"; /// Initialise tracing to write to the given file path, or to a /// platform-default path when `path` is `None`. pub fn init(path: Option<&Path>) -> Result { let chosen = match path { Some(p) => p.to_path_buf(), None => default_log_path()?, }; if let Some(parent) = chosen.parent() { create_dir_all(parent) .with_context(|| format!("create log directory {}", parent.display()))?; } let file = open_log_file(&chosen)?; let filter = EnvFilter::try_from_env("RDBMS_PLAYGROUND_LOG") .unwrap_or_else(|_| EnvFilter::new("info")); let layer = fmt::layer() .with_writer(file) .with_ansi(false) .with_target(true); tracing_subscriber::registry() .with(filter) .with(layer) .init(); tracing::info!(path = %chosen.display(), "logging initialised"); Ok(chosen) } fn open_log_file(path: &Path) -> Result { OpenOptions::new() .create(true) .append(true) .open(path) .with_context(|| format!("open log file {}", path.display())) } fn default_log_path() -> Result { let home = home_dir().context("could not determine HOME directory for default log path")?; Ok(home.join(DEFAULT_LOG_DIR).join(DEFAULT_LOG_FILE)) } fn home_dir() -> Option { // std::env::home_dir is deprecated; do the lookup ourselves with // the platform-conventional environment variables. Once we add a // proper path strategy (project storage, ADR-0004) this can be // replaced with the chosen helper crate. if let Some(p) = std::env::var_os("HOME") { return Some(PathBuf::from(p)); } if let (Some(drive), Some(path)) = (std::env::var_os("HOMEDRIVE"), std::env::var_os("HOMEPATH")) { let mut combined = PathBuf::from(drive); combined.push(path); return Some(combined); } std::env::var_os("USERPROFILE").map(PathBuf::from) }