#![allow(dead_code)]
use crate::colors::ColorScheme;
use crate::event::{flatten_dynamic, Event, FlattenStyle};
use crate::pipeline;
use rhai::Dynamic;
use std::collections::HashMap;
use std::sync::Mutex;
use once_cell::sync::Lazy;
static CSV_FORMATTER_HEADER_REGISTRY: Lazy<Mutex<HashMap<String, bool>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
#[cfg(test)]
use crate::pipeline::Formatter;
fn escape_single_quote_string(input: &str) -> String {
let mut output = String::with_capacity(input.len() + 10);
for ch in input.chars() {
match ch {
'\'' => output.push_str("\\'"),
'\\' => output.push_str("\\\\"),
'\n' => output.push_str("\\n"),
'\t' => output.push_str("\\t"),
'\r' => output.push_str("\\r"),
_ => output.push(ch),
}
}
output
}
fn escape_logfmt_string(input: &str) -> String {
let mut output = String::with_capacity(input.len() + 10);
for ch in input.chars() {
match ch {
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
'\n' => output.push_str("\\n"),
'\t' => output.push_str("\\t"),
'\r' => output.push_str("\\r"),
_ => output.push(ch),
}
}
output
}
fn needs_single_quote_quoting(value: &str) -> bool {
value.is_empty()
|| value.contains(' ')
|| value.contains('\t')
|| value.contains('\n')
|| value.contains('\r')
|| value.contains('\'')
|| value.contains('=')
}
fn needs_logfmt_quoting(value: &str) -> bool {
value.is_empty()
|| value.contains(' ')
|| value.contains('\t')
|| value.contains('\n')
|| value.contains('\r')
|| value.contains('"')
|| value.contains('=')
}
fn format_dynamic_value(value: &Dynamic) -> (String, bool) {
if value.is_string() {
if let Ok(s) = value.clone().into_string() {
(s, true) } else {
(value.to_string(), false)
}
} else {
(value.to_string(), false)
}
}
fn format_quoted_logfmt_value(value: &str, output: &mut String) {
if needs_logfmt_quoting(value) {
output.push('"');
output.push_str(&escape_logfmt_string(value));
output.push('"');
} else {
output.push_str(value);
}
}
fn dynamic_to_json(value: &Dynamic) -> serde_json::Value {
if value.is_string() {
if let Ok(s) = value.clone().into_string() {
serde_json::Value::String(s)
} else {
serde_json::Value::Null
}
} else if value.is_int() {
if let Ok(i) = value.as_int() {
serde_json::Value::Number(serde_json::Number::from(i))
} else {
serde_json::Value::Null
}
} else if value.is_float() {
if let Ok(f) = value.as_float() {
serde_json::Number::from_f64(f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)
} else {
serde_json::Value::Null
}
} else if value.is_bool() {
if let Ok(b) = value.as_bool() {
serde_json::Value::Bool(b)
} else {
serde_json::Value::Null
}
} else if value.is_unit() {
serde_json::Value::Null
} else if let Some(arr) = value.clone().try_cast::<rhai::Array>() {
let json_array: Vec<serde_json::Value> = arr.iter().map(dynamic_to_json).collect();
serde_json::Value::Array(json_array)
} else if let Some(map) = value.clone().try_cast::<rhai::Map>() {
let mut json_obj = serde_json::Map::new();
for (key, val) in map {
json_obj.insert(key.to_string(), dynamic_to_json(&val));
}
serde_json::Value::Object(json_obj)
} else {
serde_json::Value::String(value.to_string())
}
}
fn needs_csv_quoting(value: &str, delimiter: char) -> bool {
value.is_empty()
|| value.contains(delimiter)
|| value.contains('"')
|| value.contains('\n')
|| value.contains('\r')
|| value.starts_with(' ')
|| value.ends_with(' ')
}
fn escape_csv_value(value: &str, delimiter: char) -> String {
if needs_csv_quoting(value, delimiter) {
let escaped = value.replace('"', "\"\"");
format!("\"{}\"", escaped)
} else {
value.to_string()
}
}
pub struct JsonFormatter;
impl JsonFormatter {
pub fn new() -> Self {
Self
}
}
impl pipeline::Formatter for JsonFormatter {
fn format(&self, event: &Event) -> String {
let mut json_obj = serde_json::Map::new();
for (key, value) in &event.fields {
let json_value = dynamic_to_json(value);
json_obj.insert(key.clone(), json_value);
}
serde_json::to_string(&serde_json::Value::Object(json_obj))
.unwrap_or_else(|_| "{}".to_string())
}
}
pub struct DefaultFormatter {
colors: ColorScheme,
level_keys: Vec<&'static str>,
brief: bool,
timestamp_formatting: crate::config::TimestampFormatConfig,
enable_wrapping: bool,
terminal_width: usize,
}
impl DefaultFormatter {
pub fn new(
use_colors: bool,
brief: bool,
timestamp_formatting: crate::config::TimestampFormatConfig,
) -> Self {
Self {
colors: ColorScheme::new(use_colors),
level_keys: vec![
"level",
"loglevel",
"log_level",
"lvl",
"severity",
"levelname",
"@l",
],
brief,
timestamp_formatting,
enable_wrapping: true, terminal_width: crate::tty::get_terminal_width(),
}
}
pub fn new_with_wrapping(
use_colors: bool,
brief: bool,
timestamp_formatting: crate::config::TimestampFormatConfig,
enable_wrapping: bool,
) -> Self {
let terminal_width = if enable_wrapping {
crate::tty::get_terminal_width()
} else {
100 };
Self {
colors: ColorScheme::new(use_colors),
level_keys: vec![
"level",
"loglevel",
"log_level",
"lvl",
"severity",
"levelname",
"@l",
],
brief,
timestamp_formatting,
enable_wrapping,
terminal_width,
}
}
fn format_dynamic_value_into(&self, key: &str, value: &Dynamic, output: &mut String) {
if self.should_format_as_timestamp(key) {
if let Some(formatted_ts) = self.try_format_timestamp(value) {
if !self.colors.string.is_empty() {
output.push_str(self.colors.string);
}
output.push('\'');
output.push_str(&escape_single_quote_string(&formatted_ts));
output.push('\'');
if !self.colors.string.is_empty() {
output.push_str(self.colors.reset);
}
return;
}
}
let color = if self.is_level_field(key) {
if let Ok(level_str) = value.clone().into_string() {
self.level_color(&level_str)
} else {
self.colors.string
}
} else {
self.colors.string
};
let (string_val, is_string) = self.format_default_value(value);
if is_string {
output.push('\'');
if !color.is_empty() {
output.push_str(color);
}
output.push_str(&escape_single_quote_string(&string_val));
if !color.is_empty() {
output.push_str(self.colors.reset);
}
output.push('\'')
} else {
if !color.is_empty() {
output.push_str(color);
}
output.push_str(&string_val);
if !color.is_empty() {
output.push_str(self.colors.reset);
}
}
}
fn format_dynamic_value_brief_into(&self, key: &str, value: &Dynamic, output: &mut String) {
if self.should_format_as_timestamp(key) {
if let Some(formatted_ts) = self.try_format_timestamp(value) {
if !self.colors.string.is_empty() {
output.push_str(self.colors.string);
}
output.push_str(&formatted_ts);
if !self.colors.string.is_empty() {
output.push_str(self.colors.reset);
}
return;
}
}
let color = if self.is_level_field(key) {
if let Ok(level_str) = value.clone().into_string() {
self.level_color(&level_str)
} else {
self.colors.string
}
} else {
self.colors.string
};
if !color.is_empty() {
output.push_str(color);
}
let (formatted_val, _is_string) = self.format_default_value(value);
output.push_str(&formatted_val);
if !color.is_empty() {
output.push_str(self.colors.reset);
}
}
fn level_color(&self, level: &str) -> &str {
match level.to_lowercase().as_str() {
"error" | "err" | "fatal" | "panic" | "alert" | "crit" | "critical" | "emerg"
| "emergency" | "severe" => self.colors.level_error,
"warn" | "warning" => self.colors.level_warn,
"info" | "informational" | "notice" => self.colors.level_info,
"debug" | "finer" | "config" => self.colors.level_debug,
"trace" | "finest" => self.colors.level_trace,
_ => "",
}
}
fn is_level_field(&self, key: &str) -> bool {
self.level_keys.contains(&key)
}
fn should_format_as_timestamp(&self, key: &str) -> bool {
if self
.timestamp_formatting
.format_fields
.contains(&key.to_string())
{
return true;
}
if self.timestamp_formatting.auto_format_all {
return crate::event::TIMESTAMP_FIELD_NAMES.contains(&key);
}
false
}
fn try_format_timestamp(&self, value: &Dynamic) -> Option<String> {
use chrono::{DateTime, Local, Utc};
if let Some(dt) = value.clone().try_cast::<DateTime<Utc>>() {
return Some(if self.timestamp_formatting.format_as_utc {
dt.to_rfc3339()
} else {
dt.with_timezone(&Local).to_rfc3339()
});
}
if let Ok(timestamp_num) = value.as_int() {
let timestamp_str = timestamp_num.to_string();
let mut parser = crate::timestamp::AdaptiveTsParser::new();
if let Some(parsed_dt) = parser.parse_ts(×tamp_str) {
return Some(if self.timestamp_formatting.format_as_utc {
parsed_dt.to_rfc3339()
} else {
parsed_dt.with_timezone(&Local).to_rfc3339()
});
}
}
if let Ok(timestamp_float) = value.as_float() {
use chrono::DateTime;
let parsed_dt = if timestamp_float >= 1e15 {
DateTime::from_timestamp(
(timestamp_float / 1_000_000.0).floor() as i64,
((timestamp_float % 1_000_000.0) * 1000.0) as u32,
)
} else if timestamp_float >= 1e12 {
DateTime::from_timestamp(
(timestamp_float / 1000.0).floor() as i64,
((timestamp_float % 1000.0) * 1_000_000.0) as u32,
)
} else if timestamp_float >= 1e9 {
DateTime::from_timestamp(
timestamp_float.floor() as i64,
(timestamp_float.fract() * 1_000_000_000.0) as u32,
)
} else {
None
};
if let Some(dt) = parsed_dt {
let utc_dt = dt.with_timezone(&Utc);
return Some(if self.timestamp_formatting.format_as_utc {
utc_dt.to_rfc3339()
} else {
utc_dt.with_timezone(&Local).to_rfc3339()
});
}
}
if let Ok(ts_str) = value.clone().into_string() {
let mut parser = crate::timestamp::AdaptiveTsParser::new();
if let Some(parsed_dt) = parser.parse_ts(&ts_str) {
return Some(if self.timestamp_formatting.format_as_utc {
parsed_dt.to_rfc3339()
} else {
parsed_dt.with_timezone(&Local).to_rfc3339()
});
}
}
None
}
fn format_default_value(&self, value: &Dynamic) -> (String, bool) {
if value.clone().try_cast::<rhai::Map>().is_some()
|| value.clone().try_cast::<rhai::Array>().is_some()
{
let flattened = flatten_dynamic(value, FlattenStyle::Bracket, 0);
if flattened.len() == 1 {
let val = flattened.values().next().unwrap().to_string();
(val, false) } else if flattened.is_empty() {
(String::new(), true)
} else {
let formatted = flattened
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(" ");
(formatted, true) }
} else {
format_dynamic_value(value)
}
}
}
impl pipeline::Formatter for DefaultFormatter {
fn format(&self, event: &Event) -> String {
if event.fields.is_empty() {
return String::new();
}
if !self.enable_wrapping {
return self.format_single_line(event);
}
let estimated_capacity = event.fields.len() * 32;
let mut output = String::with_capacity(estimated_capacity);
let mut current_line_length = 0;
let mut first_on_line = true;
let mut first_overall = true;
for (key, value) in &event.fields {
let mut field_output = String::new();
if self.brief {
self.format_dynamic_value_brief_into(key, value, &mut field_output);
} else {
if !self.colors.key.is_empty() {
field_output.push_str(self.colors.key);
}
field_output.push_str(key);
if !self.colors.key.is_empty() {
field_output.push_str(self.colors.reset);
}
if !self.colors.equals.is_empty() {
field_output.push_str(self.colors.equals);
}
field_output.push('=');
if !self.colors.equals.is_empty() {
field_output.push_str(self.colors.reset);
}
self.format_dynamic_value_into(key, value, &mut field_output);
}
let field_display_length = self.display_length(&field_output);
let space_needed = if first_on_line { 0 } else { 1 };
if !first_overall
&& current_line_length + space_needed + field_display_length > self.terminal_width
{
output.push('\n');
output.push_str(" "); current_line_length = 2; first_on_line = true;
}
if !first_on_line {
output.push(' ');
current_line_length += 1;
}
output.push_str(&field_output);
current_line_length += field_display_length;
first_on_line = false;
first_overall = false;
}
output
}
}
impl DefaultFormatter {
fn format_single_line(&self, event: &Event) -> String {
let estimated_capacity = event.fields.len() * 32;
let mut output = String::with_capacity(estimated_capacity);
let mut first = true;
for (key, value) in &event.fields {
if !first {
output.push(' ');
}
first = false;
if self.brief {
self.format_dynamic_value_brief_into(key, value, &mut output);
} else {
if !self.colors.key.is_empty() {
output.push_str(self.colors.key);
}
output.push_str(key);
if !self.colors.key.is_empty() {
output.push_str(self.colors.reset);
}
if !self.colors.equals.is_empty() {
output.push_str(self.colors.equals);
}
output.push('=');
if !self.colors.equals.is_empty() {
output.push_str(self.colors.reset);
}
self.format_dynamic_value_into(key, value, &mut output);
}
}
output
}
fn display_length(&self, text: &str) -> usize {
let mut length = 0;
let mut in_escape = false;
for ch in text.chars() {
if ch == '\x1b' {
in_escape = true;
} else if in_escape && ch == 'm' {
in_escape = false;
} else if !in_escape {
length += 1;
}
}
length
}
}
pub struct HideFormatter;
impl HideFormatter {
pub fn new() -> Self {
Self
}
}
impl pipeline::Formatter for HideFormatter {
fn format(&self, _event: &Event) -> String {
String::new()
}
}
fn sanitize_logfmt_key(key: &str) -> String {
key.chars()
.map(|c| match c {
' ' | '\t' | '\n' | '\r' | '=' => '_',
c => c,
})
.collect()
}
pub struct LogfmtFormatter;
impl LogfmtFormatter {
pub fn new() -> Self {
Self
}
fn format_dynamic_value_into(&self, value: &Dynamic, output: &mut String) {
let string_val = self.format_logfmt_value(value);
let is_string = value.is_string();
if is_string {
format_quoted_logfmt_value(&string_val, output);
} else {
output.push_str(&string_val);
}
}
fn format_logfmt_value(&self, value: &Dynamic) -> String {
if value.clone().try_cast::<rhai::Map>().is_some()
|| value.clone().try_cast::<rhai::Array>().is_some()
{
let flattened = flatten_dynamic(value, FlattenStyle::Underscore, 0);
if flattened.len() == 1 {
flattened.values().next().unwrap().to_string()
} else if flattened.is_empty() {
String::new()
} else {
flattened
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(",")
}
} else {
value.to_string()
}
}
}
impl pipeline::Formatter for LogfmtFormatter {
fn format(&self, event: &Event) -> String {
if event.fields.is_empty() {
return String::new();
}
let estimated_capacity = event.fields.len() * 32;
let mut output = String::with_capacity(estimated_capacity);
let mut first = true;
for (key, value) in &event.fields {
if !first {
output.push(' ');
}
first = false;
let sanitized_key = sanitize_logfmt_key(key);
output.push_str(&sanitized_key);
output.push('=');
self.format_dynamic_value_into(value, &mut output);
}
output
}
}
pub struct CsvFormatter {
delimiter: char,
keys: Vec<String>,
include_header: bool,
formatter_key: String,
worker_mode: bool, }
impl CsvFormatter {
pub fn new(keys: Vec<String>) -> Self {
let formatter_key = format!(",_{}", Self::keys_hash(&keys));
Self {
delimiter: ',',
keys,
include_header: true,
formatter_key,
worker_mode: false,
}
}
pub fn new_tsv(keys: Vec<String>) -> Self {
let formatter_key = format!("\t_{}", Self::keys_hash(&keys));
Self {
delimiter: '\t',
keys,
include_header: true,
formatter_key,
worker_mode: false,
}
}
pub fn new_csv_no_header(keys: Vec<String>) -> Self {
let formatter_key = format!(",_noheader_{}", Self::keys_hash(&keys));
Self {
delimiter: ',',
keys,
include_header: false,
formatter_key,
worker_mode: false,
}
}
pub fn new_tsv_no_header(keys: Vec<String>) -> Self {
let formatter_key = format!("\t_noheader_{}", Self::keys_hash(&keys));
Self {
delimiter: '\t',
keys,
include_header: false,
formatter_key,
worker_mode: false,
}
}
pub fn new_worker(keys: Vec<String>) -> Self {
let formatter_key = format!(",_worker_{}", Self::keys_hash(&keys));
Self {
delimiter: ',',
keys,
include_header: false, formatter_key,
worker_mode: true,
}
}
pub fn new_tsv_worker(keys: Vec<String>) -> Self {
let formatter_key = format!("\t_worker_{}", Self::keys_hash(&keys));
Self {
delimiter: '\t',
keys,
include_header: false, formatter_key,
worker_mode: true,
}
}
pub fn new_csv_no_header_worker(keys: Vec<String>) -> Self {
let formatter_key = format!(",_noheader_worker_{}", Self::keys_hash(&keys));
Self {
delimiter: ',',
keys,
include_header: false,
formatter_key,
worker_mode: true,
}
}
pub fn new_tsv_no_header_worker(keys: Vec<String>) -> Self {
let formatter_key = format!("\t_noheader_worker_{}", Self::keys_hash(&keys));
Self {
delimiter: '\t',
keys,
include_header: false,
formatter_key,
worker_mode: true,
}
}
fn keys_hash(keys: &[String]) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
keys.hash(&mut hasher);
hasher.finish()
}
fn mark_header_written_globally(&self) -> bool {
let mut registry = CSV_FORMATTER_HEADER_REGISTRY.lock().unwrap();
if registry.get(&self.formatter_key).copied().unwrap_or(false) {
false
} else {
registry.insert(self.formatter_key.clone(), true);
true
}
}
pub fn format_header(&self) -> String {
self.keys
.iter()
.map(|key| escape_csv_value(key, self.delimiter))
.collect::<Vec<_>>()
.join(&self.delimiter.to_string())
}
fn format_data_row(&self, event: &Event) -> String {
self.keys
.iter()
.map(|key| {
if let Some(value) = event.fields.get(key) {
let string_value = self.format_csv_value(value);
escape_csv_value(&string_value, self.delimiter)
} else {
String::new() }
})
.collect::<Vec<_>>()
.join(&self.delimiter.to_string())
}
fn format_csv_value(&self, value: &Dynamic) -> String {
if value.clone().try_cast::<rhai::Map>().is_some()
|| value.clone().try_cast::<rhai::Array>().is_some()
{
let flattened = flatten_dynamic(value, FlattenStyle::Underscore, 0);
if flattened.len() == 1 {
flattened.values().next().unwrap().to_string()
} else if flattened.is_empty() {
String::new()
} else {
flattened
.iter()
.map(|(k, v)| format!("{}:{}", k, v))
.collect::<Vec<_>>()
.join(",")
}
} else {
value.to_string()
}
}
}
impl pipeline::Formatter for CsvFormatter {
fn format(&self, event: &Event) -> String {
let mut output = String::new();
if !self.worker_mode && self.include_header && self.mark_header_written_globally() {
output.push_str(&self.format_header());
output.push('\n');
}
output.push_str(&self.format_data_row(event));
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_formatter_empty_event() {
let event = Event::default();
let formatter = JsonFormatter::new();
let result = formatter.format(&event);
assert!(result.starts_with('{') && result.ends_with('}'));
}
#[test]
fn test_json_formatter_with_fields() {
let mut event = Event::default();
event.set_field("level".to_string(), Dynamic::from("INFO".to_string()));
event.set_field("msg".to_string(), Dynamic::from("Test message".to_string()));
event.set_field("user".to_string(), Dynamic::from("alice".to_string()));
event.set_field("status".to_string(), Dynamic::from(200i64));
let formatter = JsonFormatter::new();
let result = formatter.format(&event);
assert!(result.contains("\"level\":\"INFO\""));
assert!(result.contains("\"msg\":\"Test message\""));
assert!(result.contains("\"user\":\"alice\""));
assert!(result.contains("\"status\":200"));
}
#[test]
fn test_default_formatter() {
let mut event = Event::default();
event.set_field("level".to_string(), Dynamic::from("INFO".to_string()));
event.set_field("user".to_string(), Dynamic::from("alice".to_string()));
event.set_field("count".to_string(), Dynamic::from(42i64));
let formatter = DefaultFormatter::new_with_wrapping(
false,
false,
crate::config::TimestampFormatConfig::default(),
false, ); let result = formatter.format(&event);
assert!(result.contains("level='INFO'"));
assert!(result.contains("user='alice'"));
assert!(result.contains("count=42"));
assert!(result.contains(" "));
}
#[test]
fn test_default_formatter_brief_mode() {
let mut event = Event::default();
event.set_field("level".to_string(), Dynamic::from("info".to_string()));
event.set_field("msg".to_string(), Dynamic::from("test message".to_string()));
let formatter = DefaultFormatter::new_with_wrapping(
false,
true,
crate::config::TimestampFormatConfig::default(),
false, ); let result = formatter.format(&event);
assert_eq!(result, "info test message");
}
#[test]
fn test_logfmt_formatter_basic() {
let mut event = Event::default();
event.set_field("level".to_string(), Dynamic::from("INFO".to_string()));
event.set_field("msg".to_string(), Dynamic::from("Test message".to_string()));
event.set_field("user".to_string(), Dynamic::from("alice".to_string()));
event.set_field("status".to_string(), Dynamic::from(200i64));
let formatter = LogfmtFormatter::new();
let result = formatter.format(&event);
assert!(result.contains("level=INFO"));
assert!(result.contains("msg=\"Test message\""));
assert!(result.contains("user=alice"));
assert!(result.contains("status=200"));
assert!(result.contains(" "));
}
#[test]
fn test_logfmt_formatter_quoting() {
let mut event = Event::default();
event.set_field("simple".to_string(), Dynamic::from("value".to_string()));
event.set_field(
"spaced".to_string(),
Dynamic::from("has spaces".to_string()),
);
event.set_field("empty".to_string(), Dynamic::from("".to_string()));
event.set_field(
"quoted".to_string(),
Dynamic::from("has\"quotes".to_string()),
);
event.set_field("equals".to_string(), Dynamic::from("has=sign".to_string()));
let formatter = LogfmtFormatter::new();
let result = formatter.format(&event);
assert!(result.contains("simple=value")); assert!(result.contains("spaced=\"has spaces\"")); assert!(result.contains("empty=\"\"")); assert!(result.contains("quoted=\"has\\\"quotes\"")); assert!(result.contains("equals=\"has=sign\"")); }
#[test]
fn test_logfmt_formatter_types() {
let mut event = Event::default();
event.set_field("string".to_string(), Dynamic::from("hello".to_string()));
event.set_field("integer".to_string(), Dynamic::from(42i64));
event.set_field("float".to_string(), Dynamic::from(2.5f64));
event.set_field("bool_true".to_string(), Dynamic::from(true));
event.set_field("bool_false".to_string(), Dynamic::from(false));
let formatter = LogfmtFormatter::new();
let result = formatter.format(&event);
assert!(result.contains("string=hello"));
assert!(result.contains("integer=42"));
assert!(result.contains("float=2.5"));
assert!(result.contains("bool_true=true"));
assert!(result.contains("bool_false=false"));
}
#[test]
fn test_logfmt_formatter_empty_event() {
let event = Event::default();
let formatter = LogfmtFormatter::new();
let result = formatter.format(&event);
assert_eq!(result, "");
}
#[test]
fn test_logfmt_formatter_key_sanitization() {
let mut event = Event::default();
event.set_field(
"field with spaces".to_string(),
Dynamic::from("value1".to_string()),
);
event.set_field(
"field=with=equals".to_string(),
Dynamic::from("value2".to_string()),
);
event.set_field(
"field\twith\ttabs".to_string(),
Dynamic::from("value3".to_string()),
);
event.set_field(
"field\nwith\nnewlines".to_string(),
Dynamic::from("value4".to_string()),
);
event.set_field(
"field\rwith\rcarriage".to_string(),
Dynamic::from("value5".to_string()),
);
event.set_field(
"normal_field".to_string(),
Dynamic::from("value6".to_string()),
);
event.set_field(
"field-with-dashes".to_string(),
Dynamic::from("value7".to_string()),
);
event.set_field(
"field.with.dots".to_string(),
Dynamic::from("value8".to_string()),
);
let formatter = LogfmtFormatter::new();
let result = formatter.format(&event);
assert!(result.contains("field_with_spaces=value1"));
assert!(result.contains("field_with_equals=value2"));
assert!(result.contains("field_with_tabs=value3"));
assert!(result.contains("field_with_newlines=value4"));
assert!(result.contains("field_with_carriage=value5"));
assert!(result.contains("normal_field=value6"));
assert!(result.contains("field-with-dashes=value7"));
assert!(result.contains("field.with.dots=value8"));
let parser = crate::parsers::logfmt::LogfmtParser::new();
let parsed = crate::pipeline::EventParser::parse(&parser, &result);
assert!(
parsed.is_ok(),
"Sanitized logfmt output should be parseable: {}",
result
);
let parsed_event = parsed.unwrap();
assert_eq!(
parsed_event
.fields
.get("field_with_spaces")
.unwrap()
.to_string(),
"value1"
);
assert_eq!(
parsed_event
.fields
.get("field_with_equals")
.unwrap()
.to_string(),
"value2"
);
assert_eq!(
parsed_event.fields.get("normal_field").unwrap().to_string(),
"value6"
);
}
#[test]
fn test_sanitize_logfmt_key_function() {
assert_eq!(sanitize_logfmt_key("normal_field"), "normal_field");
assert_eq!(
sanitize_logfmt_key("field with spaces"),
"field_with_spaces"
);
assert_eq!(
sanitize_logfmt_key("field=with=equals"),
"field_with_equals"
);
assert_eq!(sanitize_logfmt_key("field\twith\ttabs"), "field_with_tabs");
assert_eq!(
sanitize_logfmt_key("field\nwith\nnewlines"),
"field_with_newlines"
);
assert_eq!(
sanitize_logfmt_key("field\rwith\rcarriage"),
"field_with_carriage"
);
assert_eq!(
sanitize_logfmt_key("field-with-dashes"),
"field-with-dashes"
);
assert_eq!(sanitize_logfmt_key("field.with.dots"), "field.with.dots");
assert_eq!(
sanitize_logfmt_key("field_with_underscores"),
"field_with_underscores"
);
assert_eq!(sanitize_logfmt_key(""), "");
assert_eq!(sanitize_logfmt_key("==="), "___");
assert_eq!(sanitize_logfmt_key(" "), "___");
assert_eq!(sanitize_logfmt_key(" = \t = \n = \r "), "_____________");
}
#[test]
fn test_hide_formatter() {
let mut event = Event::default();
event.set_field("level".to_string(), Dynamic::from("INFO".to_string()));
event.set_field("msg".to_string(), Dynamic::from("Test message".to_string()));
event.set_field("user".to_string(), Dynamic::from("alice".to_string()));
let formatter = HideFormatter::new();
let result = formatter.format(&event);
assert_eq!(result, "");
}
#[test]
fn test_hide_formatter_empty_event() {
let event = Event::default();
let formatter = HideFormatter::new();
let result = formatter.format(&event);
assert_eq!(result, "");
}
#[test]
fn test_null_formatter_behavior() {
let mut event = Event::default();
event.set_field("level".to_string(), Dynamic::from("ERROR".to_string()));
event.set_field(
"msg".to_string(),
Dynamic::from("Critical error".to_string()),
);
let formatter = HideFormatter::new(); let result = formatter.format(&event);
assert_eq!(result, ""); }
#[test]
fn test_shared_escaping_utilities() {
assert_eq!(escape_logfmt_string("simple"), "simple");
assert_eq!(escape_logfmt_string("with\"quotes"), "with\\\"quotes");
assert_eq!(escape_logfmt_string("with\nnewline"), "with\\nnewline");
assert_eq!(escape_logfmt_string("with\ttab"), "with\\ttab");
assert_eq!(escape_logfmt_string("with\\backslash"), "with\\\\backslash");
assert!(!needs_logfmt_quoting("simple"));
assert!(needs_logfmt_quoting("with spaces"));
assert!(needs_logfmt_quoting(""));
assert!(needs_logfmt_quoting("with=equals"));
assert!(needs_logfmt_quoting("with\"quotes"));
assert!(needs_logfmt_quoting("with\ttab"));
assert_eq!(
format_dynamic_value(&Dynamic::from("test")),
("test".to_string(), true)
);
assert_eq!(
format_dynamic_value(&Dynamic::from(42i64)),
("42".to_string(), false)
);
assert_eq!(
format_dynamic_value(&Dynamic::from(true)),
("true".to_string(), false)
);
}
#[test]
fn test_csv_formatter_basic() {
let keys = vec!["name".to_string(), "age".to_string(), "city".to_string()];
let formatter = CsvFormatter::new(keys);
let mut event = Event::default();
event.set_field("name".to_string(), Dynamic::from("Alice".to_string()));
event.set_field("age".to_string(), Dynamic::from(25i64));
event.set_field("city".to_string(), Dynamic::from("New York".to_string()));
let result = formatter.format(&event);
assert!(result.contains("name,age,city"));
assert!(result.contains("Alice,25,New York"));
}
#[test]
fn test_csv_formatter_with_quoting() {
let keys = vec!["name".to_string(), "msg".to_string()];
let formatter = CsvFormatter::new(keys);
let mut event = Event::default();
event.set_field("name".to_string(), Dynamic::from("Smith, John".to_string()));
event.set_field(
"msg".to_string(),
Dynamic::from("He said \"hello\"".to_string()),
);
let result = formatter.format(&event);
assert!(result.contains("\"Smith, John\""));
assert!(result.contains("\"He said \"\"hello\"\"\""));
}
#[test]
fn test_tsv_formatter_basic() {
let keys = vec!["name".to_string(), "age".to_string()];
let formatter = CsvFormatter::new_tsv(keys);
let mut event = Event::default();
event.set_field("name".to_string(), Dynamic::from("Alice".to_string()));
event.set_field("age".to_string(), Dynamic::from(25i64));
let result = formatter.format(&event);
assert!(result.contains("name\tage"));
assert!(result.contains("Alice\t25"));
}
#[test]
fn test_csv_formatter_no_header() {
let keys = vec!["name".to_string(), "age".to_string()];
let formatter = CsvFormatter::new_csv_no_header(keys);
let mut event = Event::default();
event.set_field("name".to_string(), Dynamic::from("Alice".to_string()));
event.set_field("age".to_string(), Dynamic::from(25i64));
let result = formatter.format(&event);
assert!(!result.contains("name,age"));
assert_eq!(result, "Alice,25");
}
#[test]
fn test_csv_formatter_missing_fields() {
let keys = vec!["name".to_string(), "age".to_string(), "city".to_string()];
let formatter = CsvFormatter::new_csv_no_header(keys);
let mut event = Event::default();
event.set_field("name".to_string(), Dynamic::from("Alice".to_string()));
event.set_field("city".to_string(), Dynamic::from("Boston".to_string()));
let result = formatter.format(&event);
assert_eq!(result, "Alice,,Boston");
}
#[test]
fn test_csv_escaping_utilities() {
assert!(!needs_csv_quoting("simple", ','));
assert!(needs_csv_quoting("with,comma", ','));
assert!(needs_csv_quoting("with\"quote", ','));
assert!(needs_csv_quoting("with\nnewline", ','));
assert!(needs_csv_quoting("", ','));
assert!(needs_csv_quoting(" leading", ','));
assert!(needs_csv_quoting("trailing ", ','));
assert!(!needs_csv_quoting("with,comma", '\t'));
assert!(needs_csv_quoting("with\ttab", '\t'));
assert_eq!(escape_csv_value("simple", ','), "simple");
assert_eq!(escape_csv_value("with,comma", ','), "\"with,comma\"");
assert_eq!(escape_csv_value("with\"quote", ','), "\"with\"\"quote\"");
assert_eq!(escape_csv_value("", ','), "\"\"");
}
#[test]
fn test_default_formatter_wrapping_disabled() {
let mut event = Event::default();
event.set_field("level".to_string(), Dynamic::from("INFO".to_string()));
event.set_field(
"message".to_string(),
Dynamic::from("This is a very long message that would normally wrap".to_string()),
);
event.set_field("user".to_string(), Dynamic::from("alice".to_string()));
let formatter = DefaultFormatter::new_with_wrapping(
false,
false,
crate::config::TimestampFormatConfig::default(),
false, );
let result = formatter.format(&event);
assert!(!result.contains('\n'));
assert!(result.contains("level='INFO'"));
assert!(result.contains("message='This is a very long message that would normally wrap'"));
assert!(result.contains("user='alice'"));
}
#[test]
fn test_default_formatter_wrapping_enabled() {
let mut event = Event::default();
event.set_field("field1".to_string(), Dynamic::from("value1".to_string()));
event.set_field("field2".to_string(), Dynamic::from("value2".to_string()));
event.set_field(
"very_long_field_name".to_string(),
Dynamic::from(
"a very long field value that will definitely cause wrapping".to_string(),
),
);
event.set_field("field4".to_string(), Dynamic::from("value4".to_string()));
let formatter = DefaultFormatter {
colors: crate::colors::ColorScheme::new(false),
level_keys: vec!["level"],
brief: false,
timestamp_formatting: crate::config::TimestampFormatConfig::default(),
enable_wrapping: true,
terminal_width: 50, };
let result = formatter.format(&event);
assert!(result.contains('\n'));
assert!(result.contains(" "));
assert!(result.contains("field1='value1'"));
assert!(result.contains("field2='value2'"));
assert!(result.contains(
"very_long_field_name='a very long field value that will definitely cause wrapping'"
));
assert!(result.contains("field4='value4'"));
}
#[test]
fn test_default_formatter_wrapping_brief_mode() {
let mut event = Event::default();
event.set_field("field1".to_string(), Dynamic::from("short".to_string()));
event.set_field(
"field2".to_string(),
Dynamic::from("this is a much longer value that should cause wrapping".to_string()),
);
event.set_field("field3".to_string(), Dynamic::from("end".to_string()));
let formatter = DefaultFormatter {
colors: crate::colors::ColorScheme::new(false),
level_keys: vec![],
brief: true,
timestamp_formatting: crate::config::TimestampFormatConfig::default(),
enable_wrapping: true,
terminal_width: 30, };
let result = formatter.format(&event);
assert!(result.contains('\n'));
assert!(result.contains(" "));
assert!(result.contains("short"));
assert!(result.contains("this is a much longer value that should cause wrapping"));
assert!(result.contains("end"));
assert!(!result.contains("field1="));
assert!(!result.contains("field2="));
assert!(!result.contains("field3="));
}
#[test]
fn test_display_length_ignores_ansi_codes() {
let formatter = DefaultFormatter::new_with_wrapping(
false,
false,
crate::config::TimestampFormatConfig::default(),
true,
);
let colored_text = "\x1b[31mred text\x1b[0m";
assert_eq!(formatter.display_length(colored_text), 8);
let plain_text = "red text";
assert_eq!(formatter.display_length(plain_text), 8);
assert_eq!(formatter.display_length(""), 0);
assert_eq!(formatter.display_length("\x1b[31m\x1b[0m"), 0);
}
#[test]
fn test_wrapping_preserves_field_boundaries() {
let mut event = Event::default();
event.set_field("a".to_string(), Dynamic::from("value".to_string()));
event.set_field("b".to_string(), Dynamic::from("value".to_string()));
event.set_field("c".to_string(), Dynamic::from("value".to_string()));
let formatter = DefaultFormatter {
colors: crate::colors::ColorScheme::new(false),
level_keys: vec![],
brief: false,
timestamp_formatting: crate::config::TimestampFormatConfig::default(),
enable_wrapping: true,
terminal_width: 20, };
let result = formatter.format(&event);
assert!(!result.contains("a='val\n ue'")); assert!(result.contains("a='value'"));
let lines: Vec<&str> = result.split('\n').collect();
assert!(lines.len() > 1);
for (i, line) in lines.iter().enumerate() {
if i > 0 && !line.is_empty() {
assert!(
line.starts_with(" "),
"Line {} should be indented: '{}'",
i,
line
);
}
}
}
#[test]
fn test_default_formatter_new_constructor_enables_wrapping_by_default() {
let mut event = Event::default();
event.set_field("field1".to_string(), Dynamic::from("value1".to_string()));
event.set_field(
"very_long_field_name_that_exceeds_width".to_string(),
Dynamic::from(
"a very long field value that should definitely cause wrapping in most terminals"
.to_string(),
),
);
event.set_field("field3".to_string(), Dynamic::from("value3".to_string()));
let formatter = DefaultFormatter::new(
false,
false,
crate::config::TimestampFormatConfig::default(),
);
let result = formatter.format(&event);
assert!(
result.contains('\n'),
"Default constructor should enable wrapping"
);
assert!(
result.contains(" "),
"Should have indentation when wrapping"
);
assert!(result.contains("field1='value1'"));
assert!(result.contains("very_long_field_name_that_exceeds_width="));
assert!(result.contains("field3='value3'"));
}
}