endpoint_libs/libs/
log_reader.rs

1use core::str::FromStr;
2use eyre::ContextCompat;
3use lazy_static::lazy_static;
4use regex::Regex;
5use rev_lines::RevLines;
6use tracing::warn;
7
8// Define a struct to hold the parts of the log entry
9
10#[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    // Specify the path to your log file
54    let file = std::fs::File::open(path.as_ref())?;
55    let mut lines = RevLines::new(file);
56    // get all entries first
57    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.datetime, "2024-02-09 18:10:46");
115        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.datetime, "2024-02-09 18:10:46");
126        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    // #[allow(dead_code)]
134    async fn log_entry_from_file() {
135        // NOTE use some valid file when needed
136        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}