openlatch-client 0.1.14

The open-source security layer for AI agents — client forwarder
/// 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,
    foreground: bool,
) -> 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"));

    let file_layer = fmt::layer()
        .json()
        .with_writer(non_blocking)
        .with_target(true)
        .with_thread_ids(false)
        .with_file(false)
        .with_line_number(false);

    let stderr_layer = foreground.then(|| {
        fmt::layer()
            .with_writer(std::io::stderr)
            .with_target(false)
            .with_thread_ids(false)
            .with_file(false)
            .with_line_number(false)
            .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
    });

    tracing_subscriber::registry()
        .with(env_filter)
        .with(file_layer)
        .with(stderr_layer)
        .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 effective status of the observability subsystems (telemetry +
/// crash reporting) at daemon startup. Operators need to see at a glance
/// whether the daemon will send anonymous events / panics upstream, and why
/// (consent file, env override, no key baked, …). Emitted once, right after
/// `log_startup`. Never logs DSNs or keys — only enabled/decided_by.
pub fn log_observability_status(
    telemetry_enabled: bool,
    telemetry_decided_by: &str,
    crash_report_enabled: bool,
    crash_report_decided_by: &str,
) {
    tracing::info!(
        telemetry_enabled = telemetry_enabled,
        telemetry_decided_by = telemetry_decided_by,
        crash_report_enabled = crash_report_enabled,
        crash_report_decided_by = crash_report_decided_by,
        event = "observability_status",
        "observability subsystems initialized"
    );
}

/// 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(), false);
        // 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(), false);
        // 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(), false);
        // 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));
    }
}