pipedash_plugin_api/logs/
tagged.rs1use 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;