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