Skip to main content

freeswitch_log_parser/session/
state.rs

1//! Per-UUID accumulated state and the point-in-time snapshot attached to each
2//! enriched entry.
3
4use std::collections::HashMap;
5use std::str::FromStr;
6
7use freeswitch_types::variables::VariableName;
8use freeswitch_types::{CallDirection, CallState, ChannelState, EventHeader, HangupCause};
9
10use crate::line::parse_line;
11use crate::message::{classify_message, LifecycleEvent, MessageKind};
12use crate::stream::{Block, LogEntry, ParseWarning, SessionReading};
13use crate::uuid::is_uuid;
14
15use super::conference::ConferenceMembership;
16use super::media::SessionMedia;
17use super::parse::{
18    parse_bridge_args, parse_dialplan_context, parse_hangup, parse_new_channel,
19    parse_processing_line, parse_state_change, StateChange,
20};
21
22/// Resolve a typed value into `slot`, or record that it could not be read.
23///
24/// The failed reading leaves whatever `slot` already held — a repeat of a field
25/// the vocabulary cannot read must not erase what an earlier one established.
26fn read<T: FromStr>(
27    slot: &mut Option<T>,
28    value: &str,
29    reading: SessionReading,
30    warnings: &mut Vec<ParseWarning>,
31) {
32    match T::from_str(value) {
33        Ok(parsed) => *slot = Some(parsed),
34        Err(_) => warnings.push(ParseWarning::UnreadableValue {
35            reading,
36            value: ParseWarning::excerpt(value),
37        }),
38    }
39}
40
41/// Where an entry's dump values come from.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43enum DumpSource {
44    /// A reassembled block holds them, so the raw attached lines are skipped:
45    /// re-applying one would clobber a multi-line value with its first fragment.
46    Block,
47    /// The attached lines are all there is.
48    AttachedLines,
49}
50
51/// Mutable per-UUID state accumulator, updated as entries are processed.
52///
53/// Fields are `None` until the corresponding data is first seen in the stream.
54/// Variables accumulate from CHANNEL_DATA dumps, `set()`/`export()` executions,
55/// `SET`/`EXPORT` log lines, and inline `variable_*` lines.
56#[derive(Debug, Clone, Default)]
57#[non_exhaustive]
58pub struct SessionState {
59    /// `None` until a `Channel-Name` field is encountered.
60    pub channel_name: Option<String>,
61    /// `None` until a `State Change` line or a `Channel-State` field is seen.
62    pub channel_state: Option<ChannelState>,
63    /// `None` until a `Callstate Change` line is seen. A distinct vocabulary from
64    /// [`channel_state`](Self::channel_state); neither displaces the other.
65    pub call_state: Option<CallState>,
66    /// First dialplan context seen; set once and never overwritten.
67    pub initial_context: Option<String>,
68    /// Destination of the first `Processing` line = the dialed number at ingress;
69    /// set once and never overwritten (unlike last-wins `dialplan_to`).
70    pub initial_destination: Option<String>,
71    /// Current dialplan context; updated on each transfer/continue.
72    pub dialplan_context: Option<String>,
73    /// Caller side of the last `Processing` line — a number, or a display name
74    /// and number. `None` until one is seen.
75    pub dialplan_from: Option<String>,
76    /// Dialed destination of the last `Processing` line. `None` until one is seen.
77    pub dialplan_to: Option<String>,
78    /// Call direction from `Call-Direction` CHANNEL_DATA field; `None` until seen.
79    pub call_direction: Option<CallDirection>,
80    /// Caller ID number from `Caller-Caller-ID-Number` CHANNEL_DATA field; `None` until seen.
81    pub caller_id_number: Option<String>,
82    /// Caller ID name from `Caller-Caller-ID-Name` CHANNEL_DATA field; `None` until seen.
83    pub caller_id_name: Option<String>,
84    /// Destination number from `Caller-Destination-Number` CHANNEL_DATA field; `None` until seen.
85    pub destination_number: Option<String>,
86    /// Hangup cause extracted from ChannelLifecycle Hangup detail; `None` until hangup seen.
87    pub hangup_cause: Option<HangupCause>,
88    /// Timestamp when "has been answered" lifecycle event was seen; `None` until answered.
89    pub answered_at: Option<String>,
90    /// Other leg's UUID; `None` until bridged. Set from `Originate Resulted in Success` on A-leg,
91    /// and from `New Channel` on B-leg (back-pointing to A-leg via originate context).
92    pub other_leg_uuid: Option<String>,
93    /// Conference this session is currently a member of; `None` once it leaves.
94    pub conference: Option<ConferenceMembership>,
95    /// Codecs negotiated on this leg, by media type and direction.
96    pub media: SessionMedia,
97    /// Pending bridge target channel from `EXECUTE bridge()`, consumed when B-leg `New Channel` matches.
98    pub(crate) pending_bridge_target: Option<String>,
99    /// All variables learned so far, with the `variable_` prefix stripped from names.
100    pub variables: HashMap<String, String>,
101}
102/// Immutable point-in-time copy of a session's state, attached to each [`EnrichedEntry`](crate::EnrichedEntry).
103///
104/// Does not include `variables` to keep snapshots lightweight — access the full
105/// variable map via [`SessionTracker::sessions()`](crate::SessionTracker::sessions).
106#[derive(Debug, Clone)]
107#[non_exhaustive]
108pub struct SessionSnapshot {
109    pub channel_name: Option<String>,
110    pub channel_state: Option<ChannelState>,
111    pub call_state: Option<CallState>,
112    pub initial_context: Option<String>,
113    pub initial_destination: Option<String>,
114    pub dialplan_context: Option<String>,
115    pub dialplan_from: Option<String>,
116    pub dialplan_to: Option<String>,
117    pub call_direction: Option<CallDirection>,
118    pub caller_id_number: Option<String>,
119    pub caller_id_name: Option<String>,
120    pub destination_number: Option<String>,
121    pub hangup_cause: Option<HangupCause>,
122    pub answered_at: Option<String>,
123    pub other_leg_uuid: Option<String>,
124    pub conference: Option<ConferenceMembership>,
125    pub media: SessionMedia,
126}
127
128impl SessionState {
129    /// Value of a typed channel variable, or `None` if this session never saw it.
130    ///
131    /// Accepts any of `freeswitch-types`' variable-name enums, and spares the
132    /// caller from knowing that [`variables`](Self::variables) keys are stored
133    /// with the `variable_` prefix stripped.
134    pub fn variable<V: VariableName>(&self, var: V) -> Option<&str> {
135        self.variables.get(var.as_str()).map(String::as_str)
136    }
137
138    /// Bound exhaustively so a field added here and forgotten in the snapshot
139    /// fails to compile; the two `_` bindings are the deliberate omissions.
140    pub(super) fn snapshot(&self) -> SessionSnapshot {
141        let SessionState {
142            channel_name,
143            channel_state,
144            call_state,
145            initial_context,
146            initial_destination,
147            dialplan_context,
148            dialplan_from,
149            dialplan_to,
150            call_direction,
151            caller_id_number,
152            caller_id_name,
153            destination_number,
154            hangup_cause,
155            answered_at,
156            other_leg_uuid,
157            conference,
158            media,
159            pending_bridge_target: _,
160            variables: _,
161        } = self;
162
163        SessionSnapshot {
164            channel_name: channel_name.clone(),
165            channel_state: *channel_state,
166            call_state: *call_state,
167            initial_context: initial_context.clone(),
168            initial_destination: initial_destination.clone(),
169            dialplan_context: dialplan_context.clone(),
170            dialplan_from: dialplan_from.clone(),
171            dialplan_to: dialplan_to.clone(),
172            call_direction: *call_direction,
173            caller_id_number: caller_id_number.clone(),
174            caller_id_name: caller_id_name.clone(),
175            destination_number: destination_number.clone(),
176            hangup_cause: *hangup_cause,
177            answered_at: answered_at.clone(),
178            other_leg_uuid: other_leg_uuid.clone(),
179            conference: conference.clone(),
180            media: media.clone(),
181        }
182    }
183
184    /// Absorb one CHANNEL_DATA field. The collision splitter can hand a dump
185    /// field over as its own entry rather than inside a block, so both arrival
186    /// shapes decode here — a lighter reading on one of them would drop whatever
187    /// it left out, `Other-Leg-Unique-ID` included, only for split dumps.
188    fn apply_channel_field(&mut self, name: &str, value: &str, warnings: &mut Vec<ParseWarning>) {
189        match EventHeader::from_str(name) {
190            Ok(EventHeader::ChannelName) => self.channel_name = Some(value.to_string()),
191            Ok(EventHeader::ChannelState) => read(
192                &mut self.channel_state,
193                value,
194                SessionReading::ChannelState,
195                warnings,
196            ),
197            Ok(EventHeader::CallDirection) => read(
198                &mut self.call_direction,
199                value,
200                SessionReading::CallDirection,
201                warnings,
202            ),
203            Ok(EventHeader::CallerCallerIdNumber) => {
204                self.caller_id_number = Some(value.to_string())
205            }
206            Ok(EventHeader::CallerCallerIdName) => self.caller_id_name = Some(value.to_string()),
207            Ok(EventHeader::CallerDestinationNumber) => {
208                self.destination_number = Some(value.to_string())
209            }
210            Ok(EventHeader::OtherLegUniqueId) => self.other_leg_uuid = Some(value.to_string()),
211            _ => {}
212        }
213    }
214
215    /// Whether this session has reached a state it cannot leave. Stragglers in a
216    /// terminal state are never candidates for leg linking.
217    ///
218    /// `DOWN` is not terminal: it doubles as the initial call state before any
219    /// change is observed.
220    pub(super) fn is_terminal(&self) -> bool {
221        matches!(
222            self.channel_state,
223            Some(
224                ChannelState::CsHangup
225                    | ChannelState::CsReporting
226                    | ChannelState::CsDestroy
227                    | ChannelState::CsNone
228            )
229        ) || matches!(self.call_state, Some(CallState::Hangup))
230    }
231
232    /// Absorb an entry, returning whatever readings its values defeated.
233    pub(super) fn update_from_entry(&mut self, entry: &LogEntry) -> Vec<ParseWarning> {
234        let mut warnings = Vec::new();
235        let dump = match entry.block {
236            Some(Block::ChannelData { .. }) => DumpSource::Block,
237            _ => DumpSource::AttachedLines,
238        };
239        if let Some(Block::ChannelData { fields, variables }) = &entry.block {
240            for (name, value) in fields {
241                self.apply_channel_field(name, value, &mut warnings);
242            }
243            for (name, value) in variables {
244                self.variables
245                    .insert(name.bare().to_string(), value.clone());
246            }
247        }
248
249        match &entry.message_kind {
250            MessageKind::Execute {
251                application,
252                arguments,
253                ..
254            } => match application.as_str() {
255                "set" | "export" => {
256                    if let Some((name, value)) = arguments.split_once('=') {
257                        self.variables.insert(name.to_string(), value.to_string());
258                    }
259                }
260                "bridge" => {
261                    if let Some(info) = parse_bridge_args(arguments) {
262                        if let Some(uuid) = &info.origination_uuid {
263                            self.other_leg_uuid = Some(uuid.clone());
264                        }
265                        self.pending_bridge_target = Some(info.target_channel);
266                    }
267                }
268                _ => {}
269            },
270            MessageKind::ChannelLifecycle { event, detail } => match event {
271                LifecycleEvent::NewChannel => {
272                    if self.channel_name.is_none() {
273                        self.channel_name = parse_new_channel(detail).map(str::to_string);
274                    }
275                }
276                LifecycleEvent::Hangup => {
277                    if let Some(cause) = parse_hangup(detail) {
278                        read(
279                            &mut self.hangup_cause,
280                            cause,
281                            SessionReading::HangupCause,
282                            &mut warnings,
283                        );
284                    }
285                }
286                LifecycleEvent::Answered => {
287                    self.answered_at
288                        .get_or_insert_with(|| entry.timestamp.clone());
289                }
290                _ => {}
291            },
292            kind => self.apply_kind(kind, &mut warnings),
293        }
294
295        self.apply_processing(&entry.message);
296        self.media.update_from_entry(entry);
297
298        for attached in &entry.attached {
299            let parsed = parse_line(attached);
300            self.update_from_message(parsed.message, dump, &mut warnings);
301        }
302        warnings
303    }
304
305    /// Canonical extraction for message kinds that appear on both primary and
306    /// attached lines. Entry-only kinds (Execute, ChannelLifecycle — the
307    /// latter needs the entry timestamp) stay in `update_from_entry`.
308    fn apply_kind(&mut self, kind: &MessageKind, warnings: &mut Vec<ParseWarning>) {
309        match kind {
310            MessageKind::Dialplan { detail, .. } => {
311                if let Some(context) = parse_dialplan_context(detail) {
312                    self.initial_context.get_or_insert(context.to_string());
313                    self.dialplan_context = Some(context.to_string());
314                }
315            }
316            MessageKind::Variable { name, value } => {
317                self.variables
318                    .insert(name.bare().to_string(), value.clone());
319            }
320            MessageKind::ChannelField { name, value } => {
321                self.apply_channel_field(name, value, warnings)
322            }
323            // A suffix the log wrote is evidence of a peer, so a value that is
324            // not a UUID is named rather than left to read as an absent suffix.
325            MessageKind::OriginateSuccess {
326                peer_uuid: Some(peer),
327                ..
328            } if !is_uuid(peer) => warnings.push(ParseWarning::UnreadableValue {
329                reading: SessionReading::PeerUuid,
330                value: ParseWarning::excerpt(peer),
331            }),
332            MessageKind::StateChange { detail } => match parse_state_change(detail) {
333                Some(StateChange::Channel(to)) => read(
334                    &mut self.channel_state,
335                    to,
336                    SessionReading::ChannelState,
337                    warnings,
338                ),
339                Some(StateChange::Call(to)) => read(
340                    &mut self.call_state,
341                    to,
342                    SessionReading::CallState,
343                    warnings,
344                ),
345                None => {}
346            },
347            _ => {}
348        }
349    }
350
351    /// `Processing <caller>-><dest> in context <ctx>` — emitted by the dialplan
352    /// hunt on both primary and attached lines; anchored on raw text because
353    /// `classify_message` folds it into `Dialplan` with the prefix stripped.
354    fn apply_processing(&mut self, msg: &str) {
355        if let Some(dp) = parse_processing_line(msg) {
356            self.initial_context.get_or_insert(dp.context.clone());
357            self.initial_destination.get_or_insert(dp.to.clone());
358            self.dialplan_context = Some(dp.context);
359            self.dialplan_from = Some(dp.from);
360            self.dialplan_to = Some(dp.to);
361        }
362    }
363
364    fn update_from_message(
365        &mut self,
366        msg: &str,
367        dump: DumpSource,
368        warnings: &mut Vec<ParseWarning>,
369    ) {
370        let kind = classify_message(msg);
371        match &kind {
372            MessageKind::Variable { .. } | MessageKind::ChannelField { .. }
373                if dump == DumpSource::Block => {}
374            kind => self.apply_kind(kind, warnings),
375        }
376        self.apply_processing(msg);
377    }
378}