// Syslog RFC 5424 and GELF format support
// RFC 5424 style: version timestamp hostname appname procid msgid [sd] msg
// Simplified: timestamp appname msg [fields]
///|
pub fn format_syslog(record : LogRecord, app_name : String) -> String {
let pri = syslog_priority(record.level)
let ts = record.timestamp.to_string()
let fields_str = build_syslog_sd(record.fields, 0, "")
"<" +
pri.to_string() +
">" +
"1 " +
ts +
" " +
sanitize_syslog_token(app_name, "moonlogtrace") +
" - - - " +
fields_str +
" " +
sanitize_syslog_message(record.message)
}
///|
fn syslog_priority(level : Level) -> Int {
// Facility 1 (user) + severity
let facility = 1
let severity = match level {
Level::TRACE => 7
Level::DEBUG => 7
Level::INFO => 6
Level::WARN => 4
Level::ERROR => 3
}
facility * 8 + severity
}
///|
fn build_syslog_sd(
fields : Array[(String, String)],
idx : Int,
result : String,
) -> String {
if idx >= fields.length() {
if result == "" {
"-"
} else {
"[" + result + "]"
}
} else {
let (k, v) = fields[idx]
let pair = sanitize_syslog_token(k, "field") +
"=\"" +
escape_syslog_param(v) +
"\""
let sep = if idx > 0 { " " } else { "" }
build_syslog_sd(fields, idx + 1, result + sep + pair)
}
}
///|
fn sanitize_syslog_token(text : String, fallback : String) -> String {
let chars = text.to_array()
if chars.length() == 0 {
fallback
} else {
sanitize_syslog_token_chars(chars, 0, "")
}
}
///|
fn sanitize_syslog_token_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_syslog_token_chars(chars, idx + 1, result + safe)
}
}
///|
fn escape_syslog_param(text : String) -> String {
escape_syslog_param_chars(text.to_array(), 0, "")
}
///|
fn escape_syslog_param_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 == '\n' {
"\\n"
} else if ch == '\r' {
"\\r"
} else if ch.is_control() {
"_"
} else {
ch.to_string()
}
escape_syslog_param_chars(chars, idx + 1, result + escaped)
}
}
///|
fn sanitize_syslog_message(text : String) -> String {
escape_syslog_message_chars(text.to_array(), 0, "")
}
///|
fn escape_syslog_message_chars(
chars : Array[Char],
idx : Int,
result : String,
) -> String {
if idx >= chars.length() {
result
} else {
let ch = chars[idx]
let safe = if ch == '\n' {
"\\n"
} else if ch == '\r' {
"\\r"
} else if ch.is_control() && ch != '\t' {
"_"
} else {
ch.to_string()
}
escape_syslog_message_chars(chars, idx + 1, result + safe)
}
}
// GELF (Graylog Extended Log Format) — JSON-based
///|
pub fn format_gelf(record : LogRecord, host : String) -> String {
let result = "{\"version\":\"1.1\",\"host\":" +
json_quote(host) +
",\"short_message\":" +
json_quote(record.message) +
",\"timestamp\":" +
record.timestamp.to_string() +
",\"level\":" +
gelf_level(record.level).to_string()
format_gelf_fields(result, record.fields, 0)
}
///|
fn gelf_level(level : Level) -> Int {
match level {
Level::TRACE => 7
Level::DEBUG => 7
Level::INFO => 6
Level::WARN => 4
Level::ERROR => 3
}
}
///|
fn format_gelf_fields(
result : String,
fields : Array[(String, String)],
idx : Int,
) -> String {
if idx >= fields.length() {
result + "}"
} else {
let (k, v) = fields[idx]
let kv = "," + json_quote("_" + k) + ":" + json_quote(v)
format_gelf_fields(result + kv, fields, idx + 1)
}
}