bob/
logging.rs

1/*
2 * Copyright (c) 2025 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17/*
18 * Logging infrastructure, outputs bunyan-style JSON to a logs directory.
19 */
20
21use anyhow::{Context, Result};
22use std::fs;
23use std::path::PathBuf;
24use std::sync::OnceLock;
25use tracing_appender::non_blocking::WorkerGuard;
26use tracing_subscriber::{
27    EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt,
28};
29
30static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
31
32/// Initialize the logging system.
33/// Creates a logs directory and writes JSON-formatted logs there.
34/// Also logs to stderr if verbose mode is enabled.
35pub fn init(logs_dir: &PathBuf, verbose: bool) -> Result<()> {
36    // Create logs directory
37    fs::create_dir_all(logs_dir).with_context(|| {
38        format!("Failed to create logs directory {:?}", logs_dir)
39    })?;
40
41    // Create a rolling file appender that writes to logs/bob.log
42    let file_appender = tracing_appender::rolling::never(logs_dir, "bob.log");
43    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
44
45    // Store the guard to keep the writer alive
46    LOG_GUARD
47        .set(guard)
48        .map_err(|_| anyhow::anyhow!("Logging already initialized"))?;
49
50    // Build the subscriber with JSON formatting for files
51    let file_layer = fmt::layer()
52        .json()
53        .with_writer(non_blocking)
54        .with_target(true)
55        .with_thread_ids(true)
56        .with_thread_names(true)
57        .with_file(true)
58        .with_line_number(true);
59
60    // Set up env filter - allow RUST_LOG to override
61    // verbose mode uses debug level, otherwise info level
62    let default_filter = if verbose { "bob=debug" } else { "bob=info" };
63    let filter = EnvFilter::try_from_default_env()
64        .unwrap_or_else(|_| EnvFilter::new(default_filter));
65
66    tracing_subscriber::registry().with(filter).with(file_layer).init();
67
68    tracing::info!(logs_dir = %logs_dir.display(),
69        verbose = verbose,
70        "Logging initialized"
71    );
72
73    Ok(())
74}