diff --git a/.gitignore b/.gitignore index 98f7320e..e3d6f2a8 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,8 @@ dist/ # omo .sisyphus/ +# cc +.claude/ + # acta data/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e1943eff..ec9fba08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,21 +8,16 @@ version = "0.1.1" dependencies = [ "acta-build", "ansi_colours", - "anstyle", "anstyle-lossy", "arc-swap", - "arrayvec", "chrono", "compact_str", - "derive_more", "flate2", "nerd-font-symbols", "owo-colors", "serde", "smallvec", - "smart-default", "supports-color", - "tap", "thiserror", "tokio", "tracing", @@ -105,12 +100,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "autocfg" version = "1.5.0" @@ -186,15 +175,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -234,29 +214,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -486,15 +443,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -516,12 +464,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.228" @@ -598,17 +540,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smart-default" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -641,12 +572,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" version = "2.0.18" @@ -810,18 +735,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 6d5f9ec4..aa9c0e3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,6 @@ serde = { version = "1", features = ["derive"], optional = true } tracing = "0.1" tracing-log = "0.2" smallvec = "1" -arrayvec = "0.7" tracing-appender = { version = "0.2", optional = true } nerd-font-symbols = { version = "0.3.0", optional = true } tokio = { version = "1", default-features = false, features = [ @@ -57,14 +56,10 @@ tracing-subscriber = { version = "0.3", features = [ "ansi", "std", ] } -smart-default = "0.7.1" supports-color = "3" ansi_colours = "1.2" anstyle-lossy = "1.1" -anstyle = "1" arc-swap = "1.7.1" -derive_more = { version = "2.0.0", features = ["full"] } -tap = "1.0.1" compact_str = { version = "0.9", features = ["serde"] } [build-dependencies] diff --git a/README.md b/README.md index 2f1ccf83..4e1ad364 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Keep the returned guard alive for as long as logging is needed. Dropping it stop | Feature | Enabled by default | Description | | -------------- | ------------------ | -------------------------------------------------------------------------------------------------------- | | `unicode` | Yes | Uses the Unicode icon set unless `nerd` selects Nerd Font icons. | -| `file` | Yes | Enables `init`, `TracingGuard`, `build_file_layer`, and file logging through `tracing-appender`. | +| `file` | Yes | Enables `init`, `TracingGuard`, and file logging through `tracing-appender`. | | `compress` | No | Enables `Rotation::Compress` for gzip-compressing old log files. | | `serde` | No | Adds `Serialize` / `Deserialize` support for config types. | | `nerd` | No | Enables Nerd Font icons through `Icons::NERD` and uses them by default. | @@ -82,14 +82,14 @@ If you disable default features, `init` is unavailable unless the `file` feature ```rust use acta::{ - init, Format, Level, Config, Result, Writer, + init, Format, LayerConfig, Level, Config, Result, Writer, }; fn main() -> Result<()> { let config = Config { - level: Level::Debug, + filter: Level::Debug.into(), writers: vec![Writer { - format: Format::Compact, + format: Format::Compact(LayerConfig::compact()), ansi: true, show_path: true, show_spans: true, @@ -109,9 +109,9 @@ fn main() -> Result<()> { | Format | Description | | ----------------- | ------------------------------------------------------------------ | -| `Format::Compact` | Default themed formatter with optional path and span display. | -| `Format::Pretty` | `tracing-subscriber` pretty formatter with file and line metadata. | -| `Format::Json` | Flattened JSON events without ANSI colors. | +| `Format::Compact(LayerConfig)` | Default themed formatter with optional path and span display. | +| `Format::Pretty(LayerConfig)` | `tracing-subscriber` pretty formatter with file and line metadata. | +| `Format::Json(LayerConfig)` | Flattened JSON events without ANSI colors. | ## File logging File logging is available with the `file` feature, which is enabled by default. File logs are written as flattened JSON @@ -119,15 +119,15 @@ events. ```rust use acta::{ - init, Level, Config, Result, Rotation, Writer, WriterTarget, + init, Format, LayerConfig, Level, Config, Result, Rotation, Writer, WriterTarget, }; use std::path::PathBuf; fn main() -> Result<()> { let config = Config { - level: Level::Info, + filter: Level::Info.into(), writers: vec![Writer { - format: Format::Compact, + format: Format::Compact(LayerConfig::compact()), target: WriterTarget::File { path: PathBuf::from("logs/app.log"), rotation: Rotation::Rename, @@ -156,10 +156,10 @@ Supported rotation modes: acta uses `tracing-subscriber` `EnvFilter` directive syntax for startup filters and runtime reloads. ```rust -use acta::{Level, Config}; +use acta::{Config, Filter}; let config = Config { - level: Level::Custom("info,my_crate=debug,my_crate::db=trace".to_owned()), + filter: Filter::from_directive("info,my_crate=debug,my_crate::db=trace"), ..Default::default() }; ``` @@ -175,23 +175,22 @@ fn main() -> Result<()> { guard.set_level(Level::Debug)?; guard.set_target_level("my_crate", Level::Trace)?; guard.remove_target_level("my_crate")?; - guard.reload("info,my_crate=trace")?; guard.set_filter( - Filter::new(Level::Warn).with_target("my_crate", Level::Debug), + Filter::from_directive("warn,my_crate=debug"), )?; Ok(()) } ``` -`RUST_LOG` is not read automatically. If you want to use it, pass its value into `Level::Custom`. +`RUST_LOG` is not read automatically. If you want to use it, pass its value into `Filter::from_directive`. ```rust -use acta::{Level, Config}; +use acta::{Config, Filter}; let directive = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); let config = Config { - level: Level::Custom(directive), + filter: Filter::from_directive(directive), ..Default::default() }; ``` @@ -206,13 +205,35 @@ use acta::{Formatter, Icons, LevelLabels, Theme}; let formatter = Formatter::new() .with_theme(Theme::tokyo_night()) .with_icons(Icons::UNICODE) - .with_labels(LevelLabels::long()) + .with_labels(LevelLabels::DEFAULT) .with_time_format("%H:%M:%S") .with_show_path(true) .with_show_spans(true); ``` -The default path width is generated at build time. +The default path width is computed at acta's build time from its own source +tree — fine for a quick start, but if you want the column to fit **your** +project's source paths, add the `acta-build` helper to your build script: + +```toml +[build-dependencies] +acta-build = "0.1" +``` + +```rust +// build.rs +fn main() { + let width = acta_build::walk_src_max_width("src", "src/"); + println!("cargo:rustc-env=ACTA_PATH_WIDTH={width}"); + println!("cargo:rerun-if-changed=src"); +} +``` + +```rust +// in your code +let width: usize = env!("ACTA_PATH_WIDTH").parse().unwrap_or(40); +let formatter = Formatter::new().with_path_width(width); +``` ## Themes @@ -232,16 +253,16 @@ Create custom themes from RGB values: ```rust use acta::Theme; -let custom = Theme::new( - (91, 206, 250), // accent - (245, 169, 184), // secondary - (255, 255, 255), // text - (255, 85, 85), // error - (255, 200, 60), // warn - (91, 206, 250), // info - (245, 169, 184), // debug - (240, 240, 240), // trace -); +let custom = Theme { + accent: (91, 206, 250), + secondary: (245, 169, 184), + text: (255, 255, 255), + error: (255, 85, 85), + warn: (255, 200, 60), + info: (91, 206, 250), + debug: (245, 169, 184), + trace: (240, 240, 240), +}; ``` ## Icons and labels @@ -250,8 +271,8 @@ let custom = Theme::new( use acta::{Icons, LevelLabels}; let unicode_icons = Icons::UNICODE; -let short_labels = LevelLabels::short(); -let long_labels = LevelLabels::long(); +let short_labels = LevelLabels::SHORT; +let long_labels = LevelLabels::DEFAULT; ``` With the `nerd` feature enabled: @@ -287,35 +308,6 @@ fn main() -> Result<()> { Ok(()) } ``` -If you build the subscriber manually, `build_reload_filter` returns a `TracingGuard` that also supports style reloading: - -```rust -use acta::{ - build_reload_filter, Formatter, Level, Result, - Theme, Style, Writer, -}; -use tracing_subscriber::prelude::*; - -fn main() -> Result<()> { - let formatter = Formatter::new().with_theme(Theme::monokai()); - let style = *formatter.style_config(); - let console_layer = tracing_subscriber::fmt::Layer::default() - .with_writer(std::io::stdout) - .event_format(formatter) - .boxed(); - let (filter_layer, mut guard) = build_reload_filter(Level::Info, style); - - let subscriber = tracing_subscriber::registry() - .with(console_layer) - .with(filter_layer); - tracing::subscriber::set_global_default(subscriber)?; - - guard.with_style(|s| s.theme = Theme::dracula()); - - Ok(()) -} -``` -This low-level setup requires adding `tracing-subscriber` as a direct dependency. ## Async console writers diff --git a/build.rs b/build.rs index be2555f7..e0a5889f 100644 --- a/build.rs +++ b/build.rs @@ -1,19 +1,11 @@ #![allow(clippy::expect_used)] fn main() { - let max = match acta_build::walk_src_max_width("src", "src/") { - Ok(m) => m, - Err(e) => { - println!("cargo::warning=walk_src_max_width failed: {e}"); - 20 - } - }; - std::fs::write( - std::path::Path::new(&std::env::var("OUT_DIR").expect("Cargo should set OUT_DIR")) - .join("path_width"), - max.to_string(), - ) - .expect("failed to write path_width to OUT_DIR"); - + let width = acta_build::walk_src_max_width("src", "src/"); + let out_dir = std::env::var_os("OUT_DIR").expect("Cargo should set OUT_DIR"); + let path = std::path::Path::new(&out_dir).join("path_width"); + if let Err(e) = std::fs::write(&path, width.to_string()) { + println!("cargo::warning=failed to write {}: {e}", path.display()); + } println!("cargo::rerun-if-changed=src"); } diff --git a/crates/acta-build/Cargo.toml b/crates/acta-build/Cargo.toml index ea089b9b..6aaffc57 100644 --- a/crates/acta-build/Cargo.toml +++ b/crates/acta-build/Cargo.toml @@ -2,7 +2,7 @@ name = "acta-build" version.workspace = true edition = "2024" -description = "Build-time helper for acta path width calculation" +description = "Build-time helper for acta: compute path-column width from a project's source tree" repository = "https://github.com/Sn0wo2/acta" publish = true license = "Apache-2.0" diff --git a/crates/acta-build/src/lib.rs b/crates/acta-build/src/lib.rs index f7bb521e..254506da 100644 --- a/crates/acta-build/src/lib.rs +++ b/crates/acta-build/src/lib.rs @@ -1,14 +1,55 @@ +//! Build-time helper for [acta](https://crates.io/crates/acta). +//! +//! Call from your `build.rs` to compute a sensible default `path_width` for +//! [`acta::Formatter`] based on the longest source-file path in your project. +//! +//! # Example +//! +//! Add to your `Cargo.toml`: +//! +//! ```toml +//! [build-dependencies] +//! acta-build = "0.1" +//! ``` +//! +//! In `build.rs`: +//! +//! ```no_run +//! # fn example() { +//! let width = acta_build::walk_src_max_width("src", "src/"); +//! println!("cargo:rustc-env=ACTA_PATH_WIDTH={width}"); +//! println!("cargo:rerun-if-changed=src"); +//! # } +//! ``` +//! +//! Then in your code: +//! +//! ```ignore +//! let width: usize = env!("ACTA_PATH_WIDTH").parse().unwrap_or(40); +//! let formatter = acta::Formatter::new().with_path_width(width); +//! ``` + +use std::path::Path; use walkdir::WalkDir; -pub fn walk_src_max_width(dir: &str, strip_prefix: &str) -> Result { - let entries: Vec<_> = WalkDir::new(dir) +const FALLBACK_WIDTH: usize = 40; +const PADDING: usize = 4; + +/// Walk `dir` recursively, find every `.rs` file, and return +/// `max(path_len_after_stripping(strip_prefix)) + 4`. +/// +/// Returns [`FALLBACK_WIDTH`] (40) if `dir` does not exist or contains no +/// `.rs` files — safe to call unconditionally from a `build.rs`. +#[must_use] +pub fn walk_src_max_width(dir: impl AsRef, strip_prefix: &str) -> usize { + let entries: Vec<_> = WalkDir::new(dir.as_ref()) .into_iter() - .filter_map(|e| e.ok()) + .filter_map(Result::ok) .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs")) .collect(); if entries.is_empty() { - return Err(format!("no .rs files found in {dir}")); + return FALLBACK_WIDTH; } let max = entries @@ -17,12 +58,10 @@ pub fn walk_src_max_width(dir: &str, strip_prefix: &str) -> Result(w); + let layer = build_layer(w); let subscriber = tracing_subscriber::registry().with(layer); tracing::subscriber::with_default(subscriber, f); } -fn none_layer() -> Option> -where - S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, -{ - None -} - -fn run_with_reload( - style: Style, - w: &Writer, - level: Level, - f: impl FnOnce(&mut acta::TracingGuard), -) { - let (filter_layer, mut guard) = build_reload_filter(level, style); - let subscriber = tracing_subscriber::Registry::default() - .with(Some(build_layer::(w))) - .with(none_layer()) - .with(none_layer()) - .with(none_layer()) - .with(none_layer()) - .with(filter_layer); - tracing::subscriber::with_default(subscriber, || f(&mut guard)); -} fn emit_demo(label: &str) { tracing::info!("{label}: info"); tracing::warn!(user = "alice", count = 42, "{label}: warn"); @@ -277,68 +254,6 @@ fn main() { emit_spans, ); - section("RELOAD"); - - log!(sub, "Level reload"); - run_with_reload( - Style::default(), - &Writer { - show_path: false, - show_spans: false, - ..Default::default() - }, - Level::Info, - |h| { - tracing::info!("level=Info: info passes"); - h.set_level(Level::Debug).unwrap(); - log!(info, "→ set_level(Debug)"); - tracing::debug!("level=Debug: debug passes"); - }, - ); - - log!(sub, "Target-level reload"); - run_with_reload( - Style::default(), - &Writer { - show_path: false, - show_spans: false, - ..Default::default() - }, - Level::Debug, - |h| { - tracing::info!(target: "demo", "before: info@demo passes (level=Debug)"); - h.set_target_level("demo", Level::Warn).unwrap(); - log!(info, "→ set_target_level(demo, Warn)"); - tracing::info!(target: "demo", "after: info@demo suppressed"); - tracing::warn!(target: "demo", "after: warn@demo passes"); - }, - ); - - log!(sub, "Style switch"); - let style = Style { - icons: Icons::UNICODE, - ..Default::default() - }; - run_with_reload( - style, - &Writer { - style, - show_path: false, - show_spans: false, - ..Default::default() - }, - Level::Info, - |h| { - tracing::info!("unicode + acta theme"); - h.with_style(|s| s.theme = Theme::monokai()); - log!(info, "→ monokai theme"); - tracing::info!("monokai theme"); - h.with_style(|s| s.icons = Icons::NERD); - log!(info, "→ nerd icons"); - tracing::error!("nerd icons"); - }, - ); - section("INFRA"); log!(sub, "Level → directive"); @@ -349,10 +264,16 @@ fn main() { Level::Debug, Level::Trace, Level::Off, - Level::Custom("info,my_crate=debug".into()), ] { log!(info, &format!("{l:?} → \"{}\"", l.as_directive())); } + log!( + info, + &format!( + "Filter::from_directive(\"info,my_crate=debug\") → \"{}\"", + Filter::from_directive("info,my_crate=debug").as_directive() + ) + ); log!(sub, "build_layer"); for (desc, w) in [ @@ -367,7 +288,7 @@ fn main() { }, ), ] { - drop(build_layer::(&w)); + drop(build_layer(&w)); log!(success, &format!("build_layer({desc})")); } @@ -396,7 +317,8 @@ fn main() { } } - log!(sub, "init — end-to-end"); + section("RELOAD via init"); + log!(sub, "init + runtime reload"); let dir = std::path::Path::new("data/logs/full"); drop(std::fs::create_dir_all(dir)); let config = Config::builder() @@ -418,12 +340,26 @@ fn main() { }) .build(); match init(config) { - Ok(g) => { + Ok(mut g) => { log!(success, "init"); if let Some(p) = g.log_path() { log!(info, &format!("file → {}", p.display())); } tracing::info!(init = true, "console + file"); + + g.set_level(Level::Warn).unwrap(); + log!(info, "→ set_level(Warn)"); + tracing::info!("info suppressed"); + tracing::warn!("warn passes"); + + g.set_target_level("demo", Level::Trace).unwrap(); + log!(info, "→ set_target_level(demo, Trace)"); + tracing::trace!(target: "demo", "demo trace passes"); + + g.with_style(|s| s.theme = Theme::monokai()); + log!(info, "→ style switch (monokai)"); + tracing::warn!("monokai"); + drop(g); } Err(e) => log!(fail, &format!("init: {e}")), diff --git a/src/builder.rs b/src/builder.rs index f148abbf..8c67de4d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -6,53 +6,116 @@ use std::io; #[cfg(feature = "file")] use std::path::PathBuf; use std::sync::Arc; +use tracing_subscriber::Registry; use tracing_subscriber::fmt::format::FmtSpan; +use tracing_subscriber::layer::Layered; use tracing_subscriber::prelude::*; -pub type FmtLayer = - Box + Send + Sync>; - -// tracing_subscriber::reload::Handle needs the concrete S. -// Six optional layers stacked → this pyramid. Users never name it. -type Layer1 = tracing_subscriber::layer::Layered< - Option>, - tracing_subscriber::Registry, ->; -type Layer2 = tracing_subscriber::layer::Layered>, Layer1>; -type Layer3 = tracing_subscriber::layer::Layered>, Layer2>; -type Layer4 = tracing_subscriber::layer::Layered>, Layer3>; -pub(crate) type InnerSubscriber = - tracing_subscriber::layer::Layered>, Layer4>; - -pub(crate) struct ReloadHandle( - pub(crate) tracing_subscriber::reload::Handle, -); - -impl ReloadHandle { - fn new( - filter: tracing_subscriber::EnvFilter, - ) -> ( - tracing_subscriber::reload::Layer, - Self, - ) { - let (layer, handle) = tracing_subscriber::reload::Layer::new(filter); - (layer, Self(handle)) +#[cfg(any(feature = "file", feature = "custom-async", feature = "native-async"))] +use crate::writer; + +pub(crate) type BoxedLayer = Box + Send + Sync>; + +/// Newtype wrapping Vec to avoid orphan-rule reliance on +/// tracing-subscriber's blanket `impl Layer for Vec`. +pub(crate) struct Layers(pub(crate) Vec); + +impl tracing_subscriber::Layer for Layers { + fn on_layer(&mut self, subscriber: &mut Registry) { + for layer in &mut self.0 { + layer.on_layer(subscriber); + } } - fn modify(&self, f: impl FnOnce(&mut tracing_subscriber::EnvFilter)) -> crate::Result<()> { - self.0.modify(f)?; - Ok(()) + fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &tracing::span::Id, ctx: tracing_subscriber::layer::Context<'_, Registry>) { + for layer in &self.0 { + layer.on_new_span(attrs, id, ctx.clone()); + } + } + + fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber::layer::Context<'_, Registry>) { + for layer in &self.0 { + layer.on_event(event, ctx.clone()); + } } -} -impl From