endpoint_libs/libs/
log_reader.rs1use core::str::FromStr;
2use eyre::ContextCompat;
3use lazy_static::lazy_static;
4use regex::Regex;
5use rev_lines::RevLines;
6use tracing::warn;
7
8#[derive(Debug, Clone)]
11pub struct LogEntry {
12 pub datetime: i64,
13 pub level: String,
14 pub thread: String,
15 pub path: String,
16 pub line_number: usize,
17 pub message: String,
18}
19
20lazy_static! {
21 static ref CONTROL_SEQUENCE_PATTERN: Regex = Regex::new("\x1b\\[[0-9;]*m").unwrap();
22}
23
24impl FromStr for LogEntry {
25 type Err = eyre::Error;
26 fn from_str(s: &str) -> Result<Self, Self::Err> {
27 println!("before: {:?}", s);
28 let no_control_sequence = CONTROL_SEQUENCE_PATTERN.replace_all(s, "");
29 println!("after: {:?}", no_control_sequence);
30 let mut split = no_control_sequence.split_whitespace();
31 let datetime = split
32 .next()
33 .and_then(|x| chrono::DateTime::parse_from_rfc3339(x).ok())
34 .context("no datetime")?;
35 let level: tracing::Level = split.next().and_then(|x| x.parse().ok()).context("no level")?;
36 let thread = split.next().context("no thread")?;
37 let path = split.next().map(|x| x[..x.len() - 1].to_string()).context("no path")?;
38
39 let line_number = split.next().and_then(|x| x[..x.len() - 1].parse().ok()).unwrap_or(0);
40 let message = split.collect::<Vec<&str>>().join(" ");
41 Ok(LogEntry {
42 datetime: datetime.timestamp_millis(),
43 level: level.to_string(),
44 thread: thread.to_string(),
45 path,
46 line_number,
47 message,
48 })
49 }
50}
51
52pub async fn get_log_entries(path: impl AsRef<std::path::Path>, limit: usize) -> eyre::Result<Vec<LogEntry>> {
53 let file = std::fs::File::open(path.as_ref())?;
55 let mut lines = RevLines::new(file);
56 let mut entries = tokio::task::spawn_blocking(move || {
58 let mut entries = vec![];
59 while let Some(line) = lines.next() {
60 if entries.len() >= limit {
61 break;
62 }
63 let line = match line {
64 Ok(line) => line,
65 Err(error) => {
66 warn!("Error reading line: {:?}", error);
67 entries.push(LogEntry {
68 datetime: 0,
69 level: "".to_string(),
70 thread: "".to_string(),
71 path: "".to_string(),
72 line_number: 0,
73 message: error.to_string(),
74 });
75 break;
76 }
77 };
78 let entry = LogEntry::from_str(&line);
79 match entry {
80 Ok(entry) => entries.push(entry),
81 Err(_) => entries.push(LogEntry {
82 datetime: 0,
83 level: "".to_string(),
84 thread: "".to_string(),
85 path: "".to_string(),
86 line_number: 0,
87 message: line,
88 }),
89 }
90 }
91 entries
92 })
93 .await?;
94 if entries.is_empty() {
95 entries.push(LogEntry {
96 datetime: 0,
97 level: "".to_string(),
98 thread: "".to_string(),
99 path: "".to_string(),
100 line_number: 0,
101 message: format!("No entries found in {}", path.as_ref().display()),
102 });
103 }
104 Ok(entries)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 #[test]
111 fn log_entry_from_line() {
112 let line = "2024-05-18T14:26:36.709390Z WARN main trading_be: 290: terminated diff 0_table_limit_1 initially";
113 let entry = LogEntry::from_str(line).unwrap();
114 assert_eq!(entry.level, "WARN");
116 assert_eq!(entry.thread, "main");
117 assert_eq!(entry.path, "trading_be");
118 assert_eq!(entry.line_number, 290);
119 assert_eq!(entry.message, "terminated diff 0_table_limit_1 initially");
120 }
121 #[test]
122 fn log_entry_from_line_control_sequence() {
123 let line = "\u{1b}[2m2024-06-07T12:25:06.735143Z\u{1b}[0m \u{1b}[32m INFO\u{1b}[0m main \u{1b}[2mtrading_be::strategy::data_factory\u{1b}[0m\u{1b}[2m:\u{1b}[0m \u{1b}[2m110:\u{1b}[0m terminated diff 0_table_limit_1 initially";
124 let entry = LogEntry::from_str(line).unwrap();
125 assert_eq!(entry.level, "INFO");
127 assert_eq!(entry.thread, "main");
128 assert_eq!(entry.path, "trading_be::strategy::data_factory");
129 assert_eq!(entry.line_number, 110);
130 assert_eq!(entry.message, "terminated diff 0_table_limit_1 initially");
131 }
132 #[tokio::test]
133 async fn log_entry_from_file() {
135 let path = "/Users/jack/Dev/InsolventCapital/trading.insolvent.app-backend/log/user.log";
137 let entries = get_log_entries(path, 20).await.unwrap();
138 println!("{:?}", entries[0]);
139 assert_eq!(entries.len(), 20);
140 }
141}