///|
fn level_label(level : Level) -> String {
level_to_string(level).trim().to_owned()
}
///|
fn json_quote(text : String) -> String {
Json::string(text).stringify()
}
///|
fn escape_xml_text(text : String) -> String {
escape_xml_chars(text.to_array(), 0, "")
}
///|
fn escape_xml_chars(chars : Array[Char], idx : Int, result : String) -> String {
if idx >= chars.length() {
result
} else {
let ch = chars[idx]
let escaped = if ch == '&' {
"&"
} else if ch == '<' {
"<"
} else if ch == '>' {
">"
} else if ch == '"' {
"""
} else if ch == '\'' {
"'"
} else if ch.is_control() && ch != '\t' && ch != '\n' && ch != '\r' {
"�"
} else {
ch.to_string()
}
escape_xml_chars(chars, idx + 1, result + escaped)
}
}
///|
fn escape_logfmt_value(text : String) -> String {
let chars = text.to_array()
let escaped = escape_logfmt_chars(chars, 0, "")
if chars.length() == 0 || should_quote_logfmt(chars, 0) {
"\"" + escaped + "\""
} else {
escaped
}
}
///|
fn escape_logfmt_chars(
chars : Array[Char],
idx : Int,
result : String,
) -> String {
if idx >= chars.length() {
result
} else {
let ch = chars[idx]
let escaped = if ch == '\\' {
"\\\\"
} else if ch == '"' {
"\\\""
} else if ch == '\n' {
"\\n"
} else if ch == '\r' {
"\\r"
} else if ch == '\t' {
"\\t"
} else if ch.is_control() {
"_"
} else {
ch.to_string()
}
escape_logfmt_chars(chars, idx + 1, result + escaped)
}
}
///|
fn should_quote_logfmt(chars : Array[Char], idx : Int) -> Bool {
if idx >= chars.length() {
false
} else {
let ch = chars[idx]
if ch.is_whitespace() ||
ch.is_control() ||
ch == '=' ||
ch == '"' ||
ch == '\\' {
true
} else {
should_quote_logfmt(chars, idx + 1)
}
}
}
///|
fn sanitize_logfmt_key(text : String) -> String {
let chars = text.to_array()
if chars.length() == 0 {
"_"
} else {
sanitize_logfmt_key_chars(chars, 0, "")
}
}
///|
fn sanitize_logfmt_key_chars(
chars : Array[Char],
idx : Int,
result : String,
) -> String {
if idx >= chars.length() {
result
} else {
let ch = chars[idx]
let safe = if ch.is_whitespace() ||
ch.is_control() ||
ch == '=' ||
ch == '"' ||
ch == '\\' {
"_"
} else {
ch.to_string()
}
sanitize_logfmt_key_chars(chars, idx + 1, result + safe)
}
}
///|
pub fn format_text(record : LogRecord) -> String {
let result = "[" +
record.timestamp.to_string() +
"] " +
level_to_string(record.level) +
" " +
record.message
format_fields(result, record.fields, 0)
}
///|
fn format_fields(
result : String,
fields : Array[(String, String)],
idx : Int,
) -> String {
if idx >= fields.length() {
result
} else {
let (k, v) = fields[idx]
let new_result = result + " " + k + "=" + v
format_fields(new_result, fields, idx + 1)
}
}
///|
pub fn format_json(record : LogRecord) -> String {
let result = "{\"time\":" +
json_quote(record.timestamp.to_string()) +
",\"level\":" +
json_quote(level_label(record.level)) +
",\"msg\":" +
json_quote(record.message)
format_json_fields(result, record.fields, 0)
}
///|
fn format_json_fields(
result : String,
fields : Array[(String, String)],
idx : Int,
) -> String {
if idx >= fields.length() {
result + "}"
} else {
let (k, v) = fields[idx]
let new_result = result + "," + json_quote(k) + ":" + json_quote(v)
format_json_fields(new_result, fields, idx + 1)
}
}
// ANSI color codes for terminal output
///|
fn ansi_color(level : Level) -> String {
match level {
Level::TRACE => "[90m"
Level::DEBUG => "[36m"
Level::INFO => "[32m"
Level::WARN => "[33m"
Level::ERROR => "[31m"
}
}
///|
fn ansi_reset() -> String {
"[0m"
}
///|
pub fn format_color(record : LogRecord) -> String {
let color = ansi_color(record.level)
let result = color +
"[" +
record.timestamp.to_string() +
"] " +
level_to_string(record.level) +
" " +
record.message +
ansi_reset()
format_fields(result, record.fields, 0)
}
// Compact single-line format: LEVEL|timestamp|msg|k=v|k=v
///|
pub fn format_compact(record : LogRecord) -> String {
let result = level_label(record.level) +
"|" +
record.timestamp.to_string() +
"|" +
record.message
format_compact_fields(result, record.fields, 0)
}
///|
fn format_compact_fields(
result : String,
fields : Array[(String, String)],
idx : Int,
) -> String {
if idx >= fields.length() {
result
} else {
let (k, v) = fields[idx]
let new_result = result + "|" + k + "=" + v
format_compact_fields(new_result, fields, idx + 1)
}
}
// CSV format: timestamp,level,message,key1,val1,key2,val2,...
///|
pub fn format_csv(record : LogRecord) -> String {
let result = escape_csv_field(record.timestamp.to_string()) +
"," +
escape_csv_field(level_label(record.level)) +
"," +
escape_csv_field(record.message)
format_csv_fields(result, record.fields, 0)
}
///|
fn format_csv_fields(
result : String,
fields : Array[(String, String)],
idx : Int,
) -> String {
if idx >= fields.length() {
result
} else {
let (k, v) = fields[idx]
let new_result = result +
"," +
escape_csv_field(k) +
"," +
escape_csv_field(v)
format_csv_fields(new_result, fields, idx + 1)
}
}
///|
fn escape_csv_field(text : String) -> String {
let chars = text.to_array()
if csv_needs_quotes(chars, 0) {
"\"" + escape_csv_quotes(chars, 0, "") + "\""
} else {
text
}
}
///|
fn csv_needs_quotes(chars : Array[Char], idx : Int) -> Bool {
if idx >= chars.length() {
false
} else {
let ch = chars[idx]
if ch == ',' || ch == '"' || ch == '\n' || ch == '\r' {
true
} else {
csv_needs_quotes(chars, idx + 1)
}
}
}
///|
fn escape_csv_quotes(chars : Array[Char], idx : Int, result : String) -> String {
if idx >= chars.length() {
result
} else {
let ch = chars[idx]
let escaped = if ch == '"' { "\"\"" } else { ch.to_string() }
escape_csv_quotes(chars, idx + 1, result + escaped)
}
}
// Format without timestamp (for testing)
///|
pub fn format_plain(record : LogRecord) -> String {
let result = level_label(record.level) + " " + record.message
format_fields(result, record.fields, 0)
}
// Pattern formatter with configurable format
// Supports: {t} = timestamp, {l} = level, {m} = message, {f} = fields
///|
pub fn format_pattern(record : LogRecord, pattern : String) -> String {
replace_pattern(pattern, record, 0, "")
}
///|
fn replace_pattern(
pattern : String,
record : LogRecord,
idx : Int,
result : String,
) -> String {
if idx >= pattern.length() {
result
} else {
let ch = pattern[idx:idx + 1].to_owned()
if ch == "{" {
if idx + 2 < pattern.length() &&
pattern[idx + 2:idx + 3].to_owned() == "}" {
let key = pattern[idx + 1:idx + 2].to_owned()
let replacement = get_pattern_value(key, record)
replace_pattern(pattern, record, idx + 3, result + replacement)
} else {
replace_pattern(pattern, record, idx + 1, result + ch)
}
} else {
replace_pattern(pattern, record, idx + 1, result + ch)
}
}
}
///|
fn get_pattern_value(key : String, record : LogRecord) -> String {
if key == "t" {
record.timestamp.to_string()
} else if key == "l" {
level_label(record.level)
} else if key == "m" {
record.message
} else if key == "f" {
format_all_fields(record.fields, 0, "")
} else {
"{" + key + "}"
}
}
///|
fn format_all_fields(
fields : Array[(String, String)],
idx : Int,
result : String,
) -> String {
if idx >= fields.length() {
result
} else {
let (k, v) = fields[idx]
let sep = if idx > 0 { " " } else { "" }
format_all_fields(fields, idx + 1, result + sep + k + "=" + v)
}
}
// Logfmt format: key=value pairs, level and message as special keys
///|
pub fn format_logfmt(record : LogRecord) -> String {
let result = "ts=" +
record.timestamp.to_string() +
" level=" +
level_label(record.level) +
" msg=" +
escape_logfmt_value(record.message)
format_logfmt_fields(result, record.fields, 0)
}
///|
fn format_logfmt_fields(
result : String,
fields : Array[(String, String)],
idx : Int,
) -> String {
if idx >= fields.length() {
result
} else {
let (k, v) = fields[idx]
let new_result = result +
" " +
sanitize_logfmt_key(k) +
"=" +
escape_logfmt_value(v)
format_logfmt_fields(new_result, fields, idx + 1)
}
}
// XML format
///|
pub fn format_xml(record : LogRecord) -> String {
let result = "" +
escape_xml_text(record.message) +
""
format_xml_fields(result, record.fields, 0) + ""
}
///|
fn format_xml_fields(
result : String,
fields : Array[(String, String)],
idx : Int,
) -> String {
if idx >= fields.length() {
result
} else {
let (k, v) = fields[idx]
let new_result = result +
"" +
escape_xml_text(v) +
""
format_xml_fields(new_result, fields, idx + 1)
}
}
// NDJSON (Newline Delimited JSON) — same as JSON but guaranteed single line
///|
pub fn format_ndjson(record : LogRecord) -> String {
format_json(record)
}