freeswitch_log_parser/session/
tracker.rs1use std::collections::{HashMap, HashSet};
4
5use crate::message::MessageKind;
6use crate::stream::{LogEntry, LogStream, ParseStats, UnclassifiedLine};
7
8use super::conference::{self, ConferenceEvent, ConferenceMembership, ConferenceRegistry};
9use super::index::{deindex, IndexedFieldChanges, IndexedFields};
10use super::loopback;
11use super::parse::{parse_new_channel, parse_originate_channel, parse_originate_success};
12use super::state::{SessionSnapshot, SessionState};
13use super::SessionHook;
14
15#[derive(Debug)]
17pub struct EnrichedEntry {
18 pub entry: LogEntry,
19 pub session: Option<SessionSnapshot>,
21}
22
23pub struct SessionTracker<I> {
30 inner: LogStream<I>,
31 pub(super) sessions: HashMap<String, SessionState>,
32 pub(super) by_channel_name: HashMap<String, HashSet<String>>,
33 pub(super) by_pending_target: HashMap<String, HashSet<String>>,
37 pub(super) by_other_leg: HashMap<String, String>,
38 pub(super) conferences: ConferenceRegistry,
39 pre_hook: Option<SessionHook>,
40 post_hook: Option<SessionHook>,
41}
42
43impl<I: Iterator<Item = String>> SessionTracker<I> {
44 pub fn new(inner: LogStream<I>) -> Self {
46 SessionTracker {
47 inner,
48 sessions: HashMap::new(),
49 by_channel_name: HashMap::new(),
50 by_pending_target: HashMap::new(),
51 by_other_leg: HashMap::new(),
52 conferences: ConferenceRegistry::default(),
53 pre_hook: None,
54 post_hook: None,
55 }
56 }
57
58 pub fn with_pre_hook<F>(mut self, hook: F) -> Self
66 where
67 F: Fn(&LogEntry, &mut SessionState) + Send + 'static,
68 {
69 self.pre_hook = Some(Box::new(hook));
70 self
71 }
72
73 pub fn with_post_hook<F>(mut self, hook: F) -> Self
98 where
99 F: Fn(&LogEntry, &mut SessionState) + Send + 'static,
100 {
101 self.post_hook = Some(Box::new(hook));
102 self
103 }
104
105 pub fn sessions(&self) -> &HashMap<String, SessionState> {
107 &self.sessions
108 }
109
110 pub fn conference_members<'a>(&'a self, instance: &'a str) -> impl Iterator<Item = &'a str> {
113 self.conferences.members(instance)
114 }
115
116 pub fn remove_session(&mut self, uuid: &str) -> Option<SessionState> {
119 let state = self.sessions.remove(uuid)?;
120 let changes =
124 IndexedFieldChanges::diff(IndexedFields::of(&state), &SessionState::default());
125 self.apply_index_changes(uuid, &changes);
126 Some(state)
127 }
128
129 pub fn stats(&self) -> &ParseStats {
131 self.inner.stats()
132 }
133
134 pub fn drain_unclassified(&mut self) -> Vec<UnclassifiedLine> {
136 self.inner.drain_unclassified()
137 }
138 fn update_conference(&mut self, uuid: &str, entry: &LogEntry) {
143 let target = match conference::detect(entry) {
144 Some(ConferenceEvent::Leave) => {
145 if let Some(state) = self.sessions.get_mut(uuid) {
146 state.conference = None;
147 }
148 return;
149 }
150 Some(ConferenceEvent::Join(target)) => Some(target),
151 None => None,
152 };
153
154 let Some(state) = self.sessions.get(uuid) else {
155 return;
156 };
157 let Some(target) = target.or_else(|| conference::target_from_variables(&state.variables))
158 else {
159 if let Some(state) = self.sessions.get_mut(uuid) {
160 let SessionState {
161 conference,
162 variables,
163 ..
164 } = state;
165 if let Some(membership) = conference {
166 conference::refresh(membership, variables);
167 }
168 }
169 return;
170 };
171
172 let instance = match state.conference.as_ref() {
176 Some(current) if current.name == target.name => current.instance.clone(),
177 _ => self
178 .conferences
179 .instance_for(&target.name)
180 .map(str::to_string)
181 .unwrap_or_else(|| uuid.to_string()),
182 };
183
184 let Some(state) = self.sessions.get_mut(uuid) else {
185 return;
186 };
187 let SessionState {
188 conference,
189 variables,
190 ..
191 } = state;
192 let joining_elsewhere = conference.as_ref().is_none_or(|c| c.name != target.name);
193 if joining_elsewhere {
194 *conference = Some(ConferenceMembership {
195 name: target.name,
196 profile: target.profile.clone(),
197 instance,
198 member_id: None,
199 conference_uuid: None,
200 });
201 }
202 let Some(membership) = conference.as_mut() else {
203 return;
204 };
205 if target.profile.is_some() {
206 membership.profile = target.profile;
207 }
208 conference::refresh(membership, variables);
209 }
210
211 fn sole_live_leg(&self, candidates: &HashSet<String>, exclude: &str) -> Option<String> {
215 let mut live = candidates
216 .iter()
217 .filter(|u| u.as_str() != exclude)
218 .filter(|u| {
219 self.sessions
220 .get(*u)
221 .map(|s| !s.is_terminal())
222 .unwrap_or(false)
223 });
224 match (live.next(), live.next()) {
225 (Some(only), None) => Some(only.clone()),
226 _ => None,
227 }
228 }
229
230 fn unique_live_leg(&self, channel: &str, exclude: &str) -> Option<String> {
232 self.sole_live_leg(self.by_channel_name.get(channel)?, exclude)
233 }
234
235 fn unique_pending_leg(&self, target: &str, exclude: &str) -> Option<String> {
237 self.sole_live_leg(self.by_pending_target.get(target)?, exclude)
238 }
239
240 fn loopback_a_leg(&self, b_channel: &str, b_uuid: &str) -> Option<String> {
244 let a_channel = loopback::a_leg_name(b_channel)?;
245 self.unique_live_leg(&a_channel, b_uuid)
246 }
247
248 fn link_pair(&mut self, a_uuid: &str, b_uuid: &str) {
255 let a_old_pending = self
256 .sessions
257 .get(a_uuid)
258 .and_then(|s| s.pending_bridge_target.clone());
259
260 let a_state = self.sessions.entry(a_uuid.to_string()).or_default();
261 let a_old_leg = a_state.other_leg_uuid.replace(b_uuid.to_string());
262 a_state.pending_bridge_target = None;
263
264 let b_state = self.sessions.entry(b_uuid.to_string()).or_default();
265 let b_old_leg = b_state.other_leg_uuid.replace(a_uuid.to_string());
266
267 self.index_other_leg(a_uuid, a_old_leg, b_uuid);
268 self.index_other_leg(b_uuid, b_old_leg, a_uuid);
269 if let Some(old_target) = a_old_pending {
270 deindex(&mut self.by_pending_target, &old_target, a_uuid);
271 }
272 }
273
274 fn link_legs(&mut self, uuid: &str, entry: &LogEntry) {
277 if entry.message.contains("Originate Resulted in Success") {
279 if let Some(peer_uuid) = parse_originate_success(&entry.message) {
280 self.link_pair(uuid, &peer_uuid);
281 } else if let Some(chan) = parse_originate_channel(&entry.message) {
282 if let Some(b_uuid) = self.unique_live_leg(chan, uuid) {
285 self.link_pair(uuid, &b_uuid);
286 }
287 }
288 return;
289 }
290
291 if let MessageKind::ChannelLifecycle { detail } = &entry.message_kind {
294 if let Some(channel_name) = parse_new_channel(detail) {
295 let a_uuid = self
296 .by_other_leg
297 .get(uuid)
298 .cloned()
299 .or_else(|| self.unique_pending_leg(&channel_name, uuid))
300 .or_else(|| self.loopback_a_leg(&channel_name, uuid))
301 .filter(|a| a.as_str() != uuid);
302
303 if let Some(a_uuid) = a_uuid {
304 self.link_pair(&a_uuid, uuid);
305 }
306 }
307 }
308 }
309}
310
311impl<I: Iterator<Item = String>> Iterator for SessionTracker<I> {
312 type Item = EnrichedEntry;
313
314 fn next(&mut self) -> Option<EnrichedEntry> {
315 let mut entry = self.inner.next()?;
316
317 let Some(uuid) = entry.uuid.clone() else {
318 return Some(EnrichedEntry {
319 entry,
320 session: None,
321 });
322 };
323
324 let state = self.sessions.entry(uuid.clone()).or_default();
325
326 let old = IndexedFields::of(state);
330
331 if let Some(hook) = &self.pre_hook {
332 hook(&entry, state);
333 }
334
335 let unreadable = state.update_from_entry(&entry);
336
337 self.update_conference(&uuid, &entry);
338 self.link_legs(&uuid, &entry);
339
340 if let Some(hook) = &self.post_hook {
344 let state = self.sessions.entry(uuid.clone()).or_default();
345 hook(&entry, state);
346 }
347
348 let state = self.sessions.entry(uuid.clone()).or_default();
349 let changes = IndexedFieldChanges::diff(old, state);
350 let snapshot = state.snapshot();
351 self.apply_index_changes(&uuid, &changes);
352 entry.warnings.extend(unreadable);
353
354 Some(EnrichedEntry {
355 entry,
356 session: Some(snapshot),
357 })
358 }
359}