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}