Pbxray - see inside your PBX (free CLI, static binary, no deps)

Built this because I was tired of eyeballing a full log next to a pjsip set logger on dump trying to line up a Dial() with the INVITE/BYE it actually caused.

pbxray parses Asterisk full/messages logs plus pjsip set logger on output into one correlated timeline - dialplan and SIP signalling together, queryable from the terminal. Single static binary, no runtime deps.

$ pbxray ls calls.log --failed
LINKED-ID   DATETIME             FROM         TO           CHANNELS  DURATION  CAUSE
C-00001a2b  2026-07-20 09:12:03  15555550142  15555550199  1         4s        17 (User Busy)

$ pbxray show calls.log C-00001a2b
2026-07-20 09:12:03  dialplan  Dial(PJSIP/15555550199@TRUNK,30) on PJSIP/alice-00000001
2026-07-20 09:12:03  sip  tx INVITE sip:15555550199@sip.example.com
2026-07-20 09:12:03  sip  rx 486 Busy Here

ls for filtering (time range, failed calls, number/DID, endpoint), show for the full timeline, tail -f for live. SIP<->call links that are best-effort guesses get flagged (probable link) rather than presented as fact.

Install:

curl -fsSL https://raw.githubusercontent.com/pbxray/pbxray-dist/main/install.sh | sh

Free. Docs & updates: https://pbxray.dev · binaries/source: GitHub - pbxray/pbxray-dist · GitHub

One ask: what did you try to debug with it, and what did it fail to show you?

This is why my Seaskirt wrapper for Asterisk APIs includes access to the console API as well. I have an example script somewhere which originates a call, and then monitors both Manager events and console output, with the option to write them (with timestamps) to separate logfiles or to the same logfile.

The script mainline looks like this:

    loop = asyncio.get_running_loop()
    run_done = loop.create_future()
    manager = await seaskirt.ManagerAsync \
      (
        username = username,
        password = password,
        want_events = True
      )
    console = await seaskirt.ConsoleAsync()
    for cmd in channel_types[channel_type]["log_verbose"] :
        console.send(cmd)
    #end for
    if combined :
        combined_log = open("%s-combined.log" % outnumber, "wt")
        event_log = console_log = combined_log
    else :
        combined_log = None
        event_log = open("%s-events.log" % outnumber, "wt")
        console_log = open("%s-console.log" % outnumber, "wt")
    #end if
    events_task = loop.create_task(listen_events(manager, event_log))
    console_task = loop.create_task(listen_console(console, console_log))
    await manager.send_request \
      (
        action = "Originate",
        parms =
            {
                "Channel" : inchannel,
                "Context" : outcontext,
                "Exten" : outnumber,
                "Priority" : 1,
                "Async" : "true",
            }
      )
    await run_done
    sys.stderr.write("terminating\n")
    events_task.cancel()
    console_task.cancel()
    await console.close()
    await manager.close()
    if combined :
        combined_log.close()
    else :
        event_log.close()
        console_log.close()
    #end if