pub(crate) use crate::uuid::{is_uuid_at, UUID_PREFIX_LEN};
use freeswitch_types::LogLevel;
use crate::mask::Mask;
use crate::uuid::{find_uuid_in, UUID_LEN};
use std::fmt;
use std::ops::Range;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineKind {
Full,
System,
UuidContinuation,
BareContinuation,
Truncated,
Empty,
}
impl fmt::Display for LineKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LineKind::Full => f.pad("full"),
LineKind::System => f.pad("system"),
LineKind::UuidContinuation => f.pad("uuid-cont"),
LineKind::BareContinuation => f.pad("bare-cont"),
LineKind::Truncated => f.pad("truncated"),
LineKind::Empty => f.pad("empty"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct RawLine<'a> {
pub uuid: Option<&'a str>,
pub timestamp: Option<&'a str>,
pub idle_pct: Option<&'a str>,
pub level: Option<LogLevel>,
pub source: Option<&'a str>,
pub message: &'a str,
pub kind: LineKind,
}
pub(crate) fn is_date_at(bytes: &[u8], offset: usize) -> bool {
if bytes.len() < offset + 5 {
return false;
}
bytes[offset..offset + 4].iter().all(u8::is_ascii_digit) && bytes[offset + 4] == b'-'
}
const TIMESTAMP_LEN: usize = 26;
const TIMESTAMP_MASK: Mask = Mask {
len: TIMESTAMP_LEN,
separators: &[
(4, b'-'),
(7, b'-'),
(10, b' '),
(13, b':'),
(16, b':'),
(19, b'.'),
],
filler: u8::is_ascii_digit,
};
const MAX_IDLE_PCT_LEN: usize = 7;
pub(crate) struct HeaderSpan {
timestamp: Range<usize>,
idle_pct: Option<Range<usize>>,
level: usize,
}
fn idle_pct_span(bytes: &[u8], from: usize) -> Option<Range<usize>> {
let end = (from + MAX_IDLE_PCT_LEN).min(bytes.len());
let window = bytes.get(from..end)?;
let pct = window.iter().position(|&b| b == b'%')?;
if !window[0].is_ascii_digit()
|| !window[..pct]
.iter()
.all(|&b| b.is_ascii_digit() || b == b'.')
{
return None;
}
(bytes.get(from + pct + 1) == Some(&b' ')).then(|| from..from + pct + 1)
}
pub(crate) fn header_at(bytes: &[u8], offset: usize) -> Option<HeaderSpan> {
if !TIMESTAMP_MASK.matches_at(bytes, offset) || bytes.get(offset + TIMESTAMP_LEN) != Some(&b' ')
{
return None;
}
let after_stamp = offset + TIMESTAMP_LEN + 1;
let idle_pct = idle_pct_span(bytes, after_stamp);
let level = idle_pct.as_ref().map_or(after_stamp, |r| r.end + 1);
if bytes.get(level) != Some(&b'[') {
return None;
}
Some(HeaderSpan {
timestamp: offset..offset + TIMESTAMP_LEN,
idle_pct,
level,
})
}
const MAX_LEVEL_LEN: usize = 7;
fn level_field_closes(bytes: &[u8], level: usize) -> bool {
let end = (level + MAX_LEVEL_LEN + 2).min(bytes.len());
bytes
.get(level + 1..end)
.and_then(|w| w.iter().position(|&b| b == b']'))
.is_some_and(|p| p > 0)
}
pub(crate) fn is_log_header_at(bytes: &[u8], offset: usize) -> bool {
header_at(bytes, offset).is_some_and(|h| level_field_closes(bytes, h.level))
}
pub fn level_from_bracketed(s: &str) -> Option<LogLevel> {
let inner = s.strip_prefix('[')?.strip_suffix(']')?;
LogLevel::ALL
.iter()
.find(|l| l.as_str().eq_ignore_ascii_case(inner))
.copied()
}
fn header_boundaries_ok(s: &str) -> bool {
s.len() < 27 || (s.is_char_boundary(26) && s.is_char_boundary(27))
}
struct LineHeader<'a> {
timestamp: Option<&'a str>,
idle_pct: Option<&'a str>,
level: Option<LogLevel>,
source: Option<&'a str>,
message: &'a str,
}
fn parse_timestamped_fields(s: &str) -> LineHeader<'_> {
if s.len() < TIMESTAMP_LEN + 1 {
return LineHeader {
timestamp: None,
idle_pct: None,
level: None,
source: None,
message: s,
};
}
let (timestamp, idle_pct, rest) = match header_at(s.as_bytes(), 0) {
Some(h) => (&s[h.timestamp], h.idle_pct.map(|r| &s[r]), &s[h.level..]),
None => (&s[..TIMESTAMP_LEN], None, &s[TIMESTAMP_LEN + 1..]),
};
let timestamp = Some(timestamp);
let bracket_end = match rest.find(']') {
Some(p) => p,
None => {
return LineHeader {
timestamp,
idle_pct,
level: None,
source: None,
message: rest,
}
}
};
let level = level_from_bracketed(&rest[0..=bracket_end]);
if rest.len() < bracket_end + 3 || !rest.is_char_boundary(bracket_end + 2) {
return LineHeader {
timestamp,
idle_pct,
level,
source: None,
message: "",
};
}
let rest = &rest[bracket_end + 2..];
let source_end = rest.find(' ').unwrap_or(rest.len());
let message = if source_end < rest.len() {
&rest[source_end + 1..]
} else {
""
};
LineHeader {
timestamp,
idle_pct,
level,
source: Some(&rest[0..source_end]),
message,
}
}
pub fn parse_line(line: &str) -> RawLine<'_> {
if line.trim().is_empty() {
return RawLine {
uuid: None,
timestamp: None,
idle_pct: None,
level: None,
source: None,
message: line,
kind: LineKind::Empty,
};
}
let bytes = line.as_bytes();
if is_uuid_at(bytes, 0) {
let uuid = &line[0..UUID_LEN];
let after_uuid = &line[UUID_PREFIX_LEN..];
if is_date_at(bytes, UUID_PREFIX_LEN) && header_boundaries_ok(after_uuid) {
let h = parse_timestamped_fields(after_uuid);
return RawLine {
uuid: Some(uuid),
timestamp: h.timestamp,
idle_pct: h.idle_pct,
level: h.level,
source: h.source,
message: h.message,
kind: LineKind::Full,
};
}
return RawLine {
uuid: Some(uuid),
timestamp: None,
idle_pct: None,
level: None,
source: None,
message: after_uuid,
kind: LineKind::UuidContinuation,
};
}
if is_date_at(bytes, 0) && header_boundaries_ok(line) {
let h = parse_timestamped_fields(line);
let (uuid, message) = if is_uuid_at(h.message.as_bytes(), 0) {
(Some(&h.message[0..UUID_LEN]), &h.message[UUID_PREFIX_LEN..])
} else {
(None, h.message)
};
return RawLine {
uuid,
timestamp: h.timestamp,
idle_pct: h.idle_pct,
level: h.level,
source: h.source,
message,
kind: LineKind::System,
};
}
if let Some(uuid_start) = find_uuid_in(bytes) {
let uuid = &line[uuid_start..uuid_start + UUID_LEN];
let message = if line.len() > uuid_start + UUID_PREFIX_LEN {
&line[uuid_start + UUID_PREFIX_LEN..]
} else {
""
};
return RawLine {
uuid: Some(uuid),
timestamp: None,
idle_pct: None,
level: None,
source: None,
message,
kind: LineKind::Truncated,
};
}
RawLine {
uuid: None,
timestamp: None,
idle_pct: None,
level: None,
source: None,
message: line,
kind: LineKind::BareContinuation,
}
}
#[cfg(test)]
mod tests {
use super::*;
const UUID1: &str = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
#[test]
fn full_line_all_fields() {
let line = format!(
"{UUID1} 2025-01-15 10:30:45.123456 95.97% [DEBUG] sofia.c:100 Test message here"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Full);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.idle_pct, Some("95.97%"));
assert_eq!(parsed.level, Some(LogLevel::Debug));
assert_eq!(parsed.source, Some("sofia.c:100"));
assert_eq!(parsed.message, "Test message here");
}
#[test]
fn full_line_each_level() {
for (name, expected) in [
("DEBUG", LogLevel::Debug),
("INFO", LogLevel::Info),
("NOTICE", LogLevel::Notice),
("WARNING", LogLevel::Warning),
("ERR", LogLevel::Error),
("CRIT", LogLevel::Crit),
("ALERT", LogLevel::Alert),
("CONSOLE", LogLevel::Console),
] {
let line =
format!("{UUID1} 2025-01-15 10:30:45.123456 95.97% [{name}] sofia.c:100 Test");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Full);
assert_eq!(parsed.level, Some(expected), "failed for [{name}]");
}
}
#[test]
fn full_line_high_idle() {
let line =
format!("{UUID1} 2025-01-15 10:30:45.123456 99.99% [DEBUG] sofia.c:100 High idle");
let parsed = parse_line(&line);
assert_eq!(parsed.idle_pct, Some("99.99%"));
}
#[test]
fn full_line_low_idle() {
let line = format!("{UUID1} 2025-01-15 10:30:45.123456 0.00% [DEBUG] sofia.c:100 Low idle");
let parsed = parse_line(&line);
assert_eq!(parsed.idle_pct, Some("0.00%"));
}
#[test]
fn idle_pct_requires_the_space_after_the_sign() {
let line = format!("{UUID1} 2025-01-15 10:30:45.123456 9%X[DEBUG] sofia.c:100 Message");
assert_eq!(parse_line(&line).idle_pct, None);
}
#[test]
fn full_line_long_message() {
let line = format!(
"{UUID1} 2025-01-15 10:30:45.123456 95.97% [DEBUG] sofia.c:100 Channel [sofia/internal] key=val:123 (test) {{braces}}"
);
let parsed = parse_line(&line);
assert_eq!(
parsed.message,
"Channel [sofia/internal] key=val:123 (test) {braces}"
);
}
#[test]
fn system_line_no_uuid() {
let line =
"2025-01-15 10:30:45.123456 95.97% [INFO] mod_event_socket.c:1772 Event Socket command";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.uuid, None);
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.idle_pct, Some("95.97%"));
assert_eq!(parsed.level, Some(LogLevel::Info));
assert_eq!(parsed.source, Some("mod_event_socket.c:1772"));
assert_eq!(parsed.message, "Event Socket command");
}
#[test]
fn system_line_with_embedded_uuid() {
let line = format!(
"2025-01-15 10:30:45.123456 95.97% [DEBUG] switch_cpp.cpp:1466 {UUID1} DAA-LOG WaveManager PSAP 911 originate"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.level, Some(LogLevel::Debug));
assert_eq!(parsed.source, Some("switch_cpp.cpp:1466"));
assert_eq!(parsed.message, "DAA-LOG WaveManager PSAP 911 originate");
}
#[test]
fn system_line_with_embedded_uuid_empty_message() {
let line = format!("2025-01-15 10:30:45.123456 95.97% [INFO] switch_cpp.cpp:1466 {UUID1} ");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.message, "");
}
#[test]
fn system_line_without_embedded_uuid() {
let line =
"2025-01-15 10:30:45.123456 95.97% [INFO] mod_event_socket.c:1772 Event Socket command";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.uuid, None);
assert_eq!(parsed.message, "Event Socket command");
}
#[test]
fn system_line_event_socket() {
let line = "2025-01-15 10:30:45.123456 95.97% [NOTICE] mod_logfile.c:217 New log started.";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.level, Some(LogLevel::Notice));
assert_eq!(parsed.message, "New log started.");
}
#[test]
fn uuid_continuation_dialplan() {
let line =
format!("{UUID1} Dialplan: sofia/internal/+15550001234@192.0.2.1 parsing [public]");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::UuidContinuation);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.timestamp, None);
assert_eq!(parsed.level, None);
assert_eq!(
parsed.message,
"Dialplan: sofia/internal/+15550001234@192.0.2.1 parsing [public]"
);
}
#[test]
fn uuid_continuation_execute() {
let line =
format!("{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(foo=bar)");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::UuidContinuation);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(
parsed.message,
"EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(foo=bar)"
);
}
#[test]
fn uuid_continuation_channel_var() {
let line = format!("{UUID1} Channel-State: [CS_EXECUTE]");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::UuidContinuation);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.message, "Channel-State: [CS_EXECUTE]");
}
#[test]
fn uuid_continuation_variable() {
let line = format!("{UUID1} variable_sip_call_id: [test123@192.0.2.1]");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::UuidContinuation);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.message, "variable_sip_call_id: [test123@192.0.2.1]");
}
#[test]
fn uuid_continuation_blank() {
let line = format!("{UUID1} ");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::UuidContinuation);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.message, "");
}
#[test]
fn bare_variable() {
let line = "variable_foo: [bar]";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.uuid, None);
assert_eq!(parsed.message, "variable_foo: [bar]");
}
#[test]
fn bare_sdp_origin() {
let line = "o=- 1234 5678 IN IP4 192.0.2.1";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.message, line);
}
#[test]
fn bare_sdp_media() {
let line = "m=audio 47758 RTP/AVP 0 101";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.message, line);
}
#[test]
fn bare_sdp_attribute() {
let line = "a=rtpmap:0 PCMU/8000";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.message, line);
}
#[test]
fn bare_closing_bracket() {
let line = "]";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.message, "]");
}
#[test]
fn bare_empty_line() {
let parsed = parse_line("");
assert_eq!(parsed.kind, LineKind::Empty);
assert_eq!(parsed.message, "");
}
#[test]
fn truncated_varia_prefix() {
let line = format!(
"varia{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Truncated);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(
parsed.message,
"EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
);
}
#[test]
fn truncated_variab_prefix() {
let line = format!(
"variab{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Truncated);
assert_eq!(parsed.uuid, Some(UUID1));
}
#[test]
fn truncated_var_prefix() {
let line =
format!("var{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Truncated);
assert_eq!(parsed.uuid, Some(UUID1));
}
#[test]
fn truncated_variable_prefix() {
let line = format!(
"variable{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Truncated);
assert_eq!(parsed.uuid, Some(UUID1));
}
#[test]
fn bracketed_level_folds_the_log_spelling() {
assert_eq!(level_from_bracketed("[DEBUG]"), Some(LogLevel::Debug));
assert_eq!(level_from_bracketed("[ERR]"), Some(LogLevel::Error));
assert_eq!(level_from_bracketed("[CONSOLE]"), Some(LogLevel::Console));
}
#[test]
fn bracketed_level_reads_any_case() {
assert_eq!(level_from_bracketed("[debug]"), Some(LogLevel::Debug));
assert_eq!(level_from_bracketed("[Warning]"), Some(LogLevel::Warning));
}
#[test]
fn bracketed_level_rejects_malformed() {
assert_eq!(level_from_bracketed("[FAKE]"), None);
assert_eq!(level_from_bracketed("DEBUG"), None);
assert_eq!(level_from_bracketed("[]"), None);
assert_eq!(level_from_bracketed("["), None);
assert_eq!(level_from_bracketed(""), None);
}
#[test]
fn levels_order_by_switch_log_level_numbering() {
assert!(LogLevel::Console < LogLevel::Alert);
assert!(LogLevel::Error < LogLevel::Warning);
assert!(LogLevel::Info < LogLevel::Debug);
}
#[test]
fn log_header_with_idle_pct() {
let line = "2024-04-02 10:31:28.785679 98.03% [NOTICE] sofia.c:1114 Hangup";
assert!(is_log_header_at(line.as_bytes(), 0));
}
#[test]
fn log_header_no_idle_pct() {
let line = "2024-04-02 10:31:28.785679 [NOTICE] sofia.c:1114 Hangup";
assert!(is_log_header_at(line.as_bytes(), 0));
}
#[test]
fn log_header_no_idle_pct_at_offset() {
let line = "Session does not exist, aborting REFER.2024-04-02 10:31:28.785679 [WARNING] sofia_presence.c:4546 x";
let offset = line.find("2024").unwrap();
assert!(is_log_header_at(line.as_bytes(), offset));
}
#[test]
fn idle_pct_rejects_non_numeric_run() {
let line = "2025-01-15 10:30:45.123456 ab% [DEBUG] sofia.c:100 Message";
assert!(!is_log_header_at(line.as_bytes(), 0));
assert_eq!(parse_line(line).idle_pct, None);
}
#[test]
fn idle_pct_needs_the_level_after_it() {
let line = "2025-01-15 10:30:45.123456 95.97%";
assert!(!is_log_header_at(line.as_bytes(), 0));
assert_eq!(parse_line(line).idle_pct, None);
}
#[test]
fn a_header_cut_inside_the_level_starts_no_record() {
for line in [
"2025-01-15 10:30:45.123456 95.97% [",
"2025-01-15 10:30:45.123456 95.97% [DEB",
"2025-01-15 10:30:45.123456 [",
] {
assert!(!is_log_header_at(line.as_bytes(), 0), "split on {line}");
}
let cut = "2025-01-15 10:30:45.123456 95.97% [";
assert_eq!(parse_line(cut).idle_pct, Some("95.97%"));
}
#[test]
fn log_header_rejects_non_header() {
let line = "2024-04-02 not a real timestamp here";
assert!(!is_log_header_at(line.as_bytes(), 0));
}
#[test]
fn full_line_no_idle_pct() {
let line = format!(
"{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] switch_core_session.c:1744 Session 3178948 ended"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Full);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.idle_pct, None);
assert_eq!(parsed.level, Some(LogLevel::Notice));
assert_eq!(parsed.source, Some("switch_core_session.c:1744"));
assert_eq!(parsed.message, "Session 3178948 ended");
}
#[test]
fn full_line_no_idle_pct_url_encoded_percent() {
let line = format!(
"{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] switch_core_session.c:1744 Session 3178948 (sofia/psap/gw%2Bsg1vofswb-inbound@198.51.100.5:5060) Ended"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Full);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.idle_pct, None);
assert_eq!(parsed.level, Some(LogLevel::Notice));
assert_eq!(parsed.source, Some("switch_core_session.c:1744"));
assert_eq!(
parsed.message,
"Session 3178948 (sofia/psap/gw%2Bsg1vofswb-inbound@198.51.100.5:5060) Ended"
);
}
#[test]
fn system_line_no_idle_pct() {
let line = "2025-01-15 10:30:45.123456 [INFO] mod_event_socket.c:1772 Event Socket command";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.uuid, None);
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.idle_pct, None);
assert_eq!(parsed.level, Some(LogLevel::Info));
assert_eq!(parsed.source, Some("mod_event_socket.c:1772"));
assert_eq!(parsed.message, "Event Socket command");
}
#[test]
fn full_line_no_idle_pct_hangup_url_encoded() {
let line = format!(
"{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] sofia.c:1089 Hangup sofia/psap/gw%2Bgateway@198.51.100.5:5060 [CS_EXCHANGE_MEDIA] [CALL_AWARDED_DELIVERED]"
);
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::Full);
assert_eq!(parsed.idle_pct, None);
assert_eq!(parsed.level, Some(LogLevel::Notice));
assert_eq!(parsed.source, Some("sofia.c:1089"));
assert_eq!(
parsed.message,
"Hangup sofia/psap/gw%2Bgateway@198.51.100.5:5060 [CS_EXCHANGE_MEDIA] [CALL_AWARDED_DELIVERED]"
);
}
#[test]
fn not_uuid_36_chars() {
let line = "this-is-not-a-valid-uuid-value-12345 rest of line";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.message, line);
}
#[test]
fn uuid_in_message_not_prefix() {
let line =
format!("This is some log message body with extra context then {UUID1} appears here");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.message, line.as_str());
}
#[test]
fn whitespace_only_is_empty() {
let parsed = parse_line(" \t ");
assert_eq!(parsed.kind, LineKind::Empty);
}
#[test]
fn multibyte_straddling_timestamp_end_not_system() {
let line = "2025-01-15 10:30:45.12345é more content following here";
assert!(!line.is_char_boundary(26));
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.timestamp, None);
assert_eq!(parsed.message, line);
}
#[test]
fn multibyte_after_timestamp_not_system() {
let line = "2025-01-15 10:30:45.123456é more content following here";
assert!(!line.is_char_boundary(27));
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::BareContinuation);
assert_eq!(parsed.timestamp, None);
assert_eq!(parsed.message, line);
}
#[test]
fn multibyte_after_uuid_timestamp_is_continuation() {
let line = format!("{UUID1} 2025-01-15 10:30:45.123456é more content");
let parsed = parse_line(&line);
assert_eq!(parsed.kind, LineKind::UuidContinuation);
assert_eq!(parsed.uuid, Some(UUID1));
assert_eq!(parsed.timestamp, None);
assert_eq!(parsed.message, "2025-01-15 10:30:45.123456é more content");
}
#[test]
fn multibyte_after_idle_pct_degrades() {
let line = "2025-01-15 10:30:45.123456 9%é[DEBUG] x";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.idle_pct, None);
assert_eq!(parsed.level, None);
}
#[test]
fn multibyte_after_level_bracket_degrades() {
let line = "2025-01-15 10:30:45.123456 95.97% [DEBUG]éxx";
let parsed = parse_line(line);
assert_eq!(parsed.kind, LineKind::System);
assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
assert_eq!(parsed.idle_pct, Some("95.97%"));
assert_eq!(parsed.level, Some(LogLevel::Debug));
assert_eq!(parsed.source, None);
assert_eq!(parsed.message, "");
}
}