# freeswitch-log-parser
[](https://github.com/ticpu/freeswitch-log-parser/actions/workflows/ci.yml)
[](https://github.com/ticpu/freeswitch-log-parser/actions/workflows/ci.yml)
[](https://crates.io/crates/freeswitch-log-parser)
[](https://docs.rs/freeswitch-log-parser)
[](https://github.com/ticpu/freeswitch-log-parser/blob/master/LICENSE)
Rust library for parsing FreeSWITCH log files. Three-layer streaming
architecture, no regex, single runtime dependency (`freeswitch-types`
for typed enums).
## Layers
```
Layer 1: parse_line() &str -> RawLine (stateless, zero-alloc)
Layer 2: LogStream Iterator -> LogEntry (structural state machine)
Layer 3: SessionTracker LogStream -> EnrichedEntry (per-UUID state)
```
**Layer 1** classifies individual log lines into five formats (Full,
System, UuidContinuation, BareContinuation, Truncated) and extracts
positional fields (UUID, timestamp, log level, source, message).
**Layer 2** groups continuation lines, detects block boundaries
(CHANNEL_DATA dumps, SDP bodies), reassembles multi-line variable
values, and classifies messages into semantic `MessageKind` variants:
`Execute`, `Dialplan`, `ChannelData`, `ChannelField`, `Variable`,
`SdpMarker`, `StateChange`, `CodecNegotiation`, `Media`,
`ChannelLifecycle`, `SipInvite`, `EventSocket`, `General`, plus the
synthetic `FileChange`/`DateChange` markers. `SipInvite` is the
canonical `sip_call_id ↔ channel_uuid` correlation primitive — sofia
emits it for every inbound and outbound call regardless of dialplan.
Every entry carries both a typed `Block` and raw `attached` lines.
**Layer 3** maintains per-UUID session state (dialplan context, channel
state, learned variables, call direction, caller/destination numbers)
and propagates it across entries. Also links bridged a-leg ↔ b-leg
sessions via `other_leg_uuid` — from the explicit `Peer UUID:` suffix
when present, or by matching a live b-leg `channel_name` when
FreeSWITCH omits it. Yields `EnrichedEntry` with a `SessionSnapshot`
alongside the raw `LogEntry`.
Each layer wraps the previous and can be used independently.
## Performance
Built to handle the worst `mod_logfile` produces — 2 KiB buffer
truncations, multi-line CHANNEL_DATA dumps with embedded SDP/XML,
write-contention collisions. An 11 MB fixture (185 physical lines
averaging ~60 KB each) parses in ~60 ms. `LogEntry::attached` uses
a compact contiguous buffer (`AttachedLines`) instead of
`Vec<String>` to amortize allocations on CHANNEL_DATA-heavy entries
— iteration via `&entry.attached` works unchanged.
## Usage
```rust
use std::io::{self, BufRead};
use freeswitch_log_parser::{LogStream, SessionTracker};
for enriched in SessionTracker::new(stream) {
let entry = &enriched.entry;
println!("{} {} {}", entry.uuid, entry.message_kind, entry.message);
if let Some(session) = &enriched.session {
if let Some(ctx) = &session.dialplan_context {
println!(" context: {ctx}");
}
}
}
```
## Unclassified data tracking
Lines that can't be fully classified are tracked, never silently dropped:
```rust
use freeswitch_log_parser::{LogStream, UnclassifiedTracking};
let mut stream = LogStream::new(lines)
.unclassified_tracking(UnclassifiedTracking::TrackLines);
for entry in stream.by_ref() { /* ... */ }
let stats = stream.stats();
eprintln!("{} lines, {} unclassified",
stats.lines_processed, stats.lines_unclassified);
```
## Custom relationship detection
The built-in leg linking handles standard FreeSWITCH patterns. For
application-specific patterns (Lua API results, custom SIP headers),
register a hook:
```rust
let tracker = SessionTracker::new(stream)
.with_relationship_hook(|entry, state| {
// Check entry.message_kind, state.variables, etc.
// Set state.other_leg_uuid if pattern matches
});
```
See `examples/custom_relationship.rs` for a complete example.
## Multi-file input with segment tracking
`TrackedChain` concatenates named input segments (typically rotated log
files) into a single iterator and records where each segment starts.
`SegmentTracker` then maps any line number back to its source file —
useful for emitting `FileChange` markers or reporting errors with the
original filename:
```rust
use freeswitch_log_parser::{LogStream, TrackedChain};
let segments = vec![
("freeswitch.log.1".into(), open_xz("freeswitch.log.1.xz")),
("freeswitch.log".into(), open_plain("freeswitch.log")),
];
let (chain, tracker) = TrackedChain::new(segments);
let stream = LogStream::new(chain);
// ... consume stream; query tracker.segment_for_line(n) as needed.
```
## `fslog` binary
The crate ships an `fslog` CLI that turns the parser into a log-search
tool: structured, UUID-aware, and able to follow a call across its
bridged and transferred legs. It reads rotated `.xz` files directly and
chains them in date order, so a single query spans the whole retention
window.
Build with `cargo build --release --features cli` for everything below,
or `--features tui` to also get the `monitor` dashboard. The library
itself pulls no CLI dependencies unless a feature is enabled.
### Commands
| `fslog list` | List discovered log files with dates and sizes |
| `fslog search` | Filter entries across many files (date range, UUID, level, pattern) |
| `fslog read [FILE]` | Parse a single file (or stdin with `-`) |
| `fslog tail [FILE]` | Follow a file live, parsed and colorized |
| `fslog monitor` | Live TUI call table (requires `--features tui`) |
| `fslog completions <SHELL>` | Emit a shell completion script |
Global flags: `--dir <PATH>` (or `FSLOG_DIR`, default
`/var/log/freeswitch`), `--color auto|always|never`, `--no-pager`.
### `search`
```
fslog search [OPTIONS] [PATTERN]
```
`PATTERN` is a case-insensitive fixed-string shorthand for `--fgrep`.
Filtering:
- `-u, --uuid <UUID>` — session UUID substring; repeat for OR matching
- `-l, --level <LEVEL>` — minimum severity (`debug`…`console`)
- `-c, --category <KIND>` — message kind (`execute`, `dialplan`, `media`, …)
- `--fgrep <PATTERN>` — case-insensitive fixed-string match
- `--grep <REGEX>` — regex match
- `--match-blocks` — also match `--fgrep`/`--grep`/`PATTERN` inside attached block lines (SDP, CHANNEL_DATA, codec negotiation), not just the message
- `--related` — expand matching sessions to their bridged/transferred peer legs (originate, `bridge()`, `uuid_bridge`, `Other-Leg-Unique-ID`, peer-UUID channel variables)
Context (grep-style), with `--` dividers between non-contiguous groups:
- `-A, --after-context <N>`, `-B, --before-context <N>`, `-C, --context <N>`
Output and selection:
- `--blocks` — expand CHANNEL_DATA fields/variables and SDP bodies inline
- `--session` — annotate each entry with tracked state (context, channel state, channel name)
- `--stats` / `--unclassified` — summary and unclassified-line report
- `-n, --line-numbers` — show physical line numbers
- `--from <DATE>` / `--until <DATE>` — bound discovery (progressive: `2026-03`, `2026-03-08`, `2026-03-08T15:48`)
- `--file <FILE>` — scan explicit files instead of date discovery (repeatable)
- `-y, --yes` — skip the confirmation prompt for large scans
UUIDs are rendered in a stable per-call truecolor so distinct sessions
stay visually separable across interleaved output.
### Examples
```sh
# Follow one call across all its legs, with session state
fslog search --from 2026-03-08 -u 9bee8676 --related --session
# grep a string with two lines of context on each side
fslog search --from 2026-03-08 'receiving invite' -C 2
# Find calls by an SDP attribute that only appears in the media block
fslog search --from 2026-03-08 --grep 'm=audio' --match-blocks --blocks
# Errors and worse from one session, expanding structured blocks
fslog search --from 2026-03-08 -u 9bee8676 -l err --blocks
```
## License
LGPL-2.1-or-later