1use crate::action::Action;
2use crate::comm::QWriter;
3use crate::server::{AsyncSignal, Config, Info};
4use chrono::{DateTime, Local};
5use eyre::{eyre, Context, Error, Result};
6use itertools::Itertools;
7use ron::ser::PrettyConfig;
8use serde::{Deserialize, Serialize, Serializer};
9use serde_cbor::{from_slice, Value};
10use std::collections::HashMap;
11use std::fmt::Debug;
12use std::fs::{create_dir_all, File};
13use std::io::Write;
14use std::path::PathBuf;
15use std::time::Duration;
16use std::{fs, thread};
17
18pub const TAG_INFO: u64 = 0x01;
19pub const TAG_CONFIG: u64 = 0x02;
20pub const TAG_ACTION: u64 = 0x03;
21
22pub type LogGroup = (Vec<(String, String, Value)>, bool);
23
24#[derive(Debug, Default)]
25pub struct Logger {
26 out_dir: PathBuf,
27 content: HashMap<String, LogGroup>,
28 needs_flush: bool,
29 log_format: LogFormat,
30}
31
32#[derive(Debug, Clone)]
33pub enum LoggerSignal {
34 Append(String, (String, Value)),
35 Extend(String, Vec<(String, Value)>),
36 Write(String, Value),
37 Flush,
38}
39
40#[derive(Debug, Copy, Clone, Deserialize, Serialize)]
41#[serde(rename_all = "lowercase")]
42pub enum LogFormat {
43 Inherit,
44 JSON,
45 YAML,
46 RON,
47}
48
49impl Default for LogFormat {
50 #[inline(always)]
51 fn default() -> Self {
52 LogFormat::Inherit
53 }
54}
55
56impl LogFormat {
57 pub fn or(&self, other: &Self) -> Self {
58 if let Self::Inherit = self {
59 *other
60 } else {
61 *self
62 }
63 }
64}
65
66impl LoggerSignal {
67 #[inline(always)]
68 fn requires_flush(&self) -> bool {
69 matches!(
70 self,
71 LoggerSignal::Append(_, _) | LoggerSignal::Extend(_, _)
72 )
73 }
74}
75
76impl From<LoggerSignal> for AsyncSignal {
77 fn from(signal: LoggerSignal) -> Self {
78 AsyncSignal::Logger(Local::now(), signal)
79 }
80}
81
82impl Logger {
83 pub fn new(info: &Info, config: &Config) -> Result<Self> {
84 let block = normalized_name(info.block());
85 let date = Local::now().format("%F").to_string();
86 let time = Local::now().format("%T").to_string().replace(':', "-");
87 let out_dir = info.output().join(format!("{date}/{block}/{time}"));
88
89 if out_dir.exists() {
90 return Err(eyre!("Output directory already exists: {out_dir:?}"));
91 }
92 create_dir_all(&out_dir)
93 .wrap_err_with(|| format!("Failed to create output directory: {out_dir:?}"))?;
94
95 Ok(Self {
96 out_dir,
97 content: HashMap::new(),
98 needs_flush: false,
99 log_format: config.log_format(),
100 })
101 }
102
103 fn append(&mut self, time: DateTime<Local>, group: String, entry: (String, Value)) {
104 let time = time.to_string();
105 let (name, value) = entry;
106 let (vec, flush) = self.content.entry(group).or_default();
107 vec.push((time, name, value));
108 *flush = true;
109 self.needs_flush = true;
110 }
111
112 fn extend(&mut self, time: DateTime<Local>, group: String, entries: Vec<(String, Value)>) {
113 let time = time.to_string();
114 let (vec, flush) = self.content.entry(group).or_default();
115 vec.extend(
116 entries
117 .into_iter()
118 .map(|(name, value)| (time.clone(), name, value)),
119 );
120 *flush = true;
121 self.needs_flush = true;
122 }
123
124 fn write(&mut self, name: String, content: Value) -> Result<()> {
125 let name = format!("{}.log", normalized_name(&name));
126 let path = self.out_dir.join(name);
127
128 if let Value::Text(content) = content {
129 fs::write(&path, &content)
130 .wrap_err_with(|| format!("Failed to write to log file ({path:?})."))?;
131 } else {
132 let file = File::create(&path)
133 .wrap_err_with(|| format!("Failed to create log file ({path:?})."))?;
134 write_as(file, &content, self.log_format)
135 .wrap_err_with(|| format!("Failed to write to log file ({path:?})."))?;
136 }
137
138 #[cfg(debug_assertions)]
139 println!("{:?} -> Wrote to file: {path:?}", Local::now());
140 Ok(())
141 }
142
143 fn flush(&mut self) -> Result<()> {
144 for (group, (vec, flush)) in self.content.iter_mut().filter(|(_, (_, flush))| *flush) {
145 let name = format!("{}.log", normalized_name(group));
146 let path = self.out_dir.join(name);
147 let mut file = File::create(&path)
148 .wrap_err_with(|| format!("Failed to create log file ({path:?})."))?;
149
150 write_vec(&mut file, self.log_format, vec)?;
151 *flush = false;
152
153 #[cfg(debug_assertions)]
154 println!("{:?} -> Wrote to file: {path:?}", Local::now());
155 }
156 self.needs_flush = false;
157 Ok(())
158 }
159
160 pub fn update(
161 &mut self,
162 time: DateTime<Local>,
163 signal: LoggerSignal,
164 async_writer: &QWriter<AsyncSignal>,
165 ) -> Result<()> {
166 if signal.requires_flush() && !self.needs_flush {
167 let mut async_writer = async_writer.clone();
168 thread::spawn(move || {
169 thread::sleep(Duration::from_secs(5));
170 async_writer.push(LoggerSignal::Flush);
171 });
172 }
173
174 match signal {
175 LoggerSignal::Append(group, entry) => {
176 self.append(time, group, entry);
177 }
178 LoggerSignal::Extend(group, entries) => {
179 self.extend(time, group, entries);
180 }
181 LoggerSignal::Write(name, content) => {
182 self.write(name, content)?;
183 }
184 LoggerSignal::Flush => {
185 self.flush()?;
186 self.needs_flush = false;
187 }
188 }
189
190 Ok(())
191 }
192
193 pub fn finish(&mut self) -> Result<()> {
194 self.flush()
195 .wrap_err("Failed to graciously close logger.")?;
196
197 self.content.clear();
198 Ok(())
199 }
200}
201
202pub fn normalized_name(name: &str) -> String {
203 name.to_lowercase()
204 .split_whitespace()
205 .join("_")
206 .replace('-', "_")
207}
208
209fn write_vec(file: &mut File, fmt: LogFormat, vec: &Vec<(String, String, Value)>) -> Result<()> {
210 let mut vec_t: Vec<(&str, &str, Serializable)> = vec![];
211 for (a, b, v) in vec {
212 vec_t.push((a, b, Serializable::try_from(v)?));
213 }
214
215 write_as(file, &vec_t, fmt)
216}
217
218fn write_as<W, T>(mut file: W, content: &T, fmt: LogFormat) -> Result<()>
219where
220 W: Write,
221 T: ?Sized + Serialize,
222{
223 match fmt {
224 LogFormat::Inherit => Err(eyre!("Cannot log with log_format=`Inherit`.")),
225 LogFormat::JSON => {
226 serde_json::to_writer_pretty(file, &content).wrap_err("Failed to log JSON to file")
227 }
228 LogFormat::YAML => {
229 serde_yaml::to_writer(file, &content).wrap_err("Failed to log YAML to file")
230 }
231 LogFormat::RON => file
232 .write_all(
233 ron::ser::to_string_pretty(&content, PrettyConfig::default())
234 .wrap_err("Failed to serialize log as RON")?
235 .as_bytes(),
236 )
237 .wrap_err("Failed to log RON to file"),
238 }
239}
240
241enum Serializable<'a> {
242 Info(Info),
243 Config(Config),
244 Action(Box<dyn Action>),
245 Value(&'a Value),
246}
247
248impl<'a> Serialize for Serializable<'a> {
249 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
250 where
251 S: Serializer,
252 {
253 match self {
254 Serializable::Info(info) => info.serialize(serializer),
255 Serializable::Config(config) => config.serialize(serializer),
256 Serializable::Action(action) => action.serialize(serializer),
257 Serializable::Value(value) => value.serialize(serializer),
258 }
259 }
260}
261
262impl<'a> TryFrom<&'a Value> for Serializable<'a> {
263 type Error = Error;
264
265 fn try_from(v: &'a Value) -> Result<Self> {
266 Ok(match v {
267 Value::Tag(TAG_INFO, v) => Serializable::Info(match v.as_ref() {
268 Value::Bytes(v) => from_slice::<Info>(v).unwrap(),
269 _ => return Err(eyre!("Failed to deserialize Info struct")),
270 }),
271 Value::Tag(TAG_CONFIG, v) => Serializable::Config(match v.as_ref() {
272 Value::Bytes(v) => from_slice::<Config>(v).unwrap(),
273 _ => return Err(eyre!("Failed to deserialize Config struct",)),
274 }),
275 Value::Tag(TAG_ACTION, v) => Serializable::Action(match v.as_ref() {
276 Value::Bytes(v) => from_slice::<Box<dyn Action>>(v).unwrap(),
277 _ => return Err(eyre!("Failed to deserialize Action struct",)),
278 }),
279 v => Serializable::Value(v),
280 })
281 }
282}