Skip to main content

pipedash_plugin_api/logs/
tagged.rs

1use chrono::{
2    DateTime,
3    SecondsFormat,
4    Utc,
5};
6
7use super::{
8    LineMarker,
9    LogDialect,
10    ParsedLine,
11};
12
13const NO_TIMESTAMP: &str = "-";
14
15pub const TAGGED_MARKERS: &[(u8, LineMarker)] = &[
16    (b'.', LineMarker::Plain),
17    (b'!', LineMarker::Error),
18    (b'?', LineMarker::Warning),
19    (b'#', LineMarker::SectionStart { open: false }),
20    (b'+', LineMarker::SectionStart { open: true }),
21];
22
23pub struct TaggedLogDialect {
24    pub tags: &'static [(u8, LineMarker)],
25    pub infer_errors: bool,
26}
27
28fn split_tag<'a>(tags: &[(u8, LineMarker)], line: &'a [u8]) -> Option<(LineMarker, &'a [u8])> {
29    let (tag, rest) = line.split_first()?;
30    let (_, marker) = tags.iter().find(|(known, _)| known == tag)?;
31    Some((*marker, rest))
32}
33
34pub fn tag_of(tags: &[(u8, LineMarker)], marker: LineMarker) -> u8 {
35    tags.iter()
36        .find(|(_, known)| *known == marker)
37        .map_or(b'.', |(tag, _)| *tag)
38}
39
40impl LogDialect for TaggedLogDialect {
41    fn parse<'a>(&self, line: &'a [u8]) -> ParsedLine<'a> {
42        let Some((marker, rest)) = split_tag(self.tags, line) else {
43            return ParsedLine::plain(line);
44        };
45        let Some(space) = rest.iter().position(|b| *b == b' ') else {
46            return ParsedLine::plain(line);
47        };
48        let timestamp = std::str::from_utf8(&rest[..space])
49            .ok()
50            .and_then(crate::utils::parse_timestamp);
51        ParsedLine {
52            timestamp,
53            marker,
54            body: &rest[space + 1..],
55        }
56    }
57
58    fn infers_errors(&self) -> bool {
59        self.infer_errors
60    }
61}
62
63pub struct SpacedTagDialect {
64    pub tags: &'static [(u8, LineMarker)],
65}
66
67impl LogDialect for SpacedTagDialect {
68    fn parse<'a>(&self, line: &'a [u8]) -> ParsedLine<'a> {
69        let Some((marker, text)) = split_tag(self.tags, line)
70            .and_then(|(marker, rest)| Some((marker, rest.strip_prefix(b" ")?)))
71        else {
72            return ParsedLine::plain(line);
73        };
74        match marker {
75            LineMarker::Command => ParsedLine {
76                marker,
77                ..ParsedLine::plain(line)
78            },
79            _ => ParsedLine {
80                timestamp: None,
81                marker,
82                body: text,
83            },
84        }
85    }
86
87    fn infers_errors(&self) -> bool {
88        false
89    }
90}
91
92pub fn write_spaced_line(
93    out: &mut Vec<u8>, tags: &[(u8, LineMarker)], marker: LineMarker, line: &str,
94) {
95    let tag = tag_of(tags, marker);
96    let line = line.strip_suffix('\r').unwrap_or(line);
97    out.extend_from_slice(&[tag, b' ']);
98    for (index, segment) in line.split('\r').enumerate() {
99        if index > 0 {
100            out.extend_from_slice(&[b'\r', tag, b' ']);
101        }
102        out.extend_from_slice(segment.as_bytes());
103    }
104    out.push(b'\n');
105}
106
107pub fn write_line(out: &mut Vec<u8>, marker: LineMarker, stamp: Option<&str>, text: &str) {
108    let tag = tag_of(TAGGED_MARKERS, marker);
109    for line in text.split("\r\n").flat_map(|line| line.split(['\n', '\r'])) {
110        out.push(tag);
111        out.extend_from_slice(stamp.unwrap_or(NO_TIMESTAMP).as_bytes());
112        out.push(b' ');
113        out.extend_from_slice(line.as_bytes());
114        out.push(b'\n');
115    }
116}
117
118pub fn write_line_at(out: &mut Vec<u8>, marker: LineMarker, at: Option<DateTime<Utc>>, text: &str) {
119    let stamp = at.map(|at| at.to_rfc3339_opts(SecondsFormat::AutoSi, true));
120    write_line(out, marker, stamp.as_deref(), text);
121}
122
123pub trait LogEvent {
124    fn is_warning(&self) -> bool;
125
126    fn event_time(&self) -> Option<DateTime<Utc>>;
127
128    fn event_text(&self) -> String;
129}
130
131pub fn write_events<E: LogEvent>(out: &mut Vec<u8>, events: &[E], empty: &str) {
132    if events.is_empty() {
133        write_line(out, LineMarker::Plain, None, empty);
134    }
135    let mut events: Vec<_> = events.iter().collect();
136    events.sort_by_key(|event| event.event_time());
137    for event in events {
138        let marker = if event.is_warning() {
139            LineMarker::Warning
140        } else {
141            LineMarker::Plain
142        };
143        write_line_at(out, marker, event.event_time(), &event.event_text());
144    }
145}
146
147#[cfg(test)]
148mod tests;