Skip to main content

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}