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