1use std::fmt::Display;
9use std::path::PathBuf;
10use std::sync::{LazyLock, RwLock};
11
12use bytes::BytesMut;
13
14use super::{PrettyPrintable, Printable};
15
16#[derive(Debug, Clone)]
20pub struct SourcedLine<'a> {
21 pub parsed: Printable<'a>,
23 pub source_file: PathBuf,
25 pub line_number: usize,
27}
28
29impl<'a> SourcedLine<'a> {
30 pub fn new(parsed: Printable<'a>, source_file: PathBuf, line_number: usize) -> Self {
32 Self {
33 parsed,
34 source_file,
35 line_number,
36 }
37 }
38
39 pub fn source_file_name(&self) -> &str {
41 self.source_file
42 .file_name()
43 .and_then(|name| name.to_str())
44 .unwrap_or("unknown")
45 }
46
47 pub fn timestamp(&self) -> Option<&jiff::Timestamp> {
49 match &self.parsed {
50 Printable::Canonical(canonical) => Some(&canonical.timestamp),
51 Printable::Java(java) => Some(&java.timestamp),
52 Printable::Message(message) => message.timestamp.as_ref(),
53 Printable::TimeOnly(timestamped) => Some(×tamped.timestamp),
54 Printable::Json(_) => None,
55 Printable::Logfmt(logfmt) => logfmt.timestamp(),
56 Printable::Text(_) => None,
57 }
58 }
59
60 pub fn sort_key(&self) -> SortKey {
62 if let Some(ts) = self.timestamp() {
63 SortKey::Timestamp(*ts)
65 } else {
66 SortKey::FileOrder {
68 file: self.source_file.clone(),
69 line: self.line_number,
70 }
71 }
72 }
73}
74
75impl<'a> From<(PathBuf, usize, &'a str)> for SourcedLine<'a> {
76 fn from(value: (PathBuf, usize, &'a str)) -> Self {
77 let parsed = match serde_json::from_str::<Printable<'a>>(value.2) {
78 Ok(v) => v,
79 Err(_) => Printable::Text(value.2.to_owned()),
80 };
81 Self {
82 parsed,
83 source_file: value.0,
84 line_number: value.1,
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum SortKey {
93 Timestamp(jiff::Timestamp),
95 FileOrder { file: PathBuf, line: usize },
98}
99
100impl PartialOrd for SortKey {
101 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
102 Some(self.cmp(other))
103 }
104}
105
106impl Ord for SortKey {
107 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
108 use std::cmp::Ordering;
109 match (self, other) {
110 (SortKey::Timestamp(a), SortKey::Timestamp(b)) => a.cmp(b),
112
113 (SortKey::FileOrder { file: f1, line: l1 }, SortKey::FileOrder { file: f2, line: l2 }) => {
115 f1.cmp(f2).then(l1.cmp(l2))
116 }
117
118 (SortKey::Timestamp(_), SortKey::FileOrder { .. }) => {
120 Ordering::Less
124 }
125
126 (SortKey::FileOrder { .. }, SortKey::Timestamp(_)) => {
127 Ordering::Greater
129 }
130 }
131 }
132}
133
134struct FileNameTracker {
135 last: RwLock<String>,
136 every: bool,
137 on_swap: bool,
138}
139
140impl FileNameTracker {
141 pub fn new() -> Self {
142 let cfg = crate::config::config();
143 Self {
144 last: RwLock::new(String::default()),
145 every: cfg.all_file_names,
146 on_swap: !cfg.no_file_names,
147 }
148 }
149}
150
151static TRACKER: LazyLock<FileNameTracker> = LazyLock::new(FileNameTracker::new);
152
153impl<'a> PrettyPrintable for SourcedLine<'a> {
154 fn write(&self, buffer: &mut BytesMut) -> usize {
155 if TRACKER.every {
156 buffer.extend_from_slice(b"==> ");
157 buffer.extend_from_slice(self.source_file_name().as_bytes());
158 buffer.extend_from_slice(b" <==\n");
159 } else if TRACKER.on_swap
160 && self.source_file_name() != *TRACKER.last.read().expect("FileNameTracker lock poisoned")
161 {
162 buffer.extend_from_slice(b"==> ");
163 buffer.extend_from_slice(self.source_file_name().as_bytes());
164 buffer.extend_from_slice(b" <==\n");
165 *TRACKER.last.write().expect("FileNameTracker lock poisoned") = self.source_file_name().to_owned();
166 }
167 self.parsed.write(buffer)
168 }
169 fn cells(&self) -> Vec<String> {
170 self.parsed.cells()
171 }
172}
173
174impl<'a> Display for SourcedLine<'a> {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 self.parsed.fmt(f)
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::super::*;
184 use super::*;
185
186 fn extract_msg(item: &SourcedLine<'_>) -> String {
188 match item.parsed {
189 Printable::Canonical(ref canonical) => canonical.message.as_ref().to_owned(),
190 Printable::Java(ref java) => java.message.as_ref().to_owned(),
191 Printable::Message(ref message) => message.message.as_ref().to_owned(),
192 Printable::TimeOnly(ref timestamped) => {
193 let obj = timestamped.rest.as_object().expect("rest should be a json object");
196 let message = obj.get("message").expect("there is a message in this bottle yeah-a");
197 message.as_str().unwrap_or_default().to_string()
198 }
199 Printable::Json(ref generic_json) => {
200 if let Some(obj) = generic_json.rest.as_object()
202 && let Some(message) = obj.get("message")
203 {
204 return message.as_str().unwrap_or_default().to_string();
205 }
206 String::default()
207 }
208 Printable::Logfmt(ref v) => v.message(),
209 Printable::Text(ref v) => v.clone(),
210 }
211 }
212
213 #[test]
214 fn chronological_sort_across_files() {
215 use std::path::PathBuf;
216
217 let lines = vec![
219 (
220 PathBuf::from("file1.log"),
221 0,
222 r#"{"timestamp": "2025-08-01T10:02:00Z", "message": "file1 line1"}"#.to_string(),
223 ),
224 (
225 PathBuf::from("file2.log"),
226 0,
227 r#"{"timestamp": "2025-08-01T10:01:00Z", "message": "file2 line1"}"#.to_string(),
228 ),
229 (
230 PathBuf::from("file1.log"),
231 1,
232 r#"{"timestamp": "2025-08-01T10:03:00Z", "message": "file1 line2"}"#.to_string(),
233 ),
234 (
235 PathBuf::from("file2.log"),
236 1,
237 r#"{"timestamp": "2025-08-01T10:00:30Z", "message": "file2 line2"}"#.to_string(),
238 ),
239 ];
240
241 let mut sorted: Vec<SourcedLine<'_>> = lines
242 .iter()
243 .map(|xs| {
244 let input = (xs.0.clone(), xs.1 as usize, xs.2.as_str());
245 SourcedLine::from(input)
246 })
247 .collect();
248 sorted.sort_by_key(|xs| xs.sort_key());
249
250 assert_eq!(extract_msg(&sorted[0]), "file2 line2");
252 assert_eq!(extract_msg(&sorted[1]), "file2 line1");
253 assert_eq!(extract_msg(&sorted[2]), "file1 line1");
254 assert_eq!(extract_msg(&sorted[3]), "file1 line2");
255 }
256
257 #[test]
258 fn sorting_mixed_types() {
259 use std::path::PathBuf;
260
261 let lines = [
263 (
264 PathBuf::from("file1.log"),
265 0,
266 r#"{"message": "no timestamp 1"}"#.to_string(),
267 ),
268 (
269 PathBuf::from("file2.log"),
270 0,
271 r#"{"timestamp": "2025-08-01T10:01:00Z", "message": "timestamped"}"#.to_string(),
272 ),
273 (
274 PathBuf::from("file1.log"),
275 1,
276 r#"{"message": "no timestamp 2"}"#.to_string(),
277 ),
278 ];
279
280 let mut sorted: Vec<SourcedLine<'_>> = lines
281 .iter()
282 .map(|xs| {
283 let input = (xs.0.clone(), xs.1 as usize, xs.2.as_str());
284 SourcedLine::from(input)
285 })
286 .collect();
287 sorted.sort_by_key(|xs| xs.sort_key());
288
289 assert_eq!(extract_msg(&sorted[0]), "timestamped");
291 assert_eq!(extract_msg(&sorted[1]), "no timestamp 1");
292 assert_eq!(extract_msg(&sorted[2]), "no timestamp 2");
293 }
294
295 #[test]
296 fn sorting_preserves_file_order() {
297 use std::path::PathBuf;
298
299 let lines = vec![
301 (PathBuf::from("b.log"), 1, r#"{"message": "b file line 2"}"#.to_string()),
302 (PathBuf::from("a.log"), 0, r#"{"message": "a file line 1"}"#.to_string()),
303 (PathBuf::from("b.log"), 0, r#"{"message": "b file line 1"}"#.to_string()),
304 (PathBuf::from("a.log"), 1, r#"{"message": "a file line 2"}"#.to_string()),
305 ];
306
307 let mut sorted: Vec<SourcedLine<'_>> = lines
308 .iter()
309 .map(|xs| {
310 let input = (xs.0.clone(), xs.1 as usize, xs.2.as_str());
311 SourcedLine::from(input)
312 })
313 .collect();
314 sorted.sort_by_key(|xs| xs.sort_key());
315
316 assert_eq!(extract_msg(&sorted[0]), "a file line 1");
318 assert_eq!(extract_msg(&sorted[1]), "a file line 2");
319 assert_eq!(extract_msg(&sorted[2]), "b file line 1");
320 assert_eq!(extract_msg(&sorted[3]), "b file line 2");
321 }
322
323 #[test]
324 fn sorting_handle_empty_input() {
325 let mut lines: Vec<SourcedLine<'_>> = vec![];
326 lines.sort_by_key(|xs| xs.sort_key());
327 assert_eq!(lines.len(), 0);
328 }
329
330 #[test]
331 fn sorting_handles_invalid_json() {
332 use std::path::PathBuf;
333
334 let lines = [
336 (PathBuf::from("test.log"), 0, "not json at all".to_string()),
337 (
338 PathBuf::from("test.log"),
339 1,
340 r#"{"timestamp": "2025-08-01T10:01:00Z", "message": "valid"}"#.to_string(),
341 ),
342 ];
343
344 let mut sorted: Vec<SourcedLine<'_>> = lines
345 .iter()
346 .map(|xs| {
347 let input = (xs.0.clone(), xs.1 as usize, xs.2.as_str());
348 SourcedLine::from(input)
349 })
350 .collect();
351 sorted.sort_by_key(|xs| xs.sort_key());
352
353 assert_eq!(extract_msg(&sorted[0]), "valid");
355 assert_eq!(extract_msg(&sorted[1]), "not json at all");
356 }
357}