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 let no_control_sequence = CONTROL_SEQUENCE_PATTERN.replace_all(s, "");
28 let mut split = no_control_sequence.split_whitespace();
29 let datetime = split
30 .next()
31 .and_then(|x| chrono::DateTime::parse_from_rfc3339(x).ok())
32 .context("no datetime")?;
33 let level: tracing::Level = split
34 .next()
35 .and_then(|x| x.parse().ok())
36 .context("no level")?;
37 let thread = split.next().context("no thread")?;
38 let path = split
39 .next()
40 .map(|x| x[..x.len() - 1].to_string())
41 .context("no path")?;
42
43 let line_number = split
44 .next()
45 .and_then(|x| x[..x.len() - 1].parse().ok())
46 .unwrap_or(0);
47 let message = split.collect::<Vec<&str>>().join(" ");
48 Ok(LogEntry {
49 datetime: datetime.timestamp_millis(),
50 level: level.to_string(),
51 thread: thread.to_string(),
52 path,
53 line_number,
54 message,
55 })
56 }
57}
58
59pub async fn get_log_entries(
60 path: impl AsRef<std::path::Path>,
61 limit: usize,
62) -> eyre::Result<Vec<LogEntry>> {
63 let file = std::fs::File::open(path.as_ref())?;
65 let lines = RevLines::new(file);
66 let mut entries = tokio::task::spawn_blocking(move || {
68 let mut entries = vec![];
69 for line in lines {
70 if entries.len() >= limit {
71 break;
72 }
73 let line = match line {
74 Ok(line) => line,
75 Err(error) => {
76 warn!("Error reading line: {:?}", error);
77 entries.push(LogEntry {
78 datetime: 0,
79 level: "".to_string(),
80 thread: "".to_string(),
81 path: "".to_string(),
82 line_number: 0,
83 message: error.to_string(),
84 });
85 break;
86 }
87 };
88 let entry = LogEntry::from_str(&line);
89 match entry {
90 Ok(entry) => entries.push(entry),
91 Err(_) => entries.push(LogEntry {
92 datetime: 0,
93 level: "".to_string(),
94 thread: "".to_string(),
95 path: "".to_string(),
96 line_number: 0,
97 message: line,
98 }),
99 }
100 }
101 entries
102 })
103 .await?;
104 if entries.is_empty() {
105 entries.push(LogEntry {
106 datetime: 0,
107 level: "".to_string(),
108 thread: "".to_string(),
109 path: "".to_string(),
110 line_number: 0,
111 message: format!("No entries found in {}", path.as_ref().display()),
112 });
113 }
114 Ok(entries)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 #[test]
121 fn log_entry_from_line() {
122 let line = "2024-05-18T14:26:36.709390Z WARN main trading_be: 290: terminated diff 0_table_limit_1 initially";
123 let entry = LogEntry::from_str(line).unwrap();
124 assert_eq!(entry.level, "WARN");
126 assert_eq!(entry.thread, "main");
127 assert_eq!(entry.path, "trading_be");
128 assert_eq!(entry.line_number, 290);
129 assert_eq!(entry.message, "terminated diff 0_table_limit_1 initially");
130 }
131 #[test]
132 fn log_entry_from_line_control_sequence() {
133 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";
134 let entry = LogEntry::from_str(line).unwrap();
135 assert_eq!(entry.level, "INFO");
137 assert_eq!(entry.thread, "main");
138 assert_eq!(entry.path, "trading_be::strategy::data_factory");
139 assert_eq!(entry.line_number, 110);
140 assert_eq!(entry.message, "terminated diff 0_table_limit_1 initially");
141 }
142
143 #[tokio::test]
144 async fn test_get_log_entries() {
145 use std::io::Write;
146 use tempfile::NamedTempFile;
147
148 let mut temp_file = NamedTempFile::new().unwrap();
149 writeln!(
150 temp_file,
151 "2025-03-19T12:34:56Z INFO thread-1 src/main.rs:42 Application started"
152 )
153 .unwrap();
154 writeln!(
155 temp_file,
156 "2025-03-19T12:35:01Z ERROR thread-2 src/lib.rs:128 Failed to connect"
157 )
158 .unwrap();
159
160 let entries = get_log_entries(temp_file.path(), 10).await.unwrap();
161 assert_eq!(entries.len(), 2);
162 assert_eq!(entries[0].level, "ERROR");
163 assert_eq!(entries[1].level, "INFO");
164 }
165}