use crate::channels::errors::ErrorEvent;
use crate::event_bus::Event;
use std::io::IsTerminal;
use std::sync::OnceLock;
pub const CONTEXT_COLOR: &str = "\x1b[32m"; pub const LINE_COLOR: &str = "\x1b[35m"; pub const RESET_COLOR: &str = "\x1b[0m";
static IS_STDERR_TERMINAL: OnceLock<bool> = OnceLock::new();
fn get_is_stderr_terminal() -> bool {
*IS_STDERR_TERMINAL.get_or_init(|| std::io::stderr().is_terminal())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FormatterMode {
#[default]
Auto,
Colored,
Plain,
}
impl FormatterMode {
pub fn auto_detect() -> Self {
if get_is_stderr_terminal() {
FormatterMode::Colored
} else {
FormatterMode::Plain
}
}
pub fn is_colored(&self) -> bool {
match self {
FormatterMode::Auto => get_is_stderr_terminal(),
FormatterMode::Colored => true,
FormatterMode::Plain => false,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct EventRender {
pub context: Option<String>,
pub lines: Vec<String>,
}
impl EventRender {
pub fn join_lines(&self) -> String {
self.lines.join("")
}
}
pub trait TelemetryFormatter: Send + Sync {
fn render_event(&self, event: &Event) -> EventRender;
fn render_errors(&self, errors: &[ErrorEvent]) -> Vec<EventRender>;
}
pub struct PlainFormatter {
mode: FormatterMode,
}
impl PlainFormatter {
pub fn new() -> Self {
Self {
mode: FormatterMode::Auto,
}
}
pub fn with_mode(mode: FormatterMode) -> Self {
Self { mode }
}
fn color<'a>(&self, ansi_code: &'a str) -> &'a str {
if self.mode.is_colored() {
ansi_code
} else {
""
}
}
fn reset(&self) -> &str {
if self.mode.is_colored() {
RESET_COLOR
} else {
""
}
}
}
impl Default for PlainFormatter {
fn default() -> Self {
Self::new()
}
}
fn format_error_chain(
error: &crate::channels::errors::WeaveError,
indent: usize,
use_color: bool,
) -> Vec<String> {
let mut lines = Vec::new();
if let Some(cause) = &error.cause {
let indent_str = " ".repeat(indent);
if use_color {
lines.push(format!(
"{LINE_COLOR}{}cause: {}{RESET_COLOR}\n",
indent_str, cause.message
));
} else {
lines.push(format!("{}cause: {}\n", indent_str, cause.message));
}
lines.extend(format_error_chain(cause, indent + 1, use_color));
}
lines
}
impl TelemetryFormatter for PlainFormatter {
fn render_event(&self, event: &Event) -> EventRender {
let line = if self.mode.is_colored() {
format!("{LINE_COLOR}{}{RESET_COLOR}\n", event)
} else {
format!("{}\n", event)
};
EventRender {
context: event.scope_label().map(|s| s.to_string()),
lines: vec![line],
}
}
fn render_errors(&self, errors: &[ErrorEvent]) -> Vec<EventRender> {
let use_color = self.mode.is_colored();
errors
.iter()
.enumerate()
.map(|(i, e)| {
let mut lines = Vec::new();
let scope_str = if use_color {
format!("{}{:?}{}", self.color(CONTEXT_COLOR), e.scope, self.reset())
} else {
format!("{:?}", e.scope)
};
lines.push(format!("[{}] {} | {}\n", i, e.when, scope_str));
if use_color {
lines.push(format!(
"{} error: {}{}\n",
self.color(LINE_COLOR),
e.error.message,
self.reset()
));
} else {
lines.push(format!(" error: {}\n", e.error.message));
}
lines.extend(format_error_chain(&e.error, 1, use_color));
if !e.tags.is_empty() {
if use_color {
lines.push(format!(
"{} tags: {:?}{}\n",
self.color(LINE_COLOR),
e.tags,
self.reset()
));
} else {
lines.push(format!(" tags: {:?}\n", e.tags));
}
}
if !e.context.is_null() {
if use_color {
lines.push(format!(
"{} context: {}{}\n",
self.color(LINE_COLOR),
e.context,
self.reset()
));
} else {
lines.push(format!(" context: {}\n", e.context));
}
}
EventRender {
context: Some(format!("{:?}", e.scope)),
lines,
}
})
.collect()
}
}