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 `variable_*` field — from dumps, `SET`, `EXPORT`, `set()`, or `CoreSession::setVariable`.
82    Variable { name: String, value: String },
83    /// Start of an SDP body block (`Local SDP:`, `Remote SDP:`).
84    SdpMarker { direction: SdpDirection },
85    /// Channel state transition (`State Change`, `Callstate Change`, `SOFIA` state).
86    StateChange { detail: String },
87    /// `Audio Codec Compare` lines during codec negotiation.
88    CodecNegotiation { media: CodecMedia },
89    /// RTP, RTCP, recording, and other media-related messages.
90    Media { detail: String },
91    /// Channel lifecycle events — new/close/hangup, bridge, ring, REFER, CANCEL, BYE.
92    ChannelLifecycle { detail: String },
93    /// Sofia logged a SIP INVITE on this channel — the line is one of:
94    /// - `sofia/<profile>/<endpoint> receiving invite from <ip>:<port> ... call-id: <id>`
95    /// - `sofia/<profile>/<endpoint> sending invite [version: ...] [call-id: <id>]`
96    ///
97    /// Always emitted by sofia for every inbound and outbound call regardless
98    /// of dialplan — the canonical primitive for `sip_call_id ↔ channel_uuid`
99    /// correlation. The line's leading UUID is on [`crate::LogEntry::uuid`].
100    SipInvite {
101        direction: SipInviteDirection,
102        /// The sofia profile name (segment between `sofia/` and the next `/`).
103        profile: String,
104        /// SIP `Call-ID` from the log line. `None` when sofia logs `(null)`
105        /// (typical for outbound at pre-routing time — a later log entry on
106        /// the same UUID will carry the actual id) or when the line carries
107        /// no `call-id:` field (the version-only DEBUG follow-up for sending).
108        call_id: Option<String>,
109    },
110    /// Event socket commands from `mod_event_socket`.
111    EventSocket { detail: String },
112    /// DTMF digit received on the channel.
113    /// Format: `[RTP] RECV DTMF <digit>:<duration_ms>` or `INFO DTMF(<digit>)`
114    Dtmf {
115        /// Where the DTMF was logged (RTP layer, channel layer, or SIP INFO).
116        source: DtmfSource,
117        /// The DTMF digit (0-9, *, #, A-D, or F for flash).
118        digit: char,
119        /// Duration in milliseconds. `None` for SIP INFO DTMF (no duration logged).
120        duration_ms: Option<u32>,
121    },
122    /// Anything not matching a more specific pattern.
123    General,
124    /// Synthetic marker emitted at log file boundaries (never from `classify_message`).
125    FileChange,
126    /// Synthetic marker emitted at date boundaries (never from `classify_message`).
127    DateChange,
128}
129
130impl MessageKind {
131    /// Exhaustive list of all category label strings, in declaration order.
132    pub const ALL_LABELS: &[&str] = &[
133        "execute",
134        "dialplan",
135        "channel-data",
136        "channel-field",
137        "variable",
138        "sdp-marker",
139        "state-change",
140        "codec-negotiation",
141        "media",
142        "channel-lifecycle",
143        "sip-invite",
144        "event-socket",
145        "dtmf",
146        "general",
147        "file-change",
148        "date-change",
149    ];
150
151    /// Returns the bare category string without variant-specific data.
152    pub fn label(&self) -> &'static str {
153        match self {
154            MessageKind::Execute { .. } => "execute",
155            MessageKind::Dialplan { .. } => "dialplan",
156            MessageKind::ChannelData => "channel-data",
157            MessageKind::ChannelField { .. } => "channel-field",
158            MessageKind::Variable { .. } => "variable",
159            MessageKind::SdpMarker { .. } => "sdp-marker",
160            MessageKind::StateChange { .. } => "state-change",
161            MessageKind::CodecNegotiation { .. } => "codec-negotiation",
162            MessageKind::Media { .. } => "media",
163            MessageKind::ChannelLifecycle { .. } => "channel-lifecycle",
164            MessageKind::SipInvite { .. } => "sip-invite",
165            MessageKind::EventSocket { .. } => "event-socket",
166            MessageKind::Dtmf { .. } => "dtmf",
167            MessageKind::General => "general",
168            MessageKind::FileChange => "file-change",
169            MessageKind::DateChange => "date-change",
170        }
171    }
172}
173
174impl fmt::Display for MessageKind {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match self {
177            MessageKind::Execute { application, .. } => write!(f, "execute({})", application),
178            MessageKind::Dialplan { .. } => f.pad("dialplan"),
179            MessageKind::ChannelData => f.pad("channel-data"),
180            MessageKind::ChannelField { name, .. } => write!(f, "field({})", name),
181            MessageKind::Variable { name, .. } => write!(f, "var({})", name),
182            MessageKind::SdpMarker { direction } => write!(f, "sdp({})", direction),
183            MessageKind::StateChange { .. } => f.pad("state-change"),
184            MessageKind::CodecNegotiation { media } => {
185                f.pad(&format!("codec-negotiation({media})"))
186            }
187            MessageKind::Media { .. } => f.pad("media"),
188            MessageKind::ChannelLifecycle { .. } => f.pad("channel-lifecycle"),
189            MessageKind::SipInvite { .. } => f.pad("sip-invite"),
190            MessageKind::EventSocket { .. } => f.pad("event-socket"),
191            MessageKind::Dtmf {
192                source,
193                digit,
194                duration_ms,
195            } => match duration_ms {
196                Some(ms) => write!(f, "dtmf({source}:{digit}:{ms}ms)"),
197                None => write!(f, "dtmf({source}:{digit})"),
198            },
199            MessageKind::General => f.pad("general"),
200            MessageKind::FileChange => f.pad("file-change"),
201            MessageKind::DateChange => f.pad("date-change"),
202        }
203    }
204}