Logging
Day emits through log, the ecosystem’s logging facade. Write a line with
the macros the prelude already gives you, and it comes out on every platform with no setup:
use day::prelude::*;
info!("importing {} rows", rows.len());
warn!("font {name:?} is missing — falling back to the system font");
error!("the database is unreadable: {e}");
debug!("hit test at {x},{y} -> {hit:?}");
trace!("frame {n}");
There is nothing to initialize. day::launch installs a logger before the first line can be
emitted, so the framework’s own diagnostics and yours land in the same place, in the same format.
Why not println!
Because it does not work everywhere, and where it fails it fails silently.
On wasm32-unknown-unknown — the target web-dom builds — Rust’s standard library has no
stdout to write to. Its implementation accepts your bytes and drops them:
// library/std/src/sys/stdio/unsupported.rs
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
Ok(buf.len()) // accepted, then discarded
}
The call reports success and writes nothing. An app whose diagnostics are println! is mute in
a browser, and there is no fd to redirect the way Android redirects fd 1 and 2 into logcat.
println! has a second problem even on native targets: it panics when the write fails, and a
closed stderr pipe is routine when day launch tears an app down. A panic raised inside a native
trampoline (an event callback, a GCD or glib block) unwinds into non-Rust frames and aborts the
process, turning a clean exit into a spurious crash. Day’s logger never panics on a failed write.
Where the lines go
| Target | Sink |
|---|---|
| macOS, Linux, Windows | the process’s stderr — your terminal |
| iOS | stderr, which is the Xcode console |
| Android | stderr, which day-android redirects into logcat (stdout at INFO, stderr at ERROR) |
| web-dom | the browser’s JavaScript console |
On the web each level maps to the matching console method (error! to console.error, warn!
to console.warn, info! to console.info, debug! and trace! to console.debug), so
devtools’ own level filter applies to Day’s output the way it does to the page’s.
Lines are formatted LEVEL target: message:
INFO my_app: importing 412 rows
WARN day_core::nav: .restore("app.section") has no NavStore installed — …
The target names the crate and module that emitted the line, which tells you whether a complaint is yours, a piece’s, or a backend’s.
Color
On a terminal the level column is colored: red ERROR, yellow WARN, green INFO, blue
DEBUG, cyan TRACE. That is env_logger’s palette, so the colors carry the meaning they
already have elsewhere in Rust. The message stays plain: color across a whole line is noise at
INFO, and an uncolored message stays greppable.
The escapes are written only where they render. Day checks whether the destination is a color
terminal, so color drops out on its own when output is redirected to a file, when NO_COLOR or
TERM=dumb is set, and on the targets where no terminal is involved (logcat, Xcode’s console,
the browser). On Windows it enables the console’s VT processing first, so older
consoles show color rather than raw escapes.
Under day launch the app’s stderr is a pipe, so the app itself writes none. day-cli colors
the lines as it re-emits them, tagged with the target they came from:
[macos-appkit] ERROR my_app: the database is unreadable
[macos-appkit] INFO my_app: importing 412 rows
[macos-appkit] DEBUG day_core::nav: restoring app.section
The prefix takes the level’s color too, so scanning the left column finds the errors.
The VS Code extension’s Run button shows exactly this, and needed no change of its own to get
it: it runs day launch as a VS Code task in an integrated terminal, which is a terminal like any
other. Its debug path (F5) spawns day launch with pipes and forwards the text to the Debug
Console, so the color is stripped there and the output is plain.
day.extraEnv does not change that. Those entries are passed through as day launch --env,
which sets the environment of the app, not of the CLI doing the coloring.
A forwarded line with no level keeps the older per-stream color, blue for stdout and yellow for
stderr: a stray println!, a Qt warning, a raw logcat line. Colored output is a presentation
choice, so it never changes routing; an ERROR an app wrote to stdout is still forwarded to
stdout.
Choosing a level
| Level | Use it for | On by default |
|---|---|---|
error! | a feature is gone: the window could not be created, the engine never bound | yes |
warn! | Day recovered, but not as intended: a missing font, a piece with no renderer on this backend, a placeholder substituted | yes |
info! | milestones a user or operator would want in a report: startup, a completed import | yes |
debug! | the trace you want while working on something — a malformed field, a layout decision | debug builds |
trace! | per-frame or per-event firehose | no |
The default maximum level is Debug in a debug build and Info in a release build.
Turning levels up
DAY_LOG takes a level name — off, error, warn, info, debug, trace:
DAY_LOG=debug day launch -p macos-appkit
The web has no process environment, so the launch server forwards it as a query parameter on the
page URL; day launch -p web-dom --env DAY_LOG=debug is the same thing. An app can also move the
level at runtime with day::set_log_level(log::LevelFilter::Trace).
Using a different logger
Day’s logger is only a default. log allows exactly one logger per process and the first
registration wins, so install yours before day::launch and Day steps aside:
fn main() {
env_logger::init(); // or tracing_subscriber, or your own log::Log
day::launch(day::WindowOptions::default(), my_app::root);
}
There is nothing else to configure. Day calls log::set_logger and ignores the Err that says
someone got there first, and every info! already written keeps working because the macros are
log’s.
Two consequences follow.
- Per-target filtering comes with the logger you choose. Day’s default has a single global
level;
env_loggergives youRUST_LOG=warn,day_uikit=debugand the ability to silence one noisy backend. tracingusers can bridge withtracing-logand receive Day’s output as tracing events.
env_logger as the logger
env_logger suits a desktop-focused app, and it is a two-line change:
# Cargo.toml
[dependencies]
env_logger = "0.11"
fn main() {
// Before `day::launch`, or Day's logger wins the race and this call does nothing.
env_logger::init();
day::launch(day::WindowOptions::default(), my_app::root);
}
The per-target filter is the main reason to switch. It turns one backend up without the rest of the app coming along:
RUST_LOG=warn,day_gtk=debug,my_app=trace day launch -p linux-gtk
RUST_LOG replaces DAY_LOG once you do this. DAY_LOG is read by Day’s logger, and that
logger is no longer installed.
Day does not adopt env_logger as its default, because it writes ANSI text to stderr on every
target. On Android that means every line lands in logcat at ERROR, because day-android maps
fd 2 to that level; on web-dom it means the lines are discarded, since std’s stdio on wasm
accepts bytes and drops them; on iOS it means escape codes in the Xcode console. Day’s default
exists to route per platform. env_logger fits an app that targets desktops; the other six
targets need the per-platform routing.
Writing a logger
The trait is log::Log. Day’s web-dom implementation shows the shape:
pub fn console_sink(level: log::Level, line: &str) {
unsafe { day_dom_log(level as u32, line.as_ptr(), line.len()) };
}
A backend that needs its own destination installs a sink, a fn(log::Level, &str) that
receives the already-formatted line, through day_core::set_log_sink instead of replacing the
logger. That keeps the format and the level filtering in one place. The facade wires web-dom’s in
day::web::start.
Day does not ship console_log or wasm-logger for the browser: each requires web-sys (and
wasm-logger requires wasm-bindgen), while the web-dom backend’s shim is numeric ids across
extern "C", built without a bundler (docs/web.md). Routing a formatted line to
console.* is one call, so the dependency would add a toolchain for no gain.
What Day itself logs
Every framework diagnostic goes through the same macros, so DAY_LOG=debug shows you Day’s
reasoning alongside your own: a piece with no renderer on the current backend, a route that did
not match, a bundled font the toolkit refused, a nav stack whose bar actions were dropped because
it merged into an enclosing one. Those were eprintln! before and are warn! now, which means
they are filterable, they carry the emitting module, and they reach the browser console.