freeswitch_log_parser/stream/entry.rs
1//! Types produced per log entry: the reassembled [`Block`] variants, the
2//! [`LogEntry`] record itself, and parsing anomalies ([`ParseWarning`]).
3
4use std::fmt;
5
6use crate::attached::AttachedLines;
7use crate::codec::{CodecMedia, CodecOffer, CodecParseError};
8use crate::decode::truncate_at_char_boundary;
9use crate::level::LogLevel;
10use crate::line::LineKind;
11use crate::message::{MessageKind, SdpDirection};
12
13use super::collision::MOD_LOGFILE_BUF_SIZE;
14
15/// Structured data extracted from a multi-line dump that follows a primary log entry.
16///
17/// Each variant corresponds to a block type that the stream state machine
18/// recognizes and reassembles from continuation lines.
19#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Block {
22 /// Channel variable dump — `Channel-*` fields and `variable_*` key-value pairs.
23 /// Multi-line variable values (e.g. embedded SDP) are reassembled with `\n` separators.
24 ChannelData {
25 fields: Vec<(String, String)>,
26 variables: Vec<(String, String)>,
27 },
28 /// SDP session description body, collected line by line.
29 Sdp {
30 direction: SdpDirection,
31 body: Vec<String>,
32 },
33 /// Codec negotiation sequence for one media type. A run only ever covers a
34 /// single [`CodecMedia`]; audio and video traces never share a block.
35 CodecNegotiation {
36 media: CodecMedia,
37 /// `(remote offer, local implementation)` for each pair compared.
38 comparisons: Vec<(CodecOffer, CodecOffer)>,
39 matched: Vec<CodecOffer>,
40 /// Codecs kept as fallbacks because only their ptime differed.
41 near_matched: Vec<CodecOffer>,
42 },
43}
44
45#[cfg(feature = "sdp")]
46impl Block {
47 /// Codecs described by an SDP body.
48 ///
49 /// `None` when there is no body to read — another block type, or an SDP
50 /// marker that carried none. Sofia logs several (`Duplicate SDP`,
51 /// `Processing updated SDP`) purely as announcements, and an empty body is
52 /// absence rather than a malformed session.
53 ///
54 /// Parsed on each call rather than stored — see `docs/design-rationale.md`.
55 /// Only a session-level failure is an `Err`: a malformed `a=rtpmap` or a
56 /// broken media section degrades into
57 /// [`SdpCodecs::warnings`](freeswitch_types::sdp::SdpCodecs::warnings).
58 pub fn sdp_codecs(
59 &self,
60 ) -> Option<Result<freeswitch_types::sdp::SdpCodecs, freeswitch_types::sdp::SdpCodecError>>
61 {
62 let Block::Sdp { body, .. } = self else {
63 return None;
64 };
65 let text = body.join("\n");
66 if text.trim().is_empty() {
67 return None;
68 }
69 Some(freeswitch_types::sdp::SdpCodecs::parse(&text))
70 }
71}
72
73/// Longest line excerpt a warning carries. An offending line can be tens of
74/// kilobytes; the excerpt is for a human reading the warning, not for matching on.
75pub(super) const WARNING_EXCERPT_LEN: usize = 80;
76
77/// A parsing anomaly, attached to the entry whose lines produced it.
78///
79/// The set is closed and every kind is named — see `docs/design-rationale.md`.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum ParseWarning {
83 /// A CHANNEL_DATA variable opened its `[` and the block ended before the `]`.
84 /// The value collected so far is still recorded.
85 UnclosedVariable { name: String },
86 /// A line inside a CHANNEL_DATA block matched neither the field nor the
87 /// variable shape, so it contributed nothing to the block.
88 UnparseableChannelData { line: String },
89 /// A codec negotiation line matched no known trace shape, or its bracketed
90 /// token would not parse. That codec is missing from the block.
91 UnrecognizedCodecLine {
92 line: String,
93 source: Option<CodecParseError>,
94 },
95 /// A continuation line arrived while a codec negotiation block was open.
96 /// The trace has no continuations, so the line belongs to nothing.
97 UnexpectedCodecContinuation { line: String },
98 /// The formatted line exceeded `mod_logfile`'s write buffer, so the record
99 /// was cut short and lost its trailing newline. `bytes` is the formatted
100 /// length, prefix included.
101 OversizeLine { bytes: usize },
102}
103
104impl ParseWarning {
105 /// Trim a line down to what a warning is willing to carry.
106 pub(crate) fn excerpt(msg: &str) -> String {
107 truncate_at_char_boundary(msg, WARNING_EXCERPT_LEN).to_string()
108 }
109}
110
111impl fmt::Display for ParseWarning {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 ParseWarning::UnclosedVariable { name } => {
115 write!(f, "unclosed multi-line variable: {name}")
116 }
117 ParseWarning::UnparseableChannelData { line } => {
118 write!(f, "unparseable CHANNEL_DATA line: {line}")
119 }
120 ParseWarning::UnrecognizedCodecLine {
121 line,
122 source: Some(e),
123 } => write!(f, "unrecognized codec negotiation line ({e}): {line}"),
124 ParseWarning::UnrecognizedCodecLine { line, source: None } => {
125 write!(f, "unrecognized codec negotiation line: {line}")
126 }
127 ParseWarning::UnexpectedCodecContinuation { line } => {
128 write!(f, "unexpected codec negotiation continuation: {line}")
129 }
130 ParseWarning::OversizeLine { bytes } => write!(
131 f,
132 "line exceeds mod_logfile {MOD_LOGFILE_BUF_SIZE}-byte buffer \
133 ({bytes} bytes), data may be truncated"
134 ),
135 }
136 }
137}
138
139/// A complete parsed log entry with all context resolved.
140///
141/// Produced by [`LogStream`](super::LogStream). Continuation lines have been
142/// grouped, UUID/timestamp inherited from context where needed, and
143/// multi-line blocks reassembled.
144#[derive(Debug)]
145pub struct LogEntry {
146 /// Session UUID, or empty string for system lines.
147 pub uuid: String,
148 /// Timestamp with microsecond precision; inherited from the previous entry for continuations.
149 pub timestamp: String,
150 /// `None` for continuation and truncated lines.
151 pub level: Option<LogLevel>,
152 /// Core scheduler idle percentage; `None` for continuations.
153 pub idle_pct: Option<String>,
154 /// Source file:line; `None` for continuations.
155 pub source: Option<String>,
156 /// The primary message text.
157 pub message: String,
158 /// Which line format originated this entry.
159 pub kind: LineKind,
160 /// Semantic classification of the message content.
161 pub message_kind: MessageKind,
162 /// Typed, parsed multi-line block; `None` for entries without a trailing block.
163 pub block: Option<Block>,
164 /// Raw continuation lines that followed the primary line.
165 pub attached: AttachedLines,
166 /// 1-based line number in the input stream.
167 pub line_number: u64,
168 /// Per-entry warnings about parsing anomalies.
169 pub warnings: Vec<ParseWarning>,
170}