byteflow/log.rs
1//! Host-side scheduler diagnostics on stderr, gated by `BYTEFLOW_LOG`.
2//!
3//! # Why this is separate from the `print` native
4//!
5//! Actor-visible logging (`CallNative print`) writes to **stdout** and is part
6//! of the program's observable behaviour — samples and demos use it to show
7//! envelopes crossing the mailbox. Scheduler diagnostics (spawn / send /
8//! park / finish) are an **operator** concern: they must not pollute stdout
9//! when an embedder pipes Flow output, and they must stay off by default
10//! so a production run is silent unless asked.
11//!
12//! ```text
13//! BYTEFLOW_LOG unset / 0 / off / false → silent
14//! BYTEFLOW_LOG=1 / info / true → spawn, send, receive, finish
15//! BYTEFLOW_LOG=debug / 2 → also park, deliver queued/handoff
16//! ```
17//!
18//! # Lazy init
19//!
20//! The level is read from the environment once (first call) and cached in
21//! atomics. That avoids re-parsing `std::env` on every send in a hot worker
22//! loop. Tests can override via [`set_level_for_tests`].
23
24use std::sync::atomic::{AtomicU8, Ordering};
25use std::time::{SystemTime, UNIX_EPOCH};
26
27const OFF: u8 = 0;
28const INFO: u8 = 1;
29const DEBUG: u8 = 2;
30
31static LEVEL: AtomicU8 = AtomicU8::new(OFF);
32static INIT: AtomicU8 = AtomicU8::new(0);
33
34fn level() -> u8 {
35 if INIT.load(Ordering::Acquire) == 0 {
36 let parsed = match std::env::var("BYTEFLOW_LOG") {
37 Ok(v) => {
38 let v = v.trim().to_ascii_lowercase();
39 match v.as_str() {
40 "" | "0" | "off" | "false" | "no" => OFF,
41 "debug" | "trace" | "2" => DEBUG,
42 // "1", "info", "true", "yes", or any other non-empty token
43 _ => INFO,
44 }
45 }
46 Err(_) => OFF,
47 };
48 LEVEL.store(parsed, Ordering::Release);
49 INIT.store(1, Ordering::Release);
50 }
51 LEVEL.load(Ordering::Acquire)
52}
53
54fn stamp_ms() -> u64 {
55 // Logging must never panic: a broken clock just prints epoch-ish 0.
56 SystemTime::now()
57 .duration_since(UNIX_EPOCH)
58 .map(|d| d.as_millis() as u64)
59 .unwrap_or(0)
60}
61
62/// Override the cached level (tests only). `0` off, `1` info, `2` debug.
63pub fn set_level_for_tests(level: u8) {
64 LEVEL.store(level.min(DEBUG), Ordering::Release);
65 INIT.store(1, Ordering::Release);
66}
67
68/// INFO-level line when [`BYTEFLOW_LOG`](crate::log) is at least `info`.
69#[inline]
70pub fn info(msg: impl std::fmt::Display) {
71 if level() >= INFO {
72 eprintln!("[{}] byteflow INFO {}", stamp_ms(), msg);
73 }
74}
75
76/// DEBUG-level line when [`BYTEFLOW_LOG`](crate::log) is `debug`.
77#[inline]
78pub fn debug(msg: impl std::fmt::Display) {
79 if level() >= DEBUG {
80 eprintln!("[{}] byteflow DEBUG {}", stamp_ms(), msg);
81 }
82}