Skip to main content

freeswitch_log_parser/message/
classify.rs

1//! The classification dispatcher — the ordered prefix checks that map a
2//! message to its [`MessageKind`], and the shape-specific constructors.
3
4use freeswitch_types::VARIABLE_PREFIX;
5
6use crate::codec::CodecMedia;
7
8use super::dtmf::parse_dtmf;
9use super::kind::MessageKind;
10use super::lifecycle::{
11    channel_lifecycle, classify_channel_prefixed, detect_channel_lifecycle, originate_success,
12};
13use super::media::{detect_media, detect_sdp_direction};
14use super::parts::{
15    dialplan_parts, execute_parts, parse_bracketed_value, set_export_parts, strip_channel_prefix,
16};
17use super::varname::VarName;
18
19fn parse_execute(msg: &str) -> MessageKind {
20    let parts = execute_parts(msg);
21    MessageKind::Execute {
22        depth: parts.depth,
23        channel: parts.channel.to_string(),
24        application: parts.application.to_string(),
25        arguments: parts.arguments.to_string(),
26    }
27}
28fn parse_dialplan(msg: &str) -> MessageKind {
29    let (channel, detail) = dialplan_parts(msg);
30    MessageKind::Dialplan {
31        channel: channel.to_string(),
32        detail: detail.to_string(),
33    }
34}
35/// Classify a log message's text into a [`MessageKind`].
36///
37/// Pure function — no state, no allocation beyond the returned enum. Works on
38/// the `message` field from [`RawLine`](crate::RawLine) or any raw message string.
39pub fn classify_message(msg: &str) -> MessageKind {
40    if msg.starts_with("EXECUTE ") || msg.starts_with("Execute ") {
41        return parse_execute(msg);
42    }
43
44    if msg.starts_with("RECV DTMF ")
45        || msg.starts_with("RTP RECV DTMF ")
46        || msg.starts_with("INFO DTMF(")
47    {
48        if let Some(dtmf) = parse_dtmf(msg) {
49            return dtmf;
50        }
51    }
52
53    if msg.starts_with("Dialplan: ") || msg.starts_with("Chatplan: ") {
54        return parse_dialplan(msg);
55    }
56
57    if msg.starts_with("Processing ")
58        && (msg.contains(" in context ") || msg.contains("recursive conditions"))
59    {
60        return parse_dialplan_processing(msg);
61    }
62
63    if msg.contains("CHANNEL_DATA") {
64        return MessageKind::ChannelData;
65    }
66
67    if msg.starts_with(VARIABLE_PREFIX) {
68        if let Some((name, value)) = parse_bracketed_value(msg, VARIABLE_PREFIX.len()) {
69            return MessageKind::Variable {
70                name: VarName::new(name),
71                value: value.to_string(),
72            };
73        }
74    }
75
76    if let Some(direction) = detect_sdp_direction(msg) {
77        return MessageKind::SdpMarker { direction };
78    }
79
80    if msg.contains("State Change") || msg.contains("Callstate Change") {
81        return MessageKind::StateChange {
82            detail: msg.to_string(),
83        };
84    }
85
86    // `set()` logs its verb first, and the stack variants share the shape.
87    if msg.starts_with("SET ")
88        || msg.starts_with("EXPORT ")
89        || msg.starts_with("PUSH ")
90        || msg.starts_with("UNSHIFT ")
91    {
92        if let Some(sv) = parse_set_or_export(msg) {
93            return sv;
94        }
95    }
96
97    if msg.starts_with("Audio Codec Compare ") {
98        return MessageKind::CodecNegotiation {
99            media: CodecMedia::Audio,
100        };
101    }
102
103    if msg.starts_with("Video Codec Compare ") {
104        return MessageKind::CodecNegotiation {
105            media: CodecMedia::Video,
106        };
107    }
108
109    if msg.starts_with("CoreSession::setVariable(") {
110        return parse_core_session_set_variable(msg);
111    }
112
113    if msg.starts_with("UNSET ") {
114        return parse_unset(msg);
115    }
116
117    if let Some(rest) = msg.strip_prefix("set variable ") {
118        if let Some((name, value)) = rest.split_once('=') {
119            return MessageKind::Variable {
120                name: VarName::new(name),
121                value: value.to_string(),
122            };
123        }
124    }
125
126    if msg.starts_with("Transfer ") {
127        return MessageKind::Dialplan {
128            channel: String::new(),
129            detail: msg.to_string(),
130        };
131    }
132
133    if msg.starts_with('(') {
134        if msg.contains(") State ") {
135            return MessageKind::StateChange {
136                detail: msg.to_string(),
137            };
138        }
139        return channel_lifecycle(msg);
140    }
141
142    if msg.starts_with("SOFIA ") {
143        return MessageKind::StateChange {
144            detail: msg.to_string(),
145        };
146    }
147
148    if msg.starts_with("checking condition") || msg.starts_with("action(") {
149        return channel_lifecycle(msg);
150    }
151
152    if msg.starts_with("Event Socket Command") {
153        return MessageKind::EventSocket {
154            detail: msg.to_string(),
155        };
156    }
157
158    if let Some(kind) = detect_media(msg) {
159        return kind;
160    }
161
162    if let Some(kind) = originate_success(msg) {
163        return kind;
164    }
165
166    if let Some(kind) = detect_channel_lifecycle(msg) {
167        return kind;
168    }
169
170    if let Some((channel_part, rest)) = strip_channel_prefix(msg) {
171        return classify_channel_prefixed(channel_part, rest);
172    }
173
174    // Last: `Key: [value]` is the loosest shape here, and every arm above
175    // would otherwise lose its lines to it.
176    if let Some((name, value)) = parse_bracketed_value(msg, 0) {
177        let name_bytes = name.as_bytes();
178        if !name_bytes.is_empty()
179            && !name.contains(' ')
180            && name_bytes[0].is_ascii_alphabetic()
181            && (name.contains('-') || name.starts_with("Channel-"))
182        {
183            return MessageKind::ChannelField {
184                name: name.to_string(),
185                value: value.to_string(),
186            };
187        }
188    }
189
190    MessageKind::General
191}
192fn parse_core_session_set_variable(msg: &str) -> MessageKind {
193    let rest = &msg["CoreSession::setVariable(".len()..];
194    if let Some(end) = rest.strip_suffix(')') {
195        if let Some(comma) = end.find(", ") {
196            return MessageKind::Variable {
197                name: VarName::new(&end[..comma]),
198                value: end[comma + 2..].to_string(),
199            };
200        }
201    }
202    MessageKind::Variable {
203        name: VarName::new(""),
204        value: msg.to_string(),
205    }
206}
207
208fn parse_unset(msg: &str) -> MessageKind {
209    let rest = &msg["UNSET ".len()..];
210    let name = if let Some(inner) = rest.strip_prefix('[') {
211        inner.strip_suffix(']').unwrap_or(inner)
212    } else {
213        rest
214    };
215    MessageKind::Variable {
216        name: VarName::new(name),
217        value: String::new(),
218    }
219}
220
221fn parse_dialplan_processing(msg: &str) -> MessageKind {
222    let rest = &msg["Processing ".len()..];
223    MessageKind::Dialplan {
224        channel: String::new(),
225        detail: rest.to_string(),
226    }
227}
228fn parse_set_or_export(msg: &str) -> Option<MessageKind> {
229    let parts = set_export_parts(msg)?;
230    Some(MessageKind::Variable {
231        name: VarName::new(parts.name),
232        value: parts.value.to_string(),
233    })
234}