use crate::channels::errors::{ErrorEvent, WeaveError};
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 STDERR_IS_TERMINAL: OnceLock<bool> = OnceLock::new();
fn stderr_is_terminal() -> bool {
*STDERR_IS_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 stderr_is_terminal() {
Self::Colored
} else {
Self::Plain
}
}
pub fn is_colored(&self) -> bool {
match self {
Self::Auto => stderr_is_terminal(),
Self::Colored => true,
Self::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 paint<'a>(&self, code: &'a str) -> &'a str {
if self.mode.is_colored() { code } else { "" }
}
fn reset(&self) -> &str {
self.paint(RESET_COLOR)
}
}
impl Default for PlainFormatter {
fn default() -> Self {
Self::new()
}
}
fn cause_chain(error: &WeaveError, depth: usize, fmt: &PlainFormatter) -> Vec<String> {
let Some(cause) = &error.cause else {
return Vec::new();
};
let indent = " ".repeat(depth);
let mut lines = vec![format!(
"{}{}cause: {}{}\n",
fmt.paint(LINE_COLOR),
indent,
cause.message,
fmt.reset()
)];
lines.extend(cause_chain(cause, depth + 1, fmt));
lines
}
impl TelemetryFormatter for PlainFormatter {
fn render_event(&self, event: &Event) -> EventRender {
let line = format!("{}{}{}\n", self.paint(LINE_COLOR), event, self.reset());
EventRender {
context: event.scope_label().map(str::to_owned),
lines: vec![line],
}
}
fn render_errors(&self, errors: &[ErrorEvent]) -> Vec<EventRender> {
errors
.iter()
.enumerate()
.map(|(i, e)| {
let scope = format!("{}{:?}{}", self.paint(CONTEXT_COLOR), e.scope, self.reset());
let mut lines = vec![format!("[{}] {} | {}\n", i, e.when, scope)];
lines.push(format!(
"{} error: {}{}\n",
self.paint(LINE_COLOR),
e.error.message,
self.reset()
));
lines.extend(cause_chain(&e.error, 1, self));
if !e.tags.is_empty() {
lines.push(format!(
"{} tags: {:?}{}\n",
self.paint(LINE_COLOR),
e.tags,
self.reset()
));
}
if !e.context.is_null() {
lines.push(format!(
"{} context: {}{}\n",
self.paint(LINE_COLOR),
e.context,
self.reset()
));
}
EventRender {
context: Some(format!("{:?}", e.scope)),
lines,
}
})
.collect()
}
}