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