Skip to main content

wowlab_node/utils/
logging.rs

1use tokio::sync::mpsc;
2use tracing_subscriber::{Layer, layer::SubscriberExt, util::SubscriberInitExt};
3use wowlab_common::{ProjectIdentity, data_dir};
4use wowlab_fs::{directory, path::PathBuf};
5
6use crate::LogLevel;
7
8const UI_LOG_CHANNEL_SIZE: usize = 100;
9
10#[must_use]
11pub fn log_dir() -> Option<PathBuf> {
12    data_dir(
13        "NODE",
14        ProjectIdentity {
15            qualifier: "gg",
16            organization: "wowlab",
17            application: "wowlab-node",
18        },
19    )
20    .map(|directory| directory.join("logs"))
21}
22
23#[derive(Clone, Debug)]
24pub struct UiLogEntry {
25    pub level: LogLevel,
26    pub message: String,
27}
28
29#[derive(Debug)]
30pub struct LoggingGuard {
31    _file: Option<tracing_appender::non_blocking::WorkerGuard>,
32}
33
34fn file_output() -> Option<(
35    tracing_appender::non_blocking::NonBlocking,
36    tracing_appender::non_blocking::WorkerGuard,
37)> {
38    let directory = log_dir()?;
39
40    directory::ensure(&directory).ok()?;
41    let appender = tracing_appender::rolling::daily(&directory, "node.log");
42    let output = tracing_appender::non_blocking(appender);
43
44    tracing::debug!(?directory, "Configured node log directory");
45
46    Some(output)
47}
48
49/// Initializes UI logging and returns its lifetime guard and event receiver.
50///
51/// # Panics
52///
53/// Panics if a built-in tracing directive is invalid or a global subscriber has already been installed.
54#[must_use]
55pub fn init_ui_logging() -> (LoggingGuard, mpsc::Receiver<UiLogEntry>) {
56    let (sender, receiver) = mpsc::channel(UI_LOG_CHANNEL_SIZE);
57    let (writer, file_guard) = file_output().unzip();
58    let file_layer = writer.map(|writer| {
59        tracing_subscriber::fmt::layer()
60            .with_ansi(false)
61            .with_target(true)
62            .with_writer(writer)
63    });
64    let env_filter = tracing_subscriber::EnvFilter::builder()
65        .with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
66        .from_env_lossy()
67        .add_directive("eframe=warn".parse().expect("valid directive"))
68        .add_directive("egui=warn".parse().expect("valid directive"));
69
70    tracing_subscriber::registry()
71        .with(env_filter)
72        .with(tracing_subscriber::fmt::layer().with_target(true))
73        .with(UiLayer { sender })
74        .with(file_layer)
75        .init();
76
77    (LoggingGuard { _file: file_guard }, receiver)
78}
79
80#[must_use]
81pub fn init_headless_logging() -> LoggingGuard {
82    let (writer, file_guard) = file_output().unzip();
83    let file_layer = writer.map(|writer| {
84        tracing_subscriber::fmt::layer()
85            .with_ansi(false)
86            .with_target(true)
87            .with_writer(writer)
88    });
89    let env_filter = tracing_subscriber::EnvFilter::builder()
90        .with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
91        .from_env_lossy();
92
93    tracing_subscriber::registry()
94        .with(env_filter)
95        .with(tracing_subscriber::fmt::layer().with_target(true))
96        .with(file_layer)
97        .init();
98
99    LoggingGuard { _file: file_guard }
100}
101
102struct UiLayer {
103    sender: mpsc::Sender<UiLogEntry>,
104}
105
106impl<S> Layer<S> for UiLayer
107where
108    S: tracing::Subscriber,
109{
110    fn on_event(
111        &self,
112        event: &tracing::Event<'_>,
113        _context: tracing_subscriber::layer::Context<'_, S>,
114    ) {
115        let level = match *event.metadata().level() {
116            tracing::Level::ERROR => LogLevel::Error,
117            tracing::Level::WARN => LogLevel::Warn,
118            tracing::Level::DEBUG | tracing::Level::TRACE => LogLevel::Debug,
119            tracing::Level::INFO => LogLevel::Info,
120        };
121        let mut visitor = MessageVisitor::default();
122
123        event.record(&mut visitor);
124        let _ = self.sender.try_send(UiLogEntry {
125            level,
126            message: visitor.message.unwrap_or_default(),
127        });
128    }
129}
130
131#[derive(Default)]
132struct MessageVisitor {
133    message: Option<String>,
134}
135
136impl tracing::field::Visit for MessageVisitor {
137    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
138        if field.name() == "message" {
139            self.message = Some(format!("{value:?}"));
140        }
141    }
142
143    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
144        if field.name() == "message" {
145            self.message = Some(value.to_string());
146        }
147    }
148}