forked from kent/consciousness
146 lines
4.8 KiB
Rust
146 lines
4.8 KiB
Rust
|
|
// logging.rs — log-crate logger that routes by target.
|
||
|
|
//
|
||
|
|
// Records with target "grpc" (or any target starting with "grpc::") go
|
||
|
|
// to ~/.consciousness/logs/daemon/grpc.log so we can tell gRPC events
|
||
|
|
// apart from the rest of consciousness's noise. Everything else goes
|
||
|
|
// to ~/.consciousness/logs/daemon/debug.log.
|
||
|
|
//
|
||
|
|
// Level threshold is taken from RUST_LOG (simple global level parse:
|
||
|
|
// "trace"/"debug"/"info"/"warn"/"error"); defaults to "info".
|
||
|
|
|
||
|
|
use std::io::Write;
|
||
|
|
use std::path::PathBuf;
|
||
|
|
use std::sync::Mutex;
|
||
|
|
|
||
|
|
use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
|
||
|
|
|
||
|
|
fn logs_dir() -> PathBuf {
|
||
|
|
dirs::home_dir().unwrap_or_default().join(".consciousness/logs/daemon")
|
||
|
|
}
|
||
|
|
|
||
|
|
struct RoutingLogger {
|
||
|
|
grpc_file: Mutex<Option<std::fs::File>>,
|
||
|
|
debug_file: Mutex<Option<std::fs::File>>,
|
||
|
|
level: LevelFilter,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl RoutingLogger {
|
||
|
|
fn new(level: LevelFilter) -> Self {
|
||
|
|
let dir = logs_dir();
|
||
|
|
let _ = std::fs::create_dir_all(&dir);
|
||
|
|
let grpc = std::fs::OpenOptions::new()
|
||
|
|
.create(true).append(true)
|
||
|
|
.open(dir.join("grpc.log")).ok();
|
||
|
|
let debug = std::fs::OpenOptions::new()
|
||
|
|
.create(true).append(true)
|
||
|
|
.open(dir.join("debug.log")).ok();
|
||
|
|
Self {
|
||
|
|
grpc_file: Mutex::new(grpc),
|
||
|
|
debug_file: Mutex::new(debug),
|
||
|
|
level,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_grpc_target(target: &str) -> bool {
|
||
|
|
target == "grpc" || target.starts_with("grpc::")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Log for RoutingLogger {
|
||
|
|
fn enabled(&self, m: &Metadata) -> bool {
|
||
|
|
// Always enable DEBUG for grpc target so the dedicated log is
|
||
|
|
// actually useful without RUST_LOG wrangling; defer to the
|
||
|
|
// configured level for everything else.
|
||
|
|
if Self::is_grpc_target(m.target()) {
|
||
|
|
return m.level() <= Level::Debug;
|
||
|
|
}
|
||
|
|
m.level() <= self.level
|
||
|
|
}
|
||
|
|
|
||
|
|
fn log(&self, record: &Record) {
|
||
|
|
if !self.enabled(record.metadata()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let line = format!(
|
||
|
|
"[{}] [{}] [{}] {}\n",
|
||
|
|
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"),
|
||
|
|
record.level(),
|
||
|
|
record.target(),
|
||
|
|
record.args(),
|
||
|
|
);
|
||
|
|
let slot = if Self::is_grpc_target(record.target()) {
|
||
|
|
&self.grpc_file
|
||
|
|
} else {
|
||
|
|
&self.debug_file
|
||
|
|
};
|
||
|
|
if let Ok(mut guard) = slot.lock() {
|
||
|
|
if let Some(ref mut f) = *guard {
|
||
|
|
let _ = f.write_all(line.as_bytes());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn flush(&self) {
|
||
|
|
for slot in [&self.grpc_file, &self.debug_file] {
|
||
|
|
if let Ok(mut g) = slot.lock() {
|
||
|
|
if let Some(ref mut f) = *g {
|
||
|
|
let _ = f.flush();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_level_from_env() -> LevelFilter {
|
||
|
|
let raw = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
|
||
|
|
// Parse a plain level word; if it's the module=level form, we take
|
||
|
|
// the first level we find.
|
||
|
|
let token = raw.split(',').next().unwrap_or("info");
|
||
|
|
let level_word = token.rsplit_once('=').map(|(_, v)| v).unwrap_or(token);
|
||
|
|
match level_word.trim().to_lowercase().as_str() {
|
||
|
|
"trace" => LevelFilter::Trace,
|
||
|
|
"debug" => LevelFilter::Debug,
|
||
|
|
"info" => LevelFilter::Info,
|
||
|
|
"warn" => LevelFilter::Warn,
|
||
|
|
"error" => LevelFilter::Error,
|
||
|
|
"off" => LevelFilter::Off,
|
||
|
|
_ => LevelFilter::Info,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Install the routing logger. Safe to call at most once — subsequent
|
||
|
|
/// calls return an error but are otherwise no-ops.
|
||
|
|
pub fn init() -> Result<(), SetLoggerError> {
|
||
|
|
let level = parse_level_from_env();
|
||
|
|
let logger = Box::new(RoutingLogger::new(level));
|
||
|
|
log::set_boxed_logger(logger)?;
|
||
|
|
// Always let DEBUG records through globally so the grpc log can
|
||
|
|
// capture them (the logger itself filters non-grpc targets by
|
||
|
|
// `level`). The cost is that log::debug! call-sites below `level`
|
||
|
|
// in other modules still do their arg formatting before being
|
||
|
|
// dropped at the logger; acceptable for a debug tool.
|
||
|
|
log::set_max_level(LevelFilter::Debug.max(level));
|
||
|
|
// Mark the file with a session boundary so it's easy to see where a
|
||
|
|
// restart happened.
|
||
|
|
log::info!(
|
||
|
|
"===== consciousness logger init (level={}, pid={}) =====",
|
||
|
|
level, std::process::id(),
|
||
|
|
);
|
||
|
|
log::info!(target: "grpc",
|
||
|
|
"===== grpc log init (level={}, pid={}) =====",
|
||
|
|
level, std::process::id(),
|
||
|
|
);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Consumer of &Level so the type is used when only some callers want it.
|
||
|
|
#[allow(dead_code)]
|
||
|
|
pub fn current_level() -> Level {
|
||
|
|
match log::max_level() {
|
||
|
|
LevelFilter::Trace => Level::Trace,
|
||
|
|
LevelFilter::Debug => Level::Debug,
|
||
|
|
LevelFilter::Info | LevelFilter::Off => Level::Info,
|
||
|
|
LevelFilter::Warn => Level::Warn,
|
||
|
|
LevelFilter::Error => Level::Error,
|
||
|
|
}
|
||
|
|
}
|