use std::borrow::Cow;
use std::io::{self, Write};
use freeswitch_log_parser::{truncate_at_char_boundary, Block, MessageKind};
use crate::dialstring::{dial_string_of, print_dial_string};
use super::color::*;
use super::ColorMode;
pub struct EntryPrinter {
pub color: ColorMode,
pub show_blocks: bool,
pub show_session: bool,
pub show_filename: bool,
pub show_line_numbers: bool,
}
impl EntryPrinter {
pub fn print_entry(
&self,
w: &mut dyn Write,
entry: &freeswitch_log_parser::LogEntry,
session: Option<&freeswitch_log_parser::SessionSnapshot>,
filename: Option<&str>,
) -> io::Result<()> {
let level = entry
.level
.map(|l| l.to_string())
.unwrap_or_else(|| "-".to_string());
let time = if entry.timestamp.len() >= 11 {
&entry.timestamp[11..]
} else {
&entry.timestamp
};
let use_color = self.color == ColorMode::Always;
let lc = if use_color {
level_color(entry.level)
} else {
""
};
let reset = if use_color { RESET } else { "" };
let dim = if use_color { DIM } else { "" };
if matches!(
entry.message_kind,
MessageKind::FileChange | MessageKind::DateChange
) {
return writeln!(w, "{dim}── {}{reset}", entry.message);
}
let uuid = match &entry.uuid {
None => format!("{dim}-{reset}"),
Some(u) if use_color => {
let mut s = String::new();
write_uuid(&mut s, u);
s
}
Some(u) => u.clone(),
};
let inline = !entry.attached.is_empty()
&& ((self.show_blocks && entry.block.is_none()) || entry.attached.len() == 1);
let (head, head_data) = match inline
.then(|| split_dialplan_line(&entry.message))
.flatten()
{
Some((channel, data)) => (channel, Some(data)),
None => (entry.message.as_str(), None),
};
let msg = if use_color {
colorize_uuids(head, lc)
} else {
Cow::Borrowed(head)
};
if let Some(fname) = filename.filter(|_| self.show_filename) {
write!(w, "{dim}{fname}{reset} ")?;
}
if self.show_line_numbers {
write!(w, "{lc}L{line:>6} ", line = entry.line_number)?;
}
writeln!(
w,
"{lc}{time:>15} {level:>7}{reset} {uuid} {lc}[{mkind}]{reset} {lc}{msg}{reset}",
mkind = entry.message_kind,
)?;
if self.show_blocks {
if let Some(block) = &entry.block {
self.print_block(w, block, use_color)?;
}
if let Some(args) = dial_string_of(&entry.message_kind) {
let (lbl, val) = if use_color { (CYAN, DIM) } else { ("", "") };
print_dial_string(w, args, lbl, val, reset)?;
}
}
if self.show_session {
if let Some(session) = session {
self.print_session(w, session, use_color)?;
}
}
for warning in &entry.warnings {
let wc = if use_color { MAGENTA } else { "" };
writeln!(w, "{wc} WARN {warning}{reset}")?;
}
if !entry.attached.is_empty() {
let dim_s = if use_color { DIM } else { "" };
if inline {
for line in head_data.into_iter().chain(&entry.attached) {
let line = strip_repeated_prefix(line, entry.uuid.as_deref().unwrap_or(""));
let rendered = if use_color {
match colorize_uuids(line, dim_s) {
Cow::Borrowed(s) => colorize_pass_fail(s, dim_s),
Cow::Owned(s) => match colorize_pass_fail(&s, dim_s) {
Cow::Borrowed(_) => Cow::Owned(s),
Cow::Owned(both) => Cow::Owned(both),
},
}
} else {
Cow::Borrowed(line)
};
writeln!(w, "{dim_s} {rendered}{reset}")?;
}
} else if !(self.show_blocks && entry.block.is_some()) {
writeln!(
w,
"{dim_s} ({} attached lines){reset}",
entry.attached.len()
)?;
}
}
Ok(())
}
#[cfg(feature = "sdp")]
fn sdp_summary_lines(block: &Block) -> Vec<String> {
use freeswitch_types::sdp::SdpCodecEntry;
let Some(Ok(codecs)) = block.sdp_codecs() else {
return Vec::new();
};
let mut parts: Vec<String> = codecs
.entries()
.map(|e| match e {
SdpCodecEntry::Rtp(c) => {
let mut s = format!("{}/{}", c.name(), c.clock_rate());
if let Some(ch) = c.channels() {
if ch > 1 {
s.push_str(&format!("/{ch}"));
}
}
s
}
_ => "T.38".to_string(),
})
.collect();
for payload in codecs.non_codec_payloads() {
parts.push(payload.to_string());
}
for u in codecs.unmapped() {
parts.push(format!("pt{}?", u.payload_type));
}
let quiet: Vec<String> = codecs
.sections()
.iter()
.filter_map(|s| {
if s.port() == 0 {
Some(format!("held m={} port 0", s.media_type()))
} else if s.entries().is_empty() && s.unmapped().is_empty() {
Some(format!("skipped m={}/{}", s.media_type(), s.proto()))
} else {
None
}
})
.collect();
let mut lines = Vec::new();
if !parts.is_empty() {
lines.push(parts.join(", "));
}
if !quiet.is_empty() {
lines.push(quiet.join(", "));
}
lines
}
fn print_block(&self, w: &mut dyn Write, block: &Block, use_color: bool) -> io::Result<()> {
let bc = if use_color { DIM_GREEN } else { "" };
let sc = if use_color { BRIGHT_GREEN } else { "" };
let reset = if use_color { RESET } else { "" };
match block {
Block::ChannelData { fields, variables } => {
for (name, value) in fields {
writeln!(w, "{bc} field {name}: {value}{reset}")?;
}
for (name, value) in variables {
writeln!(w, "{bc} var {name}: {value}{reset}")?;
}
}
Block::Sdp { direction, body } => {
writeln!(
w,
"{sc} sdp {direction} ({} lines){reset}",
body.len()
)?;
#[cfg(feature = "sdp")]
for summary in Self::sdp_summary_lines(block) {
writeln!(w, "{sc} sdp {summary}{reset}")?;
}
for line in body {
writeln!(w, "{sc} sdp {line}{reset}")?;
}
}
Block::CodecNegotiation {
media,
comparisons,
matched,
near_matched,
} => {
let cc = if use_color { DIM_YELLOW } else { "" };
writeln!(
w,
"{cc} codec {media}: {} comparisons, {} matched, {} near{reset}",
comparisons.len(),
matched.len(),
near_matched.len(),
)?;
for (offered, local) in comparisons {
writeln!(w, "{cc} codec {offered} vs {local}{reset}")?;
}
for c in matched {
writeln!(w, "{cc} codec MATCH {c}{reset}")?;
}
for c in near_matched {
writeln!(w, "{cc} codec NEAR {c}{reset}")?;
}
}
_ => {
writeln!(w, "{bc} block {block:?}{reset}")?;
}
}
Ok(())
}
fn print_session(
&self,
w: &mut dyn Write,
session: &freeswitch_log_parser::SessionSnapshot,
use_color: bool,
) -> io::Result<()> {
let dim = if use_color { DIM } else { "" };
let reset = if use_color { RESET } else { "" };
let mut parts = Vec::new();
if let Some(ctx) = &session.dialplan_context {
parts.push(format!("ctx={ctx}"));
}
if let Some(d) = &session.initial_destination {
parts.push(format!("dest={d}"));
}
if let Some(state) = &session.channel_state {
parts.push(format!("state={state}"));
}
if let Some(name) = &session.channel_name {
parts.push(format!("ch={name}"));
}
if let Some(conf) = &session.conference {
parts.push(format!("conf={}", conf.name));
if let Some(id) = conf.member_id {
parts.push(format!("member={id}"));
}
}
if !parts.is_empty() {
writeln!(w, "{dim} session {}{reset}", parts.join(" "))?;
}
Ok(())
}
pub fn print_stats(
&self,
w: &mut dyn Write,
stats: &freeswitch_log_parser::ParseStats,
entry_count: u64,
session_count: usize,
) -> io::Result<()> {
writeln!(
w,
"{entry_count} entries, {} lines, {} unclassified, {session_count} sessions",
stats.lines_processed, stats.lines_unclassified,
)
}
pub fn print_field_stats(
&self,
w: &mut dyn Write,
fields: &crate::context::FieldCounts,
matched: u64,
) -> io::Result<()> {
let total = fields.total();
if total == 0 {
return Ok(());
}
writeln!(w)?;
writeln!(
w,
"{total} field spans in {}/{matched} entries (message {}, attached {})",
fields.entries_with_fields, fields.in_message, fields.in_attached,
)?;
for (kind, count) in fields.by_kind() {
writeln!(w, " {kind:<20} {count}")?;
}
Ok(())
}
pub fn print_unclassified(
&self,
w: &mut dyn Write,
stats: &freeswitch_log_parser::ParseStats,
) -> io::Result<()> {
if stats.unclassified_lines.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, "unclassified lines:")?;
for u in &stats.unclassified_lines {
writeln!(
w,
" L{}: {:?}{}",
u.line_number,
u.reason,
u.data
.as_ref()
.map(|d| format!(" | {}", truncate_at_char_boundary(d, 100)))
.unwrap_or_default(),
)?;
}
Ok(())
}
}