# rustlog
A small, dependency-light logging crate with a pragmatic API, color (optional), groups, and a scope timer.
## Features at a glance
- **Macros:** `trace!`, `debug!`, `info!`, `warn!`, `error!`, `fatal!`
- **Extras:** `info_group!(group, ...)`, `scope_time!(label, { ... })`
- **Targets:** `Stdout`, `Stderr`, or a custom writer via `set_writer(...)` / `set_file(path)`
- **Runtime toggles:** show time, thread id, file\:line, group
- **Color (optional):** `Always` / `Never` / `Auto` (TTY detection for Stdout/Stderr)
- **Env config:** `RUST_LOG_LEVEL`, `RUST_LOG_COLOR`, `RUST_LOG_SHOW_TID`, `RUST_LOG_SHOW_TIME`
- **Compile-time floor:** `debug` includes `trace`, `release` may strip `trace`/`debug`
> **MSRV:** Rust **1.70+** (uses `OnceLock` and `std::io::IsTerminal`).
---
## Install
```toml
[dependencies]
rustlog = "x.x"
```
### Feature flags
- `color` — ANSI colors; `Auto` uses TTY detection for Stdout/Stderr
- `timestamp` — prepend timestamp to each line
- `localtime` *(optional, only if you enable it)* — with `timestamp`, format local time instead of UTC
- `thread-id` — include thread id when enabled at runtime
> If you don’t enable `color`, output never contains ANSI escapes.
---
## Quick start
```rust
use rustlog::*;
fn main() {
// Choose output early; first call wins (set-once semantics).
set_target(Target::Stderr); // default if unset
// set_file("/var/log/app.log").unwrap(); // or write to a file
// Configure runtime toggles
set_show_time(true); // requires `timestamp` feature
set_show_thread_id(false); // requires `thread-id` feature
set_show_file_line(true);
// Runtime level (compile-time floor still applies)
set_level(Level::Info);
info!("hello {}", 42);
warn!("heads up");
info_group!("net", "retry #{}", 3);
scope_time!("startup", {
// work …
}); // logs "took …" when the scope ends
}
```
Typical output (UTC timestamp shown when `timestamp` is enabled; colors elided):
```
2025-09-25 12:34:56.789Z INFO <main.rs:15> hello 42
2025-09-25 12:34:56.790Z WARN <main.rs:16> heads up
2025-09-25 12:34:56.791Z INFO <main.rs:19> [net] retry #3
2025-09-25 12:34:56.792Z INFO <main.rs:22> [startup] took 1.234 ms
```
---
## Targets
Targets are **set once** for the process (internally `OnceLock`). Set them at program start.
```rust
set_target(Target::Stdout);
set_target(Target::Stderr); // default
set_file("app.log").unwrap(); // convenience: opens/creates + selects `Writer`
// Custom sink (useful in tests):
use std::io::Write;
struct Mem(Vec<u8>);
impl Write for Mem {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> { self.0.extend_from_slice(b); Ok(b.len()) }
fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
}
set_writer(Box::new(Mem(Vec::new())));
set_target(Target::Writer);
```
> With `ColorMode::Auto`, `Writer` is treated as non-TTY (no color). Force color with `ColorMode::Always` if you control the sink.
---
## Levels & filtering
- **Macros:** `trace!`, `debug!`, `info!`, `warn!`, `error!`, `fatal!`
- **Compile-time floor:**
- `debug` builds include `trace`/`debug` code paths.
- `release` builds may compile out `trace`/`debug`; `info+` always remains.
- **Runtime filter:** `set_level(Level::Info)` etc.
A record is emitted if:
```
(level >= compile_time_min) && (level >= runtime_level)
```
---
## Groups & scope timer
```rust
info_group!("db", "query {}", "select 1");
scope_time!("init", { /* code */ }); // logs "took …" at drop
```
Duration formatting:
- `< 1_000 ns` → `NNN ns`
- `< 1_000_000 ns` → `NNN us`
- `< 1 s` → `M.us ms` (e.g. `1.234 ms`)
- `< 60 s` → `S.mmm s` (e.g. `1.234 s`)
- `< 3600 s` → `MmSS.mmm s` (e.g. `2m03.456s`)
- `< 24 h` → `HhMMmSS.mmm s` (e.g. `1h02m03.456s`)
- `≥ 24 h` → `Dd HHhMMmSS.mmm s`
---
## Colors (feature = `color`)
```rust
set_color_mode(ColorMode::Always); // force ANSI
set_color_mode(ColorMode::Never); // disable
set_color_mode(ColorMode::Auto); // Stdout/Stderr use TTY detect; Writer = no color
```
Env override (read by `init_from_env()`):
```bash
---
## Timestamps (feature = `timestamp`)
Enable at runtime:
```rust
set_show_time(true);
```
- **UTC** format (default): `YYYY-MM-DD HH:MM:SS.mmmZ`
- **Local time**: enable the `localtime` feature (if you turn it on in your build) to use the system local time.
> The UTC path uses a correct Gregorian conversion with no external deps.
---
## Thread id (feature = `thread-id`)
Enable at runtime:
```rust
set_show_thread_id(true);
```
---
## File\:line and group tag
```rust
set_show_file_line(true); // include `<file:line>`
// group tag is shown when you use info_group!(...) or scope_time!(label, ...)
```
---
## Application banner (app name & version)
Use the `banner!()` macro to print your app’s name and version as a single info-level line.
```rust
use rustlog::*;
fn main() {
set_target(Target::Stderr);
set_level(Level::Info);
banner!(); // -> "myapp v1.2.3"
}
```
### Customize name/version explicitly
If you don’t want to use Cargo metadata, pass strings directly:
```rust
banner!("myapp", "1.2.3");
```
`banner!()` is allocation-free and safe to call early during startup.
## Environment variables
Call `init_from_env()` once at startup to read these:
| `RUST_LOG_LEVEL` | `trace` `debug` `info` `warn` `error` `fatal` | Sets runtime level |
| `RUST_LOG_COLOR` | `always` `never` `auto` | Sets color mode |
| `RUST_LOG_SHOW_TID` | `1` `true` *(case-insensitive)* | Show thread id |
| `RUST_LOG_SHOW_TIME` | `1` `true` *(case-insensitive)* | Show timestamp |
Example:
```bash
RUST_LOG_LEVEL=debug RUST_LOG_COLOR=auto RUST_LOG_SHOW_TIME=1 cargo run
```
---
## Local Instance
Enable multiple logger instances with independent settings while keeping the root API (`rustlog::info!`, etc.) simple and unchanged for default usage.
```rust
rustlog::set_level(Level::Info);
rustlog::info!("default path");
// local instance
use rustlog::local::{Logger, LoggerBuilder};
use rustlog::local::info as linfo;
let lg = Logger::builder().file("trace.log").set_level(Level::Trace).build_static()?;
linfo!(&lg, "per-instance output");
```
## Testing tips
- To capture output in tests, install a memory writer and select `Target::Writer` **before** the first log in that test binary.
- Targets are set-once. Place target selection at the top of `main()` or in a per-test binary.
- Each log line is emitted with a single `write_all`, guarded by a mutex to avoid interleaving across threads.
---
## License
Dual-licensed under **MIT** or **Apache-2.0** at your option.
```
SPDX-License-Identifier: MIT OR Apache-2.0
```
If you contribute, you agree to license your contributions under the same terms.