Getting Started with Logwalker: Reading an Informix Logical Log Without onlog -l
Logwalker is a single command-line binary that walks an Informix®
logical-log file forward from a given uniqid and prints every
record — BEGIN, COMMIT, HINSERT, HUPAFT, HDELETE, and the rest of the
engine's internal record types — in a form a human can actually read.
It is a drop-in replacement for onlog -l for anyone who has
ever squinted at that tool's terse, column-positional dump trying to work
out what a transaction actually did. Logwalker is read-only
inspection, not a CDC capture mode: no replay stream, no
subscription, every open() is O_RDONLY and every
read is pread().
Where it has to run
There is no config file and no daemon, but there is one hard constraint:
Logwalker must run locally on the source Informix server,
as user informix or another account in the informix
group. Raw log dbspaces live on disk with 0660 informix:informix
permissions and there is no remote or network protocol that exposes them
— the binary checks access(R_OK) against every chunk in
the resolved extent map before it starts, and refuses with a
[CRITICAL] line if any one of them isn't readable. A
sysmaster ESQL/C connection is also required, to resolve the
LSN → chunk-path mapping and, if you ask for named output,
partnum → (db, owner, table).
Walk a single log
The minimal invocation names a log uniqid — something
still on disk, not archived past the LOGFILES ring:
$ logwalker -n 480661
LSN 480661:0x00000018 xid=4812 BEGIN pid=73628 uid=1000 06/16/2026 14:02:11
LSN 480661:0x00000050 xid=4812 UNIQID coris.sysprocedures next_serial=2723
LSN 480661:0x00000080 xid=4812 INSERT into coris.sysprocedures (rowid 0x00000101, 64 bytes)
LSN 480661:0x000000c0 xid=4812 COMMIT 06/16/2026 14:02:11
Three pre-flight gates run before any of that prints: the sysmaster
connect, the uniqid resolve against syslogs, and the chunk
readability check above. Any one failing exits non-zero with a
[CRITICAL] diagnostic instead of a partial walk — either
every chunk in the extent map is readable, or nothing gets printed at all.
-m <count> caps the number of records if you're
sanity-checking a big log, and -f, --filter T,U,… narrows
the walk to a comma-separated list of record types (names like
HINSERT,COMMIT, or hex codes like 0x0028,0x0002).
Pick an output shape
Logwalker emits three output shapes, chosen by flag; pick whichever matches how you're going to read the result:
- HUMAN (the default, shown above) — one line per
record, LSN-prefixed, with a plain-English description and partnums
rendered as their resolved
<db>.<table>names where possible. --compat onlog— the legacyonlog -lsix-column shape (addr len type xid id link) plus per-type tail extras, byte-for-byte compatible with the original tool for scripts that already parse that form.--group-by-xid— the same per-record content as HUMAN, wrapped in explicit--- TX <xid> BEGIN … ---/--- TX <xid> COMMITTED … ---framing per transaction. Because Informix recyclesxidonce a transaction completes, a later BEGIN reusing a prior frame's xid is tagged(xid REUSED)so the framing never gets ambiguous — don't dedupe on raw xid alone in any of the three modes.
Track one object across a range of logs
-t, --table <db:table|partnum> switches from walking one
log in full to tracking a single object — every record whose primary
partition matches the target — across a range of
logs, bounded by --from-log/--to-log or the
date-scoped --from-time/--to-time. It replaces
-n, since a range walk derives its own log set:
$ logwalker -t coris:sysprocedures --from-time '2026-06-16'
A bare filter by partition would show that a row changed but not
whether the change persisted — an INSERT inside a
transaction that later rolled back is still an INSERT record in the log.
So in HUMAN and --group-by-xid form, track mode passes through
the boundary of every transaction that touches the object and prints its
verdict once known:
=== TX 4812 opened on coris.sysprocedures (pid=73628 uid=1000 began 06/16/2026 14:02:11) ===
LSN 480661:0x00000080 xid=4812 INSERT into coris.sysprocedures (rowid 0x00000101, 64 bytes)
=== TX 4812 COMMITTED @ 06/16/2026 14:02:11 — 1 tracked change(s) PERSISTED ===
=== TX 4815 opened on coris.sysprocedures (pid=73630 uid=1000 began 06/16/2026 14:02:14) ===
LSN 480661:0x000001dc xid=4815 DELETE from coris.sysprocedures (rowid 0x00000090)
=== TX 4815 ROLLED BACK — 1 tracked change(s) DISCARDED (did NOT persist) ===
A transaction whose COMMIT or ROLLBACK falls beyond the walked range is flagged explicitly as open in the end-of-walk summary, because its fate is genuinely unknown until the range is widened far enough to see it.
Decode the row images
By default an INSERT/UPDATE/DELETE record's row image prints as raw bytes.
-r, --decode-rows turns that into (col1=val1,
col2=val2, …) against a live syscolumns map,
pulled per-database on startup. Without the CDC capture API's
cdc_set_fullrowlogging() opted in for a table, Informix's
standard logging only records the changed columns plus key columns, so
partial rows render honestly —
(col1=val1, col4=val4, …other columns NOT LOGGED)
— instead of pretending the rest of the row is known.
Reconstruct SQL and DDL from catalog changes
A stored procedure, a trigger, a view, or a schema change doesn't appear in
the log as its own record — it's DML against the system catalogs,
the same as any other insert. --extract-spl and
--extract-ddl walk those catalog rows and reassemble the
statement behind them, printed once as an end-of-walk summary rather than
interleaved with the rest of the record stream:
$ logwalker -n 480661 --extract-spl
=== Reconstructed SQL/SPL (from sysprocbody text rows) ===
--- coris procid=1073 (raise_salary) [2 text row(s)] ---
create procedure raise_salary(p_empno int, p_pct decimal(5,2))
define v_old decimal(10,2);
select salary into v_old from employee where empno = p_empno;
update employee set salary = v_old * (1 + p_pct/100) where empno = p_empno;
end procedure;
--extract-ddl covers the rest of the catalog surface: CREATE
TABLE and CREATE INDEX synthesized from systables /
syscolumns / sysindexes row images (there's no
source text to concatenate here — the SQL is assembled from column
metadata), and DROP statements from the catalog's before-image on delete:
$ logwalker -n 480661 --extract-ddl
=== Reconstructed DDL (from system-catalog inserts) ===
--- TABLE coris.informix.orders (tabid=500) [3 column(s)] ---
create table orders (
orderno serial,
custid integer,
odate date
);
--- INDEX coris.idx_ord_cust (tabid=500) ---
create unique index idx_ord_cust on orders (custid, orderno desc);
--- DROP trigger coris.trg_old_audit ---
drop trigger trg_old_audit;
An ALTER TABLE on an existing table is told apart from a fresh CREATE
because the systables row is updated, not inserted,
and comes back as its ADD / MODIFY / DROP column forms, each catalog type
code formatted back into its SQL type:
--- TABLE coris.informix.account (tabid=600) [alter] ---
alter table account add (iban char(34), opened date);
alter table account modify (balance money(16,2));
alter table account drop (notes);
Tracking down a dropped table or index
"Who dropped this and when" is one of the most common reasons to reach for
Logwalker after the fact. For a dropped table, view, or
trigger, --extract-ddl already does the work shown
above: it reconstructs the DROP statement straight from the catalog's
HDELETE before-image, so a plain walk of the suspect range (optionally with
--group-by-xid) surfaces both the object's name and, via the
BEGIN framing the same transaction, the pid/uid/timestamp of whoever did
it. DROP INDEX isn't reconstructed by --extract-ddl
yet — only CREATE INDEX, and DROP for TABLE/VIEW/TRIGGER, are
currently wired up. For an index, track the database's sysindexes
catalog partition directly instead:
$ logwalker -t coris:sysindexes --from-time '2026-06-16'
Every row-level change to that database's index catalog — including the HDELETE that removed your index — shows up with its own transaction boundary, the same as tracking any other table.
One wrinkle either way: once an object is dropped, its catalog entry is
gone, so -t <db:table> can no longer resolve it by name
— the phase-1 catalog lookup only sees objects that currently exist.
Carry the partnum forward instead: any walk taken before
the drop (a plain HUMAN or --compat onlog run, or a
--decode-rows walk) shows the partnum in hex for every record
against that object, and that hex value is what -t needs once
the name no longer resolves.
Where to go next
Operations documents every flag and
the pre-flight/exit-code contract in full; Output Examples has live samples of all
three output shapes plus the object-track and reconstruction output above,
captured from a real walk rather than hand-written. SQL & DDL Reconstruction covers
--extract-spl/--extract-ddl in depth, including
their current caveats. If something doesn't match what you expected to
see, start with Troubleshooting;
to get the binary itself, see Download.