use std::fmt;
use std::time::SystemTime;
use chrono::{DateTime, SecondsFormat, Utc};
use tracing::Level;
pub(crate) fn render_line(
at: SystemTime,
level: &Level,
logger: &str,
bound: &[(String, String)],
message: &str,
fields: &[(String, String)],
) -> String {
let timestamp = DateTime::<Utc>::from(at).to_rfc3339_opts(SecondsFormat::Millis, true);
let mut line = format!("{timestamp} {:<5} {logger}:", level.as_str());
if !bound.is_empty() {
line.push_str(" [");
for (index, (key, value)) in bound.iter().enumerate() {
if index > 0 {
line.push(' ');
}
line.push_str(key);
line.push('=');
line.push_str(&format_value(value));
}
line.push(']');
}
if !message.is_empty() {
line.push(' ');
line.push_str(&escape_message(message));
}
for (key, value) in fields {
line.push(' ');
line.push_str(key);
line.push('=');
line.push_str(&format_value(value));
}
line
}
pub(crate) fn logger_name(module_id: &str, target: &str) -> String {
if target.is_empty() || target == module_id || target.contains("::") {
return module_id.to_owned();
}
if !target.split('.').all(is_segment) {
return module_id.to_owned();
}
format!("{module_id}.{target}")
}
pub(crate) fn is_segment(segment: &str) -> bool {
let mut chars = segment.chars();
matches!(chars.next(), Some('a'..='z'))
&& chars.all(|character| matches!(character, 'a'..='z' | '0'..='9' | '-'))
}
fn is_escaped_control(character: char) -> bool {
matches!(character as u32, 0x00..=0x1f | 0x7f..=0x9f) && !matches!(character, '\n' | '\r')
}
fn push_control_escape(output: &mut String, character: char) {
output.push_str(&format!("\\u{:04x}", character as u32));
}
fn strip_complete_sequences(field: &str) -> std::borrow::Cow<'_, str> {
if !field
.chars()
.any(|character| matches!(character, '\u{1b}' | '\u{9b}' | '\u{9d}'))
{
return std::borrow::Cow::Borrowed(field);
}
let characters: Vec<char> = field.chars().collect();
let mut output = String::with_capacity(field.len());
let mut index = 0;
while index < characters.len() {
let character = characters[index];
let next = characters.get(index + 1).copied();
let csi_body = match (character, next) {
('\u{1b}', Some('[')) => Some(index + 2),
('\u{9b}', _) => Some(index + 1),
_ => None,
};
let osc_body = match (character, next) {
('\u{1b}', Some(']')) => Some(index + 2),
('\u{9d}', _) => Some(index + 1),
_ => None,
};
let end = if let Some(body) = csi_body {
csi_end(&characters, body)
} else if let Some(body) = osc_body {
osc_end(&characters, body)
} else {
None
};
match end {
Some(end) => index = end,
None => {
output.push(character);
index += 1;
}
}
}
std::borrow::Cow::Owned(output)
}
fn csi_end(characters: &[char], body: usize) -> Option<usize> {
for (offset, character) in characters[body..].iter().enumerate() {
match *character as u32 {
0x40..=0x7e => return Some(body + offset + 1),
0x20..=0x3f => {}
_ => return None,
}
}
None
}
fn osc_end(characters: &[char], body: usize) -> Option<usize> {
let mut index = body;
while index < characters.len() {
match characters[index] {
'\u{7}' | '\u{9c}' => return Some(index + 1),
'\u{1b}' if characters.get(index + 1) == Some(&'\\') => return Some(index + 2),
_ => index += 1,
}
}
None
}
fn escape_message(message: &str) -> String {
let cleaned = strip_complete_sequences(message);
let mut output = String::with_capacity(cleaned.len());
for character in cleaned.chars() {
match character {
'\\' => output.push_str("\\\\"),
'\r' => output.push_str("\\r"),
'\n' => output.push_str("\\n"),
control if is_escaped_control(control) => push_control_escape(&mut output, control),
other => output.push(other),
}
}
output
}
fn format_value(value: &str) -> String {
let cleaned = strip_complete_sequences(value);
let needs_quotes = cleaned.is_empty()
|| cleaned.chars().any(|character| {
matches!(character, ' ' | '"' | '\n' | '\r' | ']') || is_escaped_control(character)
});
if !needs_quotes {
return cleaned.into_owned();
}
let mut output = String::with_capacity(cleaned.len() + 2);
output.push('"');
for character in cleaned.chars() {
match character {
'\\' => output.push_str("\\\\"),
'"' => output.push_str("\\\""),
'\r' => output.push_str("\\r"),
'\n' => output.push_str("\\n"),
control if is_escaped_control(control) => push_control_escape(&mut output, control),
other => output.push(other),
}
}
output.push('"');
output
}
pub(crate) fn escape_raw_controls(line: &str) -> std::borrow::Cow<'_, str> {
if !line
.chars()
.any(|character| matches!(character, '\n' | '\r') || is_escaped_control(character))
{
return std::borrow::Cow::Borrowed(line);
}
let mut output = String::with_capacity(line.len());
for character in line.chars() {
match character {
'\r' => output.push_str("\\r"),
'\n' => output.push_str("\\n"),
control if is_escaped_control(control) => push_control_escape(&mut output, control),
other => output.push(other),
}
}
std::borrow::Cow::Owned(output)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ParsedLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedLine<'a> {
pub timestamp: SystemTime,
pub level: ParsedLevel,
pub logger: &'a str,
pub module_id: &'a str,
pub bound: Option<&'a str>,
pub body: &'a str,
}
impl ParsedLine<'_> {
pub fn session(&self) -> Option<&str> {
self.bound.and_then(|bound| bound_value(bound, "session"))
}
}
fn bound_value<'a>(bound: &'a str, key: &str) -> Option<&'a str> {
bound
.split(' ')
.filter_map(|pair| pair.split_once('='))
.find(|(candidate, _)| *candidate == key)
.map(|(_, value)| value)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ParseError {
reason: &'static str,
}
impl ParseError {
pub(crate) const fn new(reason: &'static str) -> Self {
Self { reason }
}
pub const fn reason(self) -> &'static str {
self.reason
}
}
impl fmt::Display for ParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.reason)
}
}
impl std::error::Error for ParseError {}
pub(crate) fn parse(line: &str) -> Result<ParsedLine<'_>, ParseError> {
if line.contains('\u{1b}') || line.contains('\u{9b}') {
return Err(ParseError::new("ansi_forbidden"));
}
if line.contains(['\n', '\r']) {
return Err(ParseError::new("line_break"));
}
let (timestamp_text, after_timestamp) = line
.split_once(' ')
.ok_or_else(|| ParseError::new("timestamp_missing"))?;
if !timestamp_text.ends_with('Z') {
return Err(ParseError::new("timestamp_not_utc_z"));
}
if timestamp_text.len() != 24 {
return Err(ParseError::new("timestamp_precision"));
}
let timestamp = DateTime::parse_from_rfc3339(timestamp_text)
.map_err(|_| ParseError::new("timestamp_invalid"))?;
let (level, after_level) = parse_level(after_timestamp)?;
let (logger_token, mut body) = match after_level.split_once(' ') {
Some(split) => split,
None => (after_level, ""),
};
let logger = logger_token
.strip_suffix(':')
.ok_or_else(|| ParseError::new("logger_not_terminated"))?;
if logger.is_empty() {
return Err(ParseError::new("logger_missing"));
}
if !logger.split('.').all(is_segment) {
return Err(ParseError::new("logger_segment_grammar"));
}
let module_id = logger.split('.').next().unwrap_or(logger);
let mut bound = None;
if let Some(rest) = body.strip_prefix('[') {
let close =
find_bracket_close(rest).ok_or_else(|| ParseError::new("bound_unterminated"))?;
let inside = &rest[..close];
if inside.is_empty() {
return Err(ParseError::new("empty_bound_bracket"));
}
if let Some(session) = bound_value(inside, "session") {
let valid = session
.rsplit_once(':')
.is_some_and(|(issuer, id)| !issuer.is_empty() && !id.is_empty());
if !valid {
return Err(ParseError::new("session_missing_issuer"));
}
}
bound = Some(inside);
body = rest[close + 1..]
.strip_prefix(' ')
.unwrap_or(&rest[close + 1..]);
}
if bound.is_none() && body.contains(" [") && body.ends_with(']') {
return Err(ParseError::new("bound_after_message"));
}
Ok(ParsedLine {
timestamp: SystemTime::from(timestamp),
level,
logger,
module_id,
bound,
body,
})
}
fn find_bracket_close(input: &str) -> Option<usize> {
let mut in_quotes = false;
let mut escaped = false;
for (index, character) in input.char_indices() {
if escaped {
escaped = false;
continue;
}
match character {
'\\' if in_quotes => escaped = true,
'"' => in_quotes = !in_quotes,
']' if !in_quotes => return Some(index),
_ => {}
}
}
None
}
fn parse_level(input: &str) -> Result<(ParsedLevel, &str), ParseError> {
for (prefix, level) in [
("TRACE ", ParsedLevel::Trace),
("DEBUG ", ParsedLevel::Debug),
("INFO ", ParsedLevel::Info),
("WARN ", ParsedLevel::Warn),
("ERROR ", ParsedLevel::Error),
] {
if let Some(rest) = input.strip_prefix(prefix) {
return Ok((level, rest));
}
}
if ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]
.iter()
.any(|level| input.starts_with(level))
{
Err(ParseError::new("level_column_width"))
} else {
Err(ParseError::new("level_invalid"))
}
}