1use std::fmt;
2use std::time::SystemTime;
3
4use chrono::{DateTime, SecondsFormat, Utc};
5use tracing::Level;
6
7pub(crate) fn render_line(
14 at: SystemTime,
15 level: &Level,
16 logger: &str,
17 bound: &[(String, String)],
18 message: &str,
19 fields: &[(String, String)],
20) -> String {
21 let timestamp = DateTime::<Utc>::from(at).to_rfc3339_opts(SecondsFormat::Millis, true);
22 let mut line = format!("{timestamp} {:<5} {logger}:", level.as_str());
23
24 if !bound.is_empty() {
25 line.push_str(" [");
26 for (index, (key, value)) in bound.iter().enumerate() {
27 if index > 0 {
28 line.push(' ');
29 }
30 line.push_str(key);
31 line.push('=');
32 line.push_str(&format_value(value));
33 }
34 line.push(']');
35 }
36 if !message.is_empty() {
37 line.push(' ');
38 line.push_str(&escape_message(message));
39 }
40 for (key, value) in fields {
41 line.push(' ');
42 line.push_str(key);
43 line.push('=');
44 line.push_str(&format_value(value));
45 }
46
47 line
48}
49
50pub(crate) fn logger_name(module_id: &str, target: &str) -> String {
56 if target.is_empty() || target == module_id || target.contains("::") {
57 return module_id.to_owned();
58 }
59 if !target.split('.').all(is_segment) {
60 return module_id.to_owned();
61 }
62 format!("{module_id}.{target}")
63}
64
65pub(crate) fn is_segment(segment: &str) -> bool {
66 let mut chars = segment.chars();
67 matches!(chars.next(), Some('a'..='z'))
68 && chars.all(|character| matches!(character, 'a'..='z' | '0'..='9' | '-'))
69}
70
71fn escape_message(message: &str) -> String {
74 message
75 .replace('\\', "\\\\")
76 .replace('\r', "\\r")
77 .replace('\n', "\\n")
78}
79
80fn format_value(value: &str) -> String {
81 if value.is_empty()
82 || value
83 .chars()
84 .any(|character| matches!(character, ' ' | '"' | '\n' | '\r' | ']'))
85 {
86 let escaped = value
87 .replace('\\', "\\\\")
88 .replace('"', "\\\"")
89 .replace('\r', "\\r")
90 .replace('\n', "\\n");
91 format!("\"{escaped}\"")
92 } else {
93 value.to_owned()
96 }
97}
98
99pub(crate) fn strip_ansi(input: &str) -> String {
100 let without_c1 = strip_c1_sequences(input);
101 let input = without_c1.as_str();
102 let bytes = input.as_bytes();
103 let mut output = String::with_capacity(input.len());
104 let mut index = 0;
105 let mut plain_start = 0;
106
107 while index < bytes.len() {
108 if bytes[index] != 0x1b {
109 index += 1;
110 continue;
111 }
112
113 output.push_str(&input[plain_start..index]);
114 index += 1;
115 if index >= bytes.len() {
116 plain_start = index;
117 break;
118 }
119
120 match bytes[index] {
121 b'[' => {
122 index += 1;
123 while index < bytes.len() {
124 let byte = bytes[index];
125 index += 1;
126 if (0x40..=0x7e).contains(&byte) {
127 break;
128 }
129 }
130 }
131 b']' => {
132 index += 1;
133 while index < bytes.len() {
134 if bytes[index] == 0x07 {
135 index += 1;
136 break;
137 }
138 if bytes[index] == 0x1b
139 && bytes.get(index + 1).is_some_and(|next| *next == b'\\')
140 {
141 index += 2;
142 break;
143 }
144 index += 1;
145 }
146 }
147 _ => index += 1,
148 }
149 plain_start = index;
150 }
151
152 if plain_start == 0 {
153 return without_c1;
154 }
155 output.push_str(&input[plain_start..]);
156 output
157}
158
159fn strip_c1_sequences(input: &str) -> String {
160 let mut characters = input.chars();
161 let mut output = String::with_capacity(input.len());
162 while let Some(character) = characters.next() {
163 match character {
164 '\u{009b}' => {
165 for parameter in characters.by_ref() {
166 if ('@'..='~').contains(¶meter) {
167 break;
168 }
169 }
170 }
171 '\u{009d}' => {
172 for payload in characters.by_ref() {
173 if matches!(payload, '\u{0007}' | '\u{009c}') {
174 break;
175 }
176 }
177 }
178 '\u{0080}'..='\u{009f}' => {}
179 _ => output.push(character),
180 }
181 }
182 output
183}
184
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub enum ParsedLevel {
188 Trace,
190 Debug,
192 Info,
194 Warn,
196 Error,
198}
199
200#[derive(Clone, Debug, Eq, PartialEq)]
202pub struct ParsedLine<'a> {
203 pub timestamp: SystemTime,
205 pub level: ParsedLevel,
207 pub logger: &'a str,
209 pub module_id: &'a str,
211 pub bound: Option<&'a str>,
213 pub body: &'a str,
215}
216
217impl ParsedLine<'_> {
218 pub fn session(&self) -> Option<&str> {
220 self.bound.and_then(|bound| bound_value(bound, "session"))
221 }
222}
223
224fn bound_value<'a>(bound: &'a str, key: &str) -> Option<&'a str> {
225 bound
226 .split(' ')
227 .filter_map(|pair| pair.split_once('='))
228 .find(|(candidate, _)| *candidate == key)
229 .map(|(_, value)| value)
230}
231
232#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub struct ParseError {
235 reason: &'static str,
236}
237
238impl ParseError {
239 pub(crate) const fn new(reason: &'static str) -> Self {
240 Self { reason }
241 }
242
243 pub const fn reason(self) -> &'static str {
245 self.reason
246 }
247}
248
249impl fmt::Display for ParseError {
250 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251 formatter.write_str(self.reason)
252 }
253}
254
255impl std::error::Error for ParseError {}
256
257pub(crate) fn parse(line: &str) -> Result<ParsedLine<'_>, ParseError> {
258 if line.contains('\u{1b}') || line.contains('\u{9b}') {
259 return Err(ParseError::new("ansi_forbidden"));
260 }
261 if line.contains(['\n', '\r']) {
262 return Err(ParseError::new("line_break"));
263 }
264
265 let (timestamp_text, after_timestamp) = line
266 .split_once(' ')
267 .ok_or_else(|| ParseError::new("timestamp_missing"))?;
268 if !timestamp_text.ends_with('Z') {
269 return Err(ParseError::new("timestamp_not_utc_z"));
270 }
271 if timestamp_text.len() != 24 {
272 return Err(ParseError::new("timestamp_precision"));
273 }
274 let timestamp = DateTime::parse_from_rfc3339(timestamp_text)
275 .map_err(|_| ParseError::new("timestamp_invalid"))?;
276
277 let (level, after_level) = parse_level(after_timestamp)?;
278
279 let (logger_token, mut body) = match after_level.split_once(' ') {
283 Some(split) => split,
284 None => (after_level, ""),
285 };
286 let logger = logger_token
287 .strip_suffix(':')
288 .ok_or_else(|| ParseError::new("logger_not_terminated"))?;
289 if logger.is_empty() {
290 return Err(ParseError::new("logger_missing"));
291 }
292 if !logger.split('.').all(is_segment) {
293 return Err(ParseError::new("logger_segment_grammar"));
294 }
295 let module_id = logger.split('.').next().unwrap_or(logger);
296
297 let mut bound = None;
298 if let Some(rest) = body.strip_prefix('[') {
299 let close =
300 find_bracket_close(rest).ok_or_else(|| ParseError::new("bound_unterminated"))?;
301 let inside = &rest[..close];
302 if inside.is_empty() {
303 return Err(ParseError::new("empty_bound_bracket"));
304 }
305 if let Some(session) = bound_value(inside, "session") {
306 let valid = session
307 .rsplit_once(':')
308 .is_some_and(|(issuer, id)| !issuer.is_empty() && !id.is_empty());
309 if !valid {
313 return Err(ParseError::new("session_missing_issuer"));
314 }
315 }
316 bound = Some(inside);
317 body = rest[close + 1..]
318 .strip_prefix(' ')
319 .unwrap_or(&rest[close + 1..]);
320 }
321
322 if bound.is_none() && body.contains(" [") && body.ends_with(']') {
327 return Err(ParseError::new("bound_after_message"));
328 }
329
330 Ok(ParsedLine {
331 timestamp: SystemTime::from(timestamp),
332 level,
333 logger,
334 module_id,
335 bound,
336 body,
337 })
338}
339
340fn find_bracket_close(input: &str) -> Option<usize> {
344 let mut in_quotes = false;
345 let mut escaped = false;
346 for (index, character) in input.char_indices() {
347 if escaped {
348 escaped = false;
349 continue;
350 }
351 match character {
352 '\\' if in_quotes => escaped = true,
353 '"' => in_quotes = !in_quotes,
354 ']' if !in_quotes => return Some(index),
355 _ => {}
356 }
357 }
358 None
359}
360
361fn parse_level(input: &str) -> Result<(ParsedLevel, &str), ParseError> {
362 for (prefix, level) in [
363 ("TRACE ", ParsedLevel::Trace),
364 ("DEBUG ", ParsedLevel::Debug),
365 ("INFO ", ParsedLevel::Info),
366 ("WARN ", ParsedLevel::Warn),
367 ("ERROR ", ParsedLevel::Error),
368 ] {
369 if let Some(rest) = input.strip_prefix(prefix) {
370 return Ok((level, rest));
371 }
372 }
373
374 if ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]
375 .iter()
376 .any(|level| input.starts_with(level))
377 {
378 Err(ParseError::new("level_column_width"))
379 } else {
380 Err(ParseError::new("level_invalid"))
381 }
382}