wowlab_node/utils/
logging.rs1use 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#[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}