Skip to main content

freeswitch_log_parser/message/
kind.rs

1//! The typed vocabulary a classified message decomposes into.
2
3use std::fmt;
4
5use super::VarName;
6use crate::codec::CodecMedia;
7
8/// Which end of a call an SDP body belongs to.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum SdpDirection {
11    Local,
12    /// Local SDP sent in a 180/183 early media response.
13    LocalRing,
14    Remote,
15    /// SDP reference that doesn't specify local or remote.
16    Unknown,
17}
18
19/// Direction of a sofia SIP INVITE log line.
20#[non_exhaustive]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SipInviteDirection {
23    /// `sofia/X/Y receiving invite ...` — inbound INVITE on a sofia profile.
24    Receiving,
25    /// `sofia/X/Y sending invite ...` — outbound INVITE on a sofia profile.
26    Sending,
27}
28
29/// Source of a DTMF event log line.
30#[non_exhaustive]
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum DtmfSource {
33    /// RFC2833 DTMF decoded at the RTP layer (`switch_rtp.c`).
34    Rtp,
35    /// DTMF queued to channel after validation (`switch_channel.c`).
36    Channel,
37    /// DTMF received via SIP INFO method (`sofia.c`).
38    SipInfo,
39}
40
41impl fmt::Display for DtmfSource {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            DtmfSource::Rtp => f.pad("rtp"),
45            DtmfSource::Channel => f.pad("channel"),
46            DtmfSource::SipInfo => f.pad("sip-info"),
47        }
48    }
49}
50
51impl fmt::Display for SdpDirection {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            SdpDirection::Local => f.pad("local"),
55            SdpDirection::LocalRing => f.pad("local-ring"),
56            SdpDirection::Remote => f.pad("remote"),
57            SdpDirection::Unknown => f.pad("unknown"),
58        }
59    }
60}
61
62/// Which lifecycle step a [`MessageKind::ChannelLifecycle`] line narrates.
63#[non_exhaustive]
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum LifecycleEvent {
66    /// `New Channel <channel> [<uuid>]` (`switch_channel.c`).
67    NewChannel,
68    /// `Hangup <channel> [<state>] [<cause>]` (`switch_channel.c`).
69    Hangup,
70    /// `Close Channel <channel> [<state>]` (`switch_core_session.c`).
71    Destroy,
72    /// `Channel [<channel>] has been answered` (`switch_channel.c`).
73    Answered,
74    /// A lifecycle line none of the above names.
75    Other,
76}
77
78impl fmt::Display for LifecycleEvent {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            LifecycleEvent::NewChannel => f.pad("new-channel"),
82            LifecycleEvent::Hangup => f.pad("hangup"),
83            LifecycleEvent::Destroy => f.pad("destroy"),
84            LifecycleEvent::Answered => f.pad("answered"),
85            LifecycleEvent::Other => f.pad("other"),
86        }
87    }
88}
89
90/// Semantic classification of a log message's content.
91///
92/// `Display` includes variant-specific detail (e.g. `execute(set)`, `var(sip_call_id)`)
93/// while [`label()`](MessageKind::label) returns just the category string.
94#[non_exhaustive]
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum MessageKind {
97    /// Dialplan application execution trace (`EXECUTE [depth=N] channel app(args)`).
98    Execute {
99        /// `None` when the trace's `[depth=N]` field is missing or unreadable.
100        depth: Option<u32>,
101        channel: String,
102        application: String,
103        arguments: String,
104    },
105    /// Dialplan processing output — regex matching, actions, context routing.
106    Dialplan { channel: String, detail: String },
107    /// Start of a CHANNEL_DATA variable dump block.
108    ChannelData,
109    /// A `Channel-*` or similar hyphenated field from a CHANNEL_DATA dump.
110    ChannelField { name: String, value: String },
111    /// A channel variable named with its value, whichever narration logged it.
112    /// [`VarName`] holds the bare name and renders the dump's prefixed spelling;
113    /// it compares against no `&str`, so a consumer matching a prefixed literal
114    /// fails to build rather than silently stopping.
115    Variable { name: VarName, value: String },
116    /// Start of an SDP body block (`Local SDP:`, `Remote SDP:`).
117    SdpMarker { direction: SdpDirection },
118    /// Channel state transition (`State Change`, `Callstate Change`, `SOFIA` state).
119    StateChange { detail: String },
120    /// `Audio Codec Compare` lines during codec negotiation.
121    CodecNegotiation { media: CodecMedia },
122    /// RTP, RTCP, recording, and other media-related messages.
123    Media { detail: String },
124    /// Channel lifecycle events — new/close/hangup, bridge, ring, REFER, CANCEL, BYE.
125    ChannelLifecycle {
126        event: LifecycleEvent,
127        detail: String,
128    },
129    /// `Originate Resulted in Success: [<channel>] Peer UUID: <uuid>`
130    /// (`switch_ivr_originate.c`) — the originating leg naming what it reached.
131    OriginateSuccess {
132        /// The bracketed target channel name, which may itself hold a UUID.
133        channel: String,
134        /// The `Peer UUID:` suffix as the line spelled it, `None` only on
135        /// builds that omit the suffix. A value the write buffer cut is still
136        /// present here and is not a UUID — see
137        /// [`is_uuid`](crate::is_uuid), which the per-session layer applies
138        /// before it links anything.
139        peer_uuid: Option<String>,
140    },
141    /// Sofia logged a SIP INVITE on this channel — the line is one of:
142    /// - `sofia/<profile>/<endpoint> receiving invite from <ip>:<port> ... call-id: <id>`
143    /// - `sofia/<profile>/<endpoint> sending invite [version: ...] [call-id: <id>]`
144    ///
145    /// Always emitted by sofia for every inbound and outbound call regardless
146    /// of dialplan — the canonical primitive for `sip_call_id ↔ channel_uuid`
147    /// correlation. The line's leading UUID is on [`crate::LogEntry::uuid`].
148    SipInvite {
149        direction: SipInviteDirection,
150        /// The sofia profile name (segment between `sofia/` and the next `/`).
151        profile: String,
152        /// SIP `Call-ID` from the log line. `None` when sofia logs `(null)`
153        /// (typical for outbound at pre-routing time — a later log entry on
154        /// the same UUID will carry the actual id) or when the line carries
155        /// no `call-id:` field (the version-only DEBUG follow-up for sending).
156        call_id: Option<String>,
157    },
158    /// Event socket commands from `mod_event_socket`.
159    EventSocket { detail: String },
160    /// DTMF digit received on the channel.
161    /// Format: `[RTP] RECV DTMF <digit>:<duration_ms>` or `INFO DTMF(<digit>)`
162    Dtmf {
163        /// Where the DTMF was logged (RTP layer, channel layer, or SIP INFO).
164        source: DtmfSource,
165        /// The DTMF digit (0-9, *, #, A-D, or F for flash).
166        digit: char,
167        /// Duration in milliseconds. `None` for SIP INFO DTMF (no duration logged).
168        duration_ms: Option<u32>,
169    },
170    /// Anything not matching a more specific pattern.
171    General,
172    /// Synthetic marker emitted at log file boundaries (never from `classify_message`).
173    FileChange,
174    /// Synthetic marker emitted at date boundaries (never from `classify_message`).
175    DateChange,
176}
177
178impl MessageKind {
179    /// Exhaustive list of all category label strings, in declaration order.
180    pub const ALL_LABELS: &[&str] = &[
181        "execute",
182        "dialplan",
183        "channel-data",
184        "channel-field",
185        "variable",
186        "sdp-marker",
187        "state-change",
188        "codec-negotiation",
189        "media",
190        "channel-lifecycle",
191        "originate-success",
192        "sip-invite",
193        "event-socket",
194        "dtmf",
195        "general",
196        "file-change",
197        "date-change",
198    ];
199
200    /// Returns the bare category string without variant-specific data.
201    pub fn label(&self) -> &'static str {
202        match self {
203            MessageKind::Execute { .. } => "execute",
204            MessageKind::Dialplan { .. } => "dialplan",
205            MessageKind::ChannelData => "channel-data",
206            MessageKind::ChannelField { .. } => "channel-field",
207            MessageKind::Variable { .. } => "variable",
208            MessageKind::SdpMarker { .. } => "sdp-marker",
209            MessageKind::StateChange { .. } => "state-change",
210            MessageKind::CodecNegotiation { .. } => "codec-negotiation",
211            MessageKind::Media { .. } => "media",
212            MessageKind::ChannelLifecycle { .. } => "channel-lifecycle",
213            MessageKind::OriginateSuccess { .. } => "originate-success",
214            MessageKind::SipInvite { .. } => "sip-invite",
215            MessageKind::EventSocket { .. } => "event-socket",
216            MessageKind::Dtmf { .. } => "dtmf",
217            MessageKind::General => "general",
218            MessageKind::FileChange => "file-change",
219            MessageKind::DateChange => "date-change",
220        }
221    }
222}
223
224impl fmt::Display for MessageKind {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            MessageKind::Execute { application, .. } => write!(f, "execute({})", application),
228            MessageKind::Dialplan { .. } => f.pad("dialplan"),
229            MessageKind::ChannelData => f.pad("channel-data"),
230            MessageKind::ChannelField { name, .. } => write!(f, "field({})", name),
231            MessageKind::Variable { name, .. } => write!(f, "var({name})"),
232            MessageKind::SdpMarker { direction } => write!(f, "sdp({})", direction),
233            MessageKind::StateChange { .. } => f.pad("state-change"),
234            MessageKind::CodecNegotiation { media } => {
235                f.pad(&format!("codec-negotiation({media})"))
236            }
237            MessageKind::Media { .. } => f.pad("media"),
238            MessageKind::ChannelLifecycle { .. } => f.pad("channel-lifecycle"),
239            MessageKind::OriginateSuccess { .. } => f.pad("originate-success"),
240            MessageKind::SipInvite { .. } => f.pad("sip-invite"),
241            MessageKind::EventSocket { .. } => f.pad("event-socket"),
242            MessageKind::Dtmf {
243                source,
244                digit,
245                duration_ms,
246            } => match duration_ms {
247                Some(ms) => write!(f, "dtmf({source}:{digit}:{ms}ms)"),
248                None => write!(f, "dtmf({source}:{digit})"),
249            },
250            MessageKind::General => f.pad("general"),
251            MessageKind::FileChange => f.pad("file-change"),
252            MessageKind::DateChange => f.pad("date-change"),
253        }
254    }
255}