Skip to content

How Routing Logs Work

Every time the switch runs a dialed number through a translation chain, it writes a forensic routing record: which translation contexts were walked, which blocks were tested and what they said, every rewrite the number went through, and what the walk resolved to. Where a CDR tells you what happened on a call, the routing log tells you why the switch routed it the way it did — the tool for “why did this number go to voicemail?”, “what rewrote this prefix?”, and “which block caught this call?”. Managing routing logs covers querying and retention.

The unit is a single router invocation, not a call. A simple answered call usually produces one record; a call that gets forwarded or transferred produces one per re-route. Each record’s source says where the router ran:

source When
edge The network edge resolved the initial INVITE — the normal path for every new call. No media channel exists yet, so edge records carry no leg_uuid.
ingest The call controller resolved the initial INVITE itself, because the call arrived without an edge decision (fallback path).
reroute An in-call re-walk: a call forward firing, a namespace transfer, an application redirect, a voicemail-driven redial.

Only genuine translation-chain walks are recorded — a feature code like *98 short-circuits before the router and produces no record.

Writes are deliberately fire-and-forget: persisting a forensic record never blocks call setup, and a storage hiccup drops the record rather than disturbing the call. The routing log is an investigative aid, not an accounting ledger — for billing-grade completeness, use CDRs.

Correlation — the same identifiers as CDRs

Section titled “Correlation — the same identifiers as CDRs”

The edge mints the call ULID at the moment it feeds the INVITE into the router, and the controller adopts it — so a call’s routing records and its CDR share one identifier. The workflow this enables: spot a suspect call in the CDRs, paste its call_ulid into the routing log, and read every routing decision that call went through.

  • call_ulid — on every record; groups all of a call’s walks.
  • leg_uuid — the inbound leg, on ingest/reroute records only (edge records predate the channel).
  • sip_call_id — the inbound SIP dialog Call-ID, carried on every record as a correlation fallback (the edge takes it from the routing request, controller records from the inbound leg).

Each record has two layers: a small envelope (what you see in listings) and the full trace (fetched per record).

Field Meaning
call_ulid / record_id / leg_uuid Correlation (record_id orders a call’s walks chronologically)
at / source When and where the router ran
start_context The translation context the walk began in
starting_number The number as dialed, before any rewriting
final_number The number after every rewrite that fired
matched Whether the walk found a destination
target_type / target_id What it resolved to (see below)
force_treatment Set when the walk was aborted into a treatment (a failed must-* rewrite, a policy denial) instead of a destination

target_type is the resolved destination’s kind — line, ring-group, device, trunk, route, voicemail-box, treatment, recording, namespace, pstn, hangup, translation-context, or application — and target_id names the specific resource.

The trace is the walk’s full story, in two parallel lists:

  • hops — one entry per translation context walked (a translation-context redirect ends one hop and opens the next). Each hop lists the blocks the router actually evaluated, in priority order, each with a human-readable descriptor of its match condition ("ast _9XXXX", "exact 1234", "always", "and(…)"), whether it matched, and — for a match — the target it carried, if any. A matched block with no target is a rewrite-only block: its modifications were applied and the walk continued. Blocks after the final winner never appear, because they were never tested.
  • rewrites — every modify operation that fired, tagged with the context and block priority that owned it, with the number before and after each step. A failed must-* operation is marked failed and pairs with the envelope’s force_treatment.

Together they answer the two forensic questions precisely: which block decided this call (the last evaluated entry with a target) and how the dialed number became the final number (the rewrite chain, step by step). A trimmed example:

{
"hops": [
{ "context": "internal", "evaluated": [
{ "priority": 10, "matched": false, "match": "exact 0" },
{ "priority": 20, "matched": true, "has_target": false, "match": "ast _9XXXXXXXXXX" },
{ "priority": 30, "matched": true, "has_target": true,
"target": "translation-context:pstn-out", "match": "always" }
]},
{ "context": "pstn-out", "evaluated": [
{ "priority": 10, "matched": true, "has_target": true,
"target": "pstn:", "match": "ast _+1XXXXXXXXXX" }
]}
],
"rewrites": [
{ "context": "internal", "priority": 20, "op": "strip-prefix", "arg": "9",
"before": "95551234567", "after": "5551234567" },
{ "context": "internal", "priority": 20, "op": "prepend", "arg": "+1",
"before": "5551234567", "after": "+15551234567" }
]
}

vsscli route-logs get renders exactly this as a readable step ledger — you rarely need to read the raw JSON. Records also carry a schema_version so the format can evolve without breaking consumers.

The generation flow, end to end:

  1. Edge: the edge proxy asks the routing service to resolve a new INVITE. The resolver returns the decision and the trace; the edge handler assembles the record, stamps the freshly minted call ULID, and hands it to an asynchronous recorder. The call proceeds immediately.
  2. Controller: ingest-fallback and in-call re-walks emit the record onto the same per-call event stream that feeds the CDR writer; a dedicated forensics writer persists it off the call’s hot path.
  3. Records are append-only and immutable — written once, never updated. There is no “open record” phase as with CDRs; a record appears once its walk has completed, effectively immediately.

Storage is organized around the four questions the log answers, which is why the query surface is shaped the way it is: by call (every decision of one call, with the full trace), by leg, by dialed number over time (an exact-number audit index), and by namespace + date (the listing scan). Ad-hoc analytics across namespaces or by arbitrary fields belong in a downstream analytics store, not here.

Retention — routing logs expire on their own

Section titled “Retention — routing logs expire on their own”

Unlike CDRs (kept until purged), routing records auto-expire, by default after 90 days — the dataset is high-volume and self-bounds. The window is a platform-level storage setting, not per-namespace. The range purge exists for erasing records earlier than the automatic expiry (compliance erasure), not as the primary retention mechanism.