1use std::io::Write as _;
6use std::path::PathBuf;
7
8use serde_json::Value;
9
10const MAX_LOG_BYTES: u64 = 5 * 1024 * 1024;
11
12pub fn format_message(server: &str, params: &Value, time: &str) -> String {
15 let wrapped;
16 let message = if params.is_object() {
17 params
18 } else {
19 wrapped = serde_json::json!({ "data": params });
20 &wrapped
21 };
22 let level = message["level"].as_str().unwrap_or("info");
23 let logger = message["logger"]
24 .as_str()
25 .filter(|logger| !logger.is_empty())
26 .map(|logger| format!(" {logger}:"))
27 .unwrap_or_default();
28 let data = match &message["data"] {
29 Value::String(text) => text.clone(),
30 Value::Null if message.get("data").is_none() => "undefined".into(),
31 other => yapi_types::json::stringify(other),
32 };
33 let text = data.replace("\r\n", "\n").replace('\n', "\n ");
34 format!("{time} [{server}] {level}{logger} {text}\n")
35}
36
37#[derive(Clone)]
39pub struct Log {
40 path: PathBuf,
41}
42
43impl Log {
44 pub fn new(path: PathBuf) -> Log {
46 Log { path }
47 }
48
49 pub fn write(&self, server: &str, params: &Value) {
51 let line = format_message(server, params, &crate::time::now_iso());
52 if let Some(dir) = self.path.parent() {
53 let _ = std::fs::create_dir_all(dir);
54 }
55 let size = std::fs::metadata(&self.path).map_or(0, |meta| meta.len());
56 if size > MAX_LOG_BYTES {
57 let _ = std::fs::rename(&self.path, self.path.with_extension("log.1"));
58 }
59 if let Ok(mut file) = std::fs::OpenOptions::new()
60 .create(true)
61 .append(true)
62 .open(&self.path)
63 {
64 let _ = file.write_all(line.as_bytes());
65 }
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use serde_json::json;
73
74 #[test]
75 fn formats_like_pi() {
76 assert_eq!(
77 format_message(
78 "docs",
79 &json!({"level": "error", "logger": "db", "data": "a\nb"}),
80 "T"
81 ),
82 "T [docs] error db: a\n b\n"
83 );
84 assert_eq!(format_message("x", &json!(5), "T"), "T [x] info 5\n");
85 }
86}