use crate::colors::ColorScheme;
use crate::event::Event;
use crate::pipeline;
use chrono::{DateTime, FixedOffset, SecondsFormat, Utc};
use rhai::Dynamic;
use std::sync::Mutex;
struct CompactMapState {
current_timestamp: Option<String>,
buffer: String,
visible_len: usize,
}
impl CompactMapState {
fn new(initial_capacity: usize) -> Self {
let base_capacity = initial_capacity.max(1) * 4;
Self {
current_timestamp: None,
buffer: String::with_capacity(base_capacity),
visible_len: 0,
}
}
fn reset(&mut self) {
self.current_timestamp = None;
self.buffer.clear();
self.visible_len = 0;
}
fn push_rendered(&mut self, rendered: &str) {
self.buffer.push_str(rendered);
self.visible_len += 1;
}
}
pub(super) mod compact_map_utils {
use super::*;
pub(super) fn dynamic_to_trimmed_string(value: &Dynamic) -> Option<String> {
if let Ok(s) = value.clone().into_string() {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
} else {
let fallback = value.to_string();
let trimmed = fallback.trim();
if trimmed.is_empty() || trimmed == "()" {
None
} else {
Some(trimmed.to_string())
}
}
}
pub(crate) fn format_line(timestamp: Option<&String>, buffer: &str) -> String {
match timestamp {
Some(ts) if !ts.is_empty() => format!("{} {}", ts, buffer),
_ => buffer.to_string(),
}
}
pub(crate) fn extract_timestamp(event: &Event) -> String {
if let Some(ts) = event.parsed_ts {
return format_timestamp(ts);
}
for key in crate::event::TIMESTAMP_FIELD_NAMES {
if let Some(value) = event.fields.get(*key) {
if let Some(ts) = value.clone().try_cast::<DateTime<Utc>>() {
return format_timestamp(ts);
}
if let Some(ts) = value.clone().try_cast::<DateTime<FixedOffset>>() {
return format_timestamp(ts.with_timezone(&Utc));
}
if let Ok(string_value) = value.clone().into_string() {
let trimmed = string_value.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
} else {
let fallback = value.to_string();
let trimmed = fallback.trim();
if !trimmed.is_empty() && trimmed != "()" {
return trimmed.to_string();
}
}
}
}
if let Some(line_num) = event.line_num {
format!("line {}", line_num)
} else {
"unknown".to_string()
}
}
pub(super) fn format_timestamp(ts: DateTime<Utc>) -> String {
ts.to_rfc3339_opts(SecondsFormat::Millis, true)
}
}
pub struct LevelmapFormatter {
state: Mutex<CompactMapState>,
terminal_width: usize,
buffer_width_override: Option<usize>,
colors: ColorScheme,
}
impl LevelmapFormatter {
const FALLBACK_TERMINAL_WIDTH: usize = 80;
pub fn new(use_colors: bool) -> Self {
let detected_width = crate::tty::get_terminal_width();
let terminal_width = if detected_width == 0 {
Self::FALLBACK_TERMINAL_WIDTH
} else {
detected_width
};
Self {
state: Mutex::new(CompactMapState::new(terminal_width)),
terminal_width,
buffer_width_override: None,
colors: ColorScheme::new(use_colors),
}
}
#[cfg(test)]
pub fn with_width(width: usize) -> Self {
let effective_width = width.max(1);
Self {
state: Mutex::new(CompactMapState::new(effective_width)),
terminal_width: effective_width,
buffer_width_override: Some(effective_width),
colors: ColorScheme::new(false),
}
}
fn available_width(&self, timestamp: Option<&String>) -> usize {
if let Some(override_width) = self.buffer_width_override {
return override_width.max(1);
}
let terminal_width = self.terminal_width.max(1);
let reserved = timestamp
.filter(|ts| !ts.is_empty())
.map(|ts| ts.len().saturating_add(1))
.unwrap_or(0);
terminal_width.saturating_sub(reserved).max(1)
}
fn extract_level_string(event: &Event) -> Option<String> {
for key in crate::event::LEVEL_FIELD_NAMES {
if let Some(value) = event.fields.get(*key) {
if let Some(level) = compact_map_utils::dynamic_to_trimmed_string(value) {
return Some(level);
}
}
}
None
}
fn render_level_char(&self, level: Option<&str>, ch: char) -> String {
if let Some(level_str) = level {
let color = self.level_color(level_str);
if !color.is_empty() {
let mut rendered = String::with_capacity(color.len() + self.colors.reset.len() + 1);
rendered.push_str(color);
rendered.push(ch);
rendered.push_str(self.colors.reset);
return rendered;
}
}
ch.to_string()
}
fn level_color<'a>(&'a self, level: &str) -> &'a 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,
_ => "",
}
}
}
impl pipeline::Formatter for LevelmapFormatter {
fn format(&self, event: &Event) -> String {
let mut state = self
.state
.lock()
.expect("levelmap formatter mutex poisoned");
if state.current_timestamp.is_none() {
state.current_timestamp = Some(compact_map_utils::extract_timestamp(event));
}
let available_width = self.available_width(state.current_timestamp.as_ref());
let level_string = Self::extract_level_string(event);
let display_char = level_string
.as_deref()
.and_then(|s| s.chars().next())
.unwrap_or('?');
let rendered = self.render_level_char(level_string.as_deref(), display_char);
state.push_rendered(&rendered);
if state.visible_len >= available_width {
let line =
compact_map_utils::format_line(state.current_timestamp.as_ref(), &state.buffer);
state.reset();
line
} else {
String::new()
}
}
fn finish(&self) -> Option<String> {
let mut state = self
.state
.lock()
.expect("levelmap formatter mutex poisoned");
if state.visible_len == 0 {
return None;
}
let line = compact_map_utils::format_line(state.current_timestamp.as_ref(), &state.buffer);
state.reset();
if line.is_empty() {
None
} else {
Some(line)
}
}
}
pub struct KeymapFormatter {
state: Mutex<CompactMapState>,
terminal_width: usize,
buffer_width_override: Option<usize>,
field_name: String,
}
impl KeymapFormatter {
const FALLBACK_TERMINAL_WIDTH: usize = 80;
pub fn new(field_name: Option<String>) -> Self {
let detected_width = crate::tty::get_terminal_width();
let terminal_width = if detected_width == 0 {
Self::FALLBACK_TERMINAL_WIDTH
} else {
detected_width
};
Self {
state: Mutex::new(CompactMapState::new(terminal_width)),
terminal_width,
buffer_width_override: None,
field_name: field_name.unwrap_or_else(|| "level".to_string()),
}
}
#[cfg(test)]
pub fn with_width(width: usize, field_name: Option<String>) -> Self {
let effective_width = width.max(1);
Self {
state: Mutex::new(CompactMapState::new(effective_width)),
terminal_width: effective_width,
buffer_width_override: Some(effective_width),
field_name: field_name.unwrap_or_else(|| "level".to_string()),
}
}
fn available_width(&self, timestamp: Option<&String>) -> usize {
if let Some(override_width) = self.buffer_width_override {
return override_width.max(1);
}
let terminal_width = self.terminal_width.max(1);
let reserved = timestamp
.filter(|ts| !ts.is_empty())
.map(|ts| ts.len().saturating_add(1))
.unwrap_or(0);
terminal_width.saturating_sub(reserved).max(1)
}
fn extract_field_string(&self, event: &Event) -> Option<String> {
if let Some(value) = event.fields.get(&self.field_name) {
compact_map_utils::dynamic_to_trimmed_string(value)
} else {
None
}
}
}
impl pipeline::Formatter for KeymapFormatter {
fn format(&self, event: &Event) -> String {
let mut state = self.state.lock().expect("keymap formatter mutex poisoned");
if state.current_timestamp.is_none() {
state.current_timestamp = Some(compact_map_utils::extract_timestamp(event));
}
let available_width = self.available_width(state.current_timestamp.as_ref());
let field_string = self.extract_field_string(event);
let display_char = field_string
.as_deref()
.and_then(|s| s.chars().next())
.unwrap_or('.');
state.push_rendered(&display_char.to_string());
if state.visible_len >= available_width {
let line =
compact_map_utils::format_line(state.current_timestamp.as_ref(), &state.buffer);
state.reset();
line
} else {
String::new()
}
}
fn finish(&self) -> Option<String> {
let mut state = self.state.lock().expect("keymap formatter mutex poisoned");
if state.visible_len == 0 {
return None;
}
let line = compact_map_utils::format_line(state.current_timestamp.as_ref(), &state.buffer);
state.reset();
if line.is_empty() {
None
} else {
Some(line)
}
}
}
pub(super) use compact_map_utils as utils;