Skip to main content

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}