Skip to content

Lind Logging System

1. Usage and Configuration

1.1 Quick Reference

The logging system is controlled by three environment variables set before launching lind-boot:

Variable Purpose Default
LIND_LOG_OUTPUT Where lind_log! output is written $LIND_WASM_ROOT/LIND.log (or /tmp/LIND.log)
LIND_LOG_CATEGORIES Which log categories are emitted default
LIND_DEBUG_PANIC Behavior of lind_debug_panic! panic-and-exit

1.2 Common Configurations

Development (default — nothing to set):

# Logs go to $LIND_WASM_ROOT/LIND.log, only Default category, panics on unexpected conditions.
make lind-boot
./build/lind-boot program.wasm

Print all logs to stderr and see every category:

LIND_LOG_OUTPUT=stderr LIND_LOG_CATEGORIES=all ./build/lind-boot program.wasm

Debug dynamic linking only:

LIND_LOG_OUTPUT=stderr LIND_LOG_CATEGORIES=dylink ./build/lind-boot program.wasm

Debug ThreeI syscall routing only:

LIND_LOG_OUTPUT=stderr LIND_LOG_CATEGORIES=threei ./build/lind-boot program.wasm

Debug both dynamic linking and ThreeI:

LIND_LOG_OUTPUT=stderr LIND_LOG_CATEGORIES=dylink,threei ./build/lind-boot program.wasm

Write logs to a custom file:

LIND_LOG_OUTPUT=/var/log/lind.log LIND_LOG_CATEGORIES=all ./build/lind-boot program.wasm
# or equivalently:
LIND_LOG_OUTPUT=file:/var/log/lind.log LIND_LOG_CATEGORIES=all ./build/lind-boot program.wasm

Deployment — best-effort continuation on unexpected conditions:

LIND_LOG_OUTPUT=/var/log/lind.log LIND_DEBUG_PANIC=log-only ./build/lind-boot program.wasm

CI — silence log noise but keep unexpected-condition detection as test failures:

LIND_LOG_OUTPUT=none LIND_DEBUG_PANIC=panic-and-exit ./build/lind-boot program.wasm

Maximum performance / benchmark — remove all logging code from the binary:

make lind-boot NO_LOGGING=1

1.3 Environment Variable Reference

LIND_LOG_OUTPUT

Controls where lind_log! output is written. Has no effect on whether lind_debug_panic! panics.

Value Behavior
stderr Write to standard error
stdout Write to standard output
none Discard all log output (does not suppress panics)
file:/path/to/file Append to file at path; created if absent
/path/to/file Same as file: prefix

LIND_LOG_CATEGORIES

Controls which categories of lind_log! messages are emitted. Messages from disabled categories are discarded before any formatting occurs.

Value Categories enabled
all All three categories
none No categories (silences all lind_log!)
default General / uncategorized messages
dylink Dynamic linking messages
threei ThreeI syscall routing messages
dylink,threei Any comma-separated combination

LIND_DEBUG_PANIC

Controls the behavior of lind_debug_panic!. Regardless of this setting, lind_debug_panic! does not use the log category filter — it always fires.

Value Behavior
panic or panic-and-exit Log the message then call panic!
log or log-only Log the message and return normally
none or no-action Return immediately without logging or panicking

1.4 Compile-Time Switch

When benchmarking or building for maximum performance, the entire logging system can be compiled out:

make lind-boot NO_LOGGING=1

With NO_LOGGING=1, both lind_log! and lind_debug_panic! expand to nothing. Formatting arguments are not evaluated — there is zero overhead at the call site.


2. The Three Diagnostic Levels

The logging system distinguishes three escalating levels of concern:

Is continued execution possible?
│
├─ No, always → panic!
│
└─ Yes, or maybe →
       Is this condition expected during normal operation?
       │
       ├─ Yes → lind_log!
       │
       └─ No — something that should never happen →
              lind_debug_panic!

lind_log! — General Diagnostic Output

Use for conditions that are informational or mildly suspicious but do not indicate a failure. Examples: reporting which code path was taken, noting that an optional resource was absent but a fallback succeeded, tracing intermediate values for debuggability.

lind_log!("cage {} started", cageid);
lind_log!(DYLINK, "resolved symbol {} at 0x{:x}", sym, addr);
lind_log!(THREEI, "registered handler for call {}", call_id);

lind_log! never panics. If the category is disabled or the output is None, it returns immediately without evaluating format arguments.

lind_debug_panic! — Soft Panic

Use for conditions that should never happen under correct operation and may indicate corrupted internal state, but where best-effort continuation is preferable to a hard crash in production.

lind_debug_panic!("signal_epoch_trigger: cage {} not found", cageid);

Does not take a category. Always fires regardless of the active LIND_LOG_CATEGORIES. Behavior is controlled by LIND_DEBUG_PANIC.

Call sites must always supply explicit fallback control flow, because lind_debug_panic! may return normally in log-only, no-action, and compiled-out modes:

// Good
let cage = match get_cage(cageid) {
    Some(c) => c,
    None => {
        lind_debug_panic!("cage {} not found", cageid);
        return; // required — macro may return normally
    }
};

// Bad — assumes lind_debug_panic! always diverges
let cage = get_cage(cageid).unwrap_or_else(|| {
    lind_debug_panic!("cage {} not found", cageid);
    unreachable!() // wrong
});

panic! — Unrecoverable Failure

Use only when continued execution is truly impossible — a lock is poisoned, a required resource is missing at boot, or an invariant that can never be violated has been violated. panic! is never softened by the logging system.


3. Log Categories

Category Token Scope
Default lind_log!(...) or lind_log!(Default, ...) Uncategorized Lind diagnostics
DYLINK lind_log!(DYLINK, ...) dlopen/dlsym/dlclose, GOT updates, symbol resolution, library replay, weak imports
THREEI lind_log!(THREEI, ...) Syscall routing, handler registration, inter-cage call routing, ThreeI interposition

Categories are checked before format arguments are evaluated. Disabled categories add no overhead beyond a single bitmask test.


4. Output Format

All log lines share a consistent prefix that includes the category, source file, line number, and Rust module path:

[LIND][DYLINK][src/dylink.rs:88 lind::dylink] resolved symbol malloc at 0x7f001000
[LIND][Default][src/cage.rs:42 lind::cage] cage 3 started
[LIND][THREEI][src/threei.rs:77 threei::threei] registered handler call_id=7

lind_debug_panic! uses a different prefix to make it easy to grep for unexpected conditions in logs:

# PanicAndExit mode — appears before the panic backtrace
[LIND][DEBUG PANIC][src/signal.rs:182 lind::signal] cage 3 not found

# LogOnly mode — execution continues after this line
[LIND][DEBUG PANIC continuing][src/signal.rs:182 lind::signal] cage 3 not found

5. Architecture

5.1 Compile-Time Gating

Both macros are defined in src/sysdefs/src/logging.rs and re-exported from the sysdefs crate root. The body of each macro is wrapped in #[cfg(feature = "lind-logging")]:

#[macro_export]
macro_rules! lind_log {
    (@cat $category:expr, $($arg:tt)*) => {
        {
            #[cfg(feature = "lind-logging")]
            {
                if $crate::logging::category_enabled($category) {
                    $crate::logging::log($category, format_args!($($arg)*), file!(), line!(), module_path!());
                }
            }
        }
    };
    // ... category dispatch arms
}

The #[cfg] is evaluated at the call site's crate, so every crate that uses the macros must declare and forward the feature:

# Cargo.toml of any crate that uses lind_log! or lind_debug_panic!
[features]
lind-logging = ["sysdefs/lind-logging"]

The root lind-boot crate activates lind-logging for the entire workspace. The Makefile handles this automatically and respects NO_LOGGING=1.

5.2 Global Logger State

The logger is stored in a OnceLock<LindLogger>:

static LIND_LOGGER: OnceLock<LindLogger> = OnceLock::new();

struct LindLogger {
    writer: Mutex<LogWriter>,      // output destination
    panic_behavior: PanicBehavior, // lind_debug_panic! behavior
    enabled_categories: LogCategorySet, // bitmask of active categories
}

OnceLock gives first-call-wins initialization with no locking overhead on subsequent reads. The Mutex<LogWriter> only serializes the actual write — the category check and feature gate happen before acquiring the lock.

If init_lind_logger is never called, all accesses fall through to a hardcoded default that writes to the file path derived from LIND_WASM_ROOT (or /tmp/LIND.log), panics on lind_debug_panic!, and enables only the Default category.

5.3 Initialization Sequence

init_lind_logger is called at the very top of main() in lind-boot, before CLI parsing, before chroot, and before rawposix_start. This ensures every subsequent log message — including those emitted during RawPOSIX or wasmtime initialization — picks up the environment-configured behavior:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = init_lind_logger(config_from_env().unwrap_or_default());
    // ... rest of startup
}

config_from_env() reads the three environment variables and builds a LindLoggerConfig. If the variable is absent, the corresponding field retains its default value. If the variable contains an unrecognised value, config_from_env() returns an error and unwrap_or_default() falls back to the compiled-in defaults silently.

5.4 Category Bitmask

LogCategorySet is a u8 bitmask — one bit per category. The check in category_enabled is a single AND operation:

const BIT_DEFAULT: u8 = 0b001;
const BIT_DYLINK:  u8 = 0b010;
const BIT_THREEI:  u8 = 0b100;

pub fn contains(&self, category: LogCategory) -> bool {
    self.0 & bit != 0
}

Format arguments are inside the if category_enabled(...) check, so they are not evaluated for disabled categories.

5.5 File Output

When LogOutput::File is selected, the file is opened once during init_lind_logger with OpenOptions::append(true).create(true) and the handle is stored inside the Mutex<LogWriter>. Every write_line call acquires the mutex, writes a single line, and releases it. The file descriptor is never closed and reopened per message.

5.6 LogOutput::None vs NO_LOGGING=1

These are two distinct suppression mechanisms:

LogOutput::None NO_LOGGING=1
When decided Runtime Compile time
lind_log! Suppressed (no output) Compiled out entirely
lind_debug_panic! logging Suppressed Compiled out entirely
lind_debug_panic! panic Still happens (if PanicAndExit) Compiled out
Use case CI — silence noise, keep panics as test failures Benchmarks, max-performance builds