use std::borrow::Cow;
use std::io::{self, Write};
use freeswitch_log_parser::{truncate_at_char_boundary, Block, LogEntry, 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_line_numbers: bool,
}
pub fn stamp_time(ts: &str) -> &str {
ts.get(11..).unwrap_or(ts)
}
pub fn stamp_date(ts: &str) -> Option<&str> {
ts.get(..10)
}
enum AttachedView<'a> {
Inline(Option<&'a str>),
Counted,
Silent,
}
impl EntryPrinter {
fn layout<'a>(&self, entry: &'a LogEntry) -> (&'a str, AttachedView<'a>) {
let msg = entry.message.as_str();
if entry.attached.is_empty() {
return (msg, AttachedView::Silent);
}
let inline = (self.show_blocks && entry.block.is_none()) || entry.attached.len() == 1;
if !inline {
return match self.show_blocks && entry.block.is_some() {
true => (msg, AttachedView::Silent),
false => (msg, AttachedView::Counted),
};
}
match split_dialplan_line(msg) {
Some((channel, data)) => (channel, AttachedView::Inline(Some(data))),
None => (msg, AttachedView::Inline(None)),
}
}
fn write_header(
&self,
w: &mut dyn Write,
entry: &LogEntry,
p: &Palette,
head: &str,
) -> io::Result<()> {
let level = entry
.level
.map(|l| l.to_string())
.unwrap_or_else(|| "-".to_string());
let time = stamp_time(&entry.timestamp);
let lc = p.level(entry.level);
let reset = p.reset;
let uuid = match &entry.uuid {
None => format!("{}-{reset}", p.dim),
Some(u) => {
let mut s = String::new();
p.write_uuid(&mut s, u);
s
}
};
let msg = p.colorize_uuids(head, lc);
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,
)
}
fn write_attached(
&self,
w: &mut dyn Write,
entry: &LogEntry,
p: &Palette,
view: AttachedView,
) -> io::Result<()> {
let (dim, reset) = (p.dim, p.reset);
match view {
AttachedView::Silent => Ok(()),
AttachedView::Counted => writeln!(
w,
"{dim} ({} attached lines){reset}",
entry.attached.len()
),
AttachedView::Inline(head_data) => {
for line in head_data.into_iter().chain(&entry.attached) {
let line = strip_repeated_prefix(line, entry.uuid.as_deref().unwrap_or(""));
let rendered = match p.colorize_uuids(line, dim) {
Cow::Borrowed(s) => p.colorize_pass_fail(s, dim),
Cow::Owned(s) => match p.colorize_pass_fail(&s, dim) {
Cow::Borrowed(_) => Cow::Owned(s),
Cow::Owned(both) => Cow::Owned(both),
},
};
writeln!(w, "{dim} {rendered}{reset}")?;
}
Ok(())
}
}
}
pub fn print_entry(
&self,
w: &mut dyn Write,
entry: &LogEntry,
session: Option<&freeswitch_log_parser::SessionSnapshot>,
) -> io::Result<()> {
let p = Palette::new(self.color);
if matches!(
entry.message_kind,
MessageKind::FileChange | MessageKind::DateChange
) {
return writeln!(w, "{}── {}{}", p.dim, entry.message, p.reset);
}
let (head, attached) = self.layout(entry);
self.write_header(w, entry, &p, head)?;
if self.show_blocks {
if let Some(block) = &entry.block {
self.print_block(w, block, &p)?;
}
if let Some(args) = dial_string_of(&entry.message_kind) {
print_dial_string(w, args, &p)?;
}
}
if self.show_session {
if let Some(session) = session {
self.print_session(w, session, &p)?;
}
}
for warning in &entry.warnings {
writeln!(w, "{} WARN {warning}{}", p.warning, p.reset)?;
}
self.write_attached(w, entry, &p, attached)
}
#[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, p: &Palette) -> io::Result<()> {
let bc = p.field;
let sc = p.sdp;
let reset = p.reset;
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 = p.codec;
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,
p: &Palette,
) -> io::Result<()> {
let dim = p.dim;
let reset = p.reset;
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(())
}
}