openlatch-client 0.0.1

The open-source security layer for AI agents — client forwarder
Documentation
/// Daemon operational logging setup via tracing-subscriber with JSON file appender.
///
/// Provides structured JSON logging to `{log_dir}/daemon.log` for daemon
/// startup, shutdown, and runtime events.
use std::path::Path;

use tracing_appender::rolling;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

/// Initialize daemon operational logging.
///
/// Returns a `WorkerGuard` that MUST be held for the lifetime of the daemon.
/// Dropping the guard flushes all pending log writes to disk.
///
/// # Log output
///
/// Writes JSON-formatted log lines to `{log_dir}/daemon.log` (rolling daily).
///
/// # Log level
///
/// Controlled by the `OPENLATCH_LOG` environment variable (default: `"info"`).
///
/// # Examples
///
/// ```ignore
/// let log_dir = std::path::Path::new("/tmp/openlatch/logs");
/// let _guard = init_daemon_logging(log_dir);
/// // Guard must remain in scope for the daemon's lifetime.
/// ```
pub fn init_daemon_logging(log_dir: &Path) -> tracing_appender::non_blocking::WorkerGuard {
    let file_appender = rolling::daily(log_dir, "daemon.log");
    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);

    let env_filter =
        EnvFilter::try_from_env("OPENLATCH_LOG").unwrap_or_else(|_| EnvFilter::new("info"));

    tracing_subscriber::registry()
        .with(env_filter)
        .with(
            fmt::layer()
                .json()
                .with_writer(non_blocking)
                .with_target(true)
                .with_thread_ids(false)
                .with_file(false)
                .with_line_number(false),
        )
        .init();

    guard
}

/// Log the daemon startup event with structured fields (LOG-05).
///
/// Called by `daemon::start()` after successfully binding the TCP listener.
/// Fields include version, port, pid, and platform for operational visibility.
pub fn log_startup(version: &str, port: u16, pid: u32, os: &str, arch: &str) {
    tracing::info!(
        version = version,
        port = port,
        pid = pid,
        os = os,
        arch = arch,
        event = "daemon_startup",
        "openlatch daemon started"
    );
}

/// Log the daemon shutdown event with structured fields (LOG-05).
///
/// Called by the daemon before exiting. Records uptime and total events processed
/// for operational analysis and the user-facing shutdown summary (D-05).
pub fn log_shutdown(uptime_secs: u64, events_processed: u64) {
    tracing::info!(
        uptime_secs = uptime_secs,
        events_processed = events_processed,
        event = "daemon_shutdown",
        "openlatch daemon stopped"
    );
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    // Test 1: init_daemon_logging compiles and returns a WorkerGuard.
    // Note: tracing_subscriber::registry().init() is global per-process state.
    // This test verifies the return type signature rather than calling init(),
    // which would conflict with other test processes. Full integration is verified
    // in Plan 01-06 integration tests.
    #[test]
    #[ignore = "tracing subscriber is global state — verified in Plan 01-06 integration tests"]
    fn test_init_daemon_logging_returns_guard() {
        let dir = tempfile::TempDir::new().unwrap();
        let _guard: tracing_appender::non_blocking::WorkerGuard = init_daemon_logging(dir.path());
        // Guard type check — compile-time verification is sufficient here.
    }

    // Test 2: OPENLATCH_LOG env var is respected by EnvFilter.
    // Marked ignore because tracing subscriber is global state — calling init()
    // twice in the same process panics. Verified by integration tests in Plan 01-06.
    #[test]
    #[ignore = "tracing subscriber is global state — verified in Plan 01-06 integration tests"]
    fn test_openlatch_log_env_var_respected() {
        std::env::set_var("OPENLATCH_LOG", "debug");
        let dir = tempfile::TempDir::new().unwrap();
        let _guard = init_daemon_logging(dir.path());
        // If EnvFilter::try_from_env("OPENLATCH_LOG") fails, the default "info" is used.
        // Correct behavior: the filter level is "debug" when OPENLATCH_LOG=debug.
    }

    // Test 3: Default log level is INFO when OPENLATCH_LOG is not set.
    // Marked ignore for the same reason as Test 2.
    #[test]
    #[ignore = "tracing subscriber is global state — verified in Plan 01-06 integration tests"]
    fn test_default_log_level_is_info() {
        std::env::remove_var("OPENLATCH_LOG");
        let dir = tempfile::TempDir::new().unwrap();
        let _guard = init_daemon_logging(dir.path());
        // When OPENLATCH_LOG is unset, EnvFilter::try_from_env fails and falls back to "info".
    }

    // Compile-time check: log_startup and log_shutdown have the correct signatures.
    // These are validated by type-checking at compile time — no runtime assertion needed.
    #[test]
    fn test_log_helpers_compile() {
        // This test only verifies that the function signatures are correct.
        // Actual tracing output requires an initialized subscriber (Plan 01-06).
        let _ = std::hint::black_box(log_startup as fn(&str, u16, u32, &str, &str));
        let _ = std::hint::black_box(log_shutdown as fn(u64, u64));
    }
}