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 crate::codec::CodecMedia;
6
7/// Which end of a call an SDP body belongs to.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum SdpDirection {
10    Local,
11    /// Local SDP sent in a 180/183 early media response.
12    LocalRing,
13    Remote,
14    /// SDP reference that doesn't specify local or remote.
15    Unknown,
16}
17
18/// Direction of a sofia SIP INVITE log line.
19#[non_exhaustive]
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SipInviteDirection {
22    /// `sofia/X/Y receiving invite ...` — inbound INVITE on a sofia profile.
23    Receiving,
24    /// `sofia/X/Y sending invite ...` — outbound INVITE on a sofia profile.
25    Sending,
26}
27
28/// Source of a DTMF event log line.
29#[non_exhaustive]
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DtmfSource {
32    /// RFC2833 DTMF decoded at the RTP layer (`switch_rtp.c`).
33    Rtp,
34    /// DTMF queued to channel after validation (`switch_channel.c`).
35    Channel,
36    /// DTMF received via SIP INFO method (`sofia.c`).
37    SipInfo,
38}
39
40impl fmt::Display for DtmfSource {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            DtmfSource::Rtp => f.pad("rtp"),
44            DtmfSource::Channel => f.pad("channel"),
45            DtmfSource::SipInfo => f.pad("sip-info"),
46        }
47    }
48}
49
50impl fmt::Display for SdpDirection {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            SdpDirection::Local => f.pad("local"),
54            SdpDirection::LocalRing => f.pad("local-ring"),
55            SdpDirection::Remote => f.pad("remote"),
56            SdpDirection::Unknown => f.pad("unknown"),
57        }
58    }
59}
60
61/// Semantic classification of a log message's content.
62///
63/// `Display` includes variant-specific detail (e.g. `execute(set)`, `var(sip_call_id)`)
64/// while [`label()`](MessageKind::label) returns just the category string.
65#[non_exhaustive]
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum MessageKind {
68    /// Dialplan application execution trace (`EXECUTE [depth=N] channel app(args)`).
69    Execute {
70        depth: u32,
71        channel: String,
72        application: String,
73        arguments: String,
74    },
75    /// Dialplan processing output — regex matching, actions, context routing.
76    Dialplan { channel: String, detail: String },
77    /// Start of a CHANNEL_DATA variable dump block.
78    ChannelData,
79    /// A `Channel-*` or similar hyphenated field from a CHANNEL_DATA dump.
80    ChannelField { name: String, value: String },
81    /// A channel variable named with its value, whichever narration logged it.
82    /// `name` always carries the `variable_` prefix.
83    Variable { name: String, value: String },
84    /// Start of an SDP body block (`Local SDP:`, `Remote SDP:`).
85    SdpMarker { direction: SdpDirection },
86    /// Channel state transition (`State Change`, `Callstate Change`, `SOFIA` state).
87    StateChange { detail: String },
88    /// `Audio Codec Compare` lines during codec negotiation.
89    CodecNegotiation { media: CodecMedia },
90    /// RTP, RTCP, recording, and other media-related messages.
91    Media { detail: String },
92    /// Channel lifecycle events — new/close/hangup, bridge, ring, REFER, CANCEL, BYE.
93    ChannelLifecycle { detail: String },
94    /// Sofia logged a SIP INVITE on this channel — the line is one of:
95    /// - `sofia/<profile>/<endpoint> receiving invite from <ip>:<port> ... call-id: <id>`
96    /// - `sofia/<profile>/<endpoint> sending invite [version: ...] [call-id: <id>]`
97    ///
98    /// Always emitted by sofia for every inbound and outbound call regardless
99    /// of dialplan — the canonical primitive for `sip_call_id ↔ channel_uuid`
100    /// correlation. The line's leading UUID is on [`crate::LogEntry::uuid`].
101    SipInvite {
102        direction: SipInviteDirection,
103        /// The sofia profile name (segment between `sofia/` and the next `/`).
104        profile: String,
105        /// SIP `Call-ID` from the log line. `None` when sofia logs `(null)`
106        /// (typical for outbound at pre-routing time — a later log entry on
107        /// the same UUID will carry the actual id) or when the line carries
108        /// no `call-id:` field (the version-only DEBUG follow-up for sending).
109        call_id: Option<String>,
110    },
111    /// Event socket commands from `mod_event_socket`.
112    EventSocket { detail: String },
113    /// DTMF digit received on the channel.
114    /// Format: `[RTP] RECV DTMF <digit>:<duration_ms>` or `INFO DTMF(<digit>)`
115    Dtmf {
116        /// Where the DTMF was logged (RTP layer, channel layer, or SIP INFO).
117        source: DtmfSource,
118        /// The DTMF digit (0-9, *, #, A-D, or F for flash).
119        digit: char,
120        /// Duration in milliseconds. `None` for SIP INFO DTMF (no duration logged).
121        duration_ms: Option<u32>,
122    },
123    /// Anything not matching a more specific pattern.
124    General,
125    /// Synthetic marker emitted at log file boundaries (never from `classify_message`).
126    FileChange,
127    /// Synthetic marker emitted at date boundaries (never from `classify_message`).
128    DateChange,
129}
130
131impl MessageKind {
132    /// Exhaustive list of all category label strings, in declaration order.
133    pub const ALL_LABELS: &[&str] = &[
134        "execute",
135        "dialplan",
136        "channel-data",
137        "channel-field",
138        "variable",
139        "sdp-marker",
140        "state-change",
141        "codec-negotiation",
142        "media",
143        "channel-lifecycle",
144        "sip-invite",
145        "event-socket",
146        "dtmf",
147        "general",
148        "file-change",
149        "date-change",
150    ];
151
152    /// Returns the bare category string without variant-specific data.
153    pub fn label(&self) -> &'static str {
154        match self {
155            MessageKind::Execute { .. } => "execute",
156            MessageKind::Dialplan { .. } => "dialplan",
157            MessageKind::ChannelData => "channel-data",
158            MessageKind::ChannelField { .. } => "channel-field",
159            MessageKind::Variable { .. } => "variable",
160            MessageKind::SdpMarker { .. } => "sdp-marker",
161            MessageKind::StateChange { .. } => "state-change",
162            MessageKind::CodecNegotiation { .. } => "codec-negotiation",
163            MessageKind::Media { .. } => "media",
164            MessageKind::ChannelLifecycle { .. } => "channel-lifecycle",
165            MessageKind::SipInvite { .. } => "sip-invite",
166            MessageKind::EventSocket { .. } => "event-socket",
167            MessageKind::Dtmf { .. } => "dtmf",
168            MessageKind::General => "general",
169            MessageKind::FileChange => "file-change",
170            MessageKind::DateChange => "date-change",
171        }
172    }
173}
174
175impl fmt::Display for MessageKind {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            MessageKind::Execute { application, .. } => write!(f, "execute({})", application),
179            MessageKind::Dialplan { .. } => f.pad("dialplan"),
180            MessageKind::ChannelData => f.pad("channel-data"),
181            MessageKind::ChannelField { name, .. } => write!(f, "field({})", name),
182            MessageKind::Variable { name, .. } => write!(f, "var({})", name),
183            MessageKind::SdpMarker { direction } => write!(f, "sdp({})", direction),
184            MessageKind::StateChange { .. } => f.pad("state-change"),
185            MessageKind::CodecNegotiation { media } => {
186                f.pad(&format!("codec-negotiation({media})"))
187            }
188            MessageKind::Media { .. } => f.pad("media"),
189            MessageKind::ChannelLifecycle { .. } => f.pad("channel-lifecycle"),
190            MessageKind::SipInvite { .. } => f.pad("sip-invite"),
191            MessageKind::EventSocket { .. } => f.pad("event-socket"),
192            MessageKind::Dtmf {
193                source,
194                digit,
195                duration_ms,
196            } => match duration_ms {
197                Some(ms) => write!(f, "dtmf({source}:{digit}:{ms}ms)"),
198                None => write!(f, "dtmf({source}:{digit})"),
199            },
200            MessageKind::General => f.pad("general"),
201            MessageKind::FileChange => f.pad("file-change"),
202            MessageKind::DateChange => f.pad("date-change"),
203        }
204    }
205}