Getting Started with ONI UDR Debug: A Diagnostic Subsystem for Informix C UDRs That Never Fails Silently
ONI UDR Debug is a generic, reusable diagnostic subsystem for C UDR
code — one shared, structured record of what your UDRs actually
did, built into any Informix® DataBlade module that links it. It
isn't tied to any one project's schema or business logic: the
tsapi TimeSeries API uses it internally, but the
subsystem itself has no idea what a TimeSeries is. Link it into
whatever C UDR module needs a place to put diagnostic output.
Why not mi_trace()?
The DataBlade tracing facility is available, and ONI UDR Debug can
emit to it as an optional secondary sink, but it isn't the primary
mechanism — and the reason isn't performance. It's that every
one of mi_trace()'s failure modes is silent: a trace
class not registered in systraceclasses produces no
output and no error; the trace file is established per session, so
code running in a PER_SYSTEM or background context has
no obvious destination; DPRINTF may be compiled out
entirely depending on build configuration, so identical source
behaves differently between builds; a wrong path or wrong
permissions produces nothing, again without an error. In every one
of those cases the operator sees an empty file and has no way to
tell which reason applies. ONI UDR Debug is built around the
opposite principle: if something goes wrong, that failure itself is
recorded and surfaced, never swallowed.
What it gives you
- One shared queue, instance-wide. A single
PER_SYSTEMcircular message queue holds every session's records — no per-session trace files to hunt down, no missing context because a background VP had nowhere obvious to write. - Capture-scope filtering. Run wide open
(
system— every session) or scoped to just the sessions that opted in (session), with correlation IDs so interleaved records from concurrent sessions can be pulled back apart per request. - Truncation that tells you it happened. An over-length record is truncated, never silently split or dropped — and the truncation itself is flagged on the record, in the running counters, and in the dump output.
- Flushing that survives disk trouble. Flushes trigger on queue occupancy, on age, or on demand, with a single flush owner at a time. A failing disk doesn't lose queued records — they stay in memory, readable, until the subsystem self-disables after a configurable run of consecutive failures rather than looping forever against a dead disk.
- A full SQL admin surface. Turn it on, check status, ask why nothing is showing up, adjust a class's verbosity, force a flush — all from ordinary SQL, no shell access to the server required.
How it's instrumented, from C
Every call site funnels through one gating macro:
#define ONI_DEBUG(cls, lvl, session_id, args) \
do { \
if ((lvl) <= oni_debug_level(cls)) \
oni_debug_printf((cls), (lvl), (session_id), ONI_DEBUG_FMT_ARGS args); \
} while (0)
oni_debug_level() is a single array-index read against
PER_SYSTEM shared state. When the class's level is
below what the call site asks for, the args tuple
— which can be an arbitrary printf-style format
string and its arguments — is never evaluated and
vsnprintf never runs. A disabled call site costs one
load and one branch, which is what makes it safe to leave
permanently instrumented in a hot path rather than compiling it
out.
A real, live call site — oni_ts_read()'s own
request lifecycle in src/oni_ts_read_udr.c, trimmed to
the debug-relevant lines:
uint16_t ts_class = oni_udr_debug_class("ts");
uint64_t req_t0 = oni_monotonic_ms();
oni_debug_set_correlation(req_t0 ^ (uint64_t)(uint32_t)key);
ONI_DEBUG(ts_class, 2, (uint32_t)dbg_session_id, ("read start key=%d", (int)key));
/* ... the actual TimeSeries scan happens here ... */
ONI_DEBUG(ts_class, 1, (uint32_t)dbg_session_id,
("read done key=%d rows=%zu ms=%llu", (int)key, row_count,
(unsigned long long)(oni_monotonic_ms() - req_t0)));
oni_udr_debug_class("ts") resolves a class
name to its numeric id once, up front — every
ONI_DEBUG() call after that is an id comparison, not a
string lookup. An unregistered name resolves to a sentinel id
that's always gated off, never a crash.
oni_debug_set_correlation() is called once per
request, before any ONI_DEBUG() call — every
record this thread writes afterward carries that same id, so one
request's records can be pulled back out of a queue shared by every
concurrent session. And the completion call is written
unconditionally on both the success and failure paths in the real
source — a level-1 "read done"/"read failed" record either
way, so enabling a class never means only seeing the happy path.
To instrument your own module — C or C++, since
oni_debug.h wraps its declarations in
extern "C" specifically so C++ UDR code can call
this API directly — register a class once at init:
int cls = oni_debug_class_register("myclass", /* default_level */ 0);
— then gate every call site through ONI_DEBUG()
the same way, with whatever session_id your own
connection context can supply (0 is a valid sentinel
if you have none yet).
Configure it
Configuration is a small key/value document, parsed once at
initialization. This is tsapi's own real deployed
file:
version: 1
debug:
file: /home/informix/live/tmp/oni_udr.debug
max_messages: 1000
max_flush_failures: 5
max_file_bytes: 268435456
keep_files: 5
capture: system
classes:
ts: 0
cache: 0
rowtype: 0
json: 0
capture: system records every session's activity into
the one shared queue; capture: session restricts
capture to sessions that have explicitly called
oni_debug_on(). Each entry under classes:
registers a named debug class at a starting verbosity level —
your own UDR code writes into whichever classes it defines
(tsapi's own four are project-specific; name your own
to match your UDR code), and oni_debug_class_set()
adjusts a class's level at runtime without a restart. Out-of-range
values (a negative queue size, an oversized limit, and so on) are
clamped to a documented default or bound and reported as a
warning, never silently accepted or rejected outright.
"The debug file is empty" — diagnosing with oni_debug_why()
The most common operator complaint. Don't go read the file first — ask the subsystem why it has nothing:
EXECUTE FUNCTION oni_debug_why();
-- NO_CLASSES_ENABLED
This is the expected state right after startup: a fresh config
ships every class at level 0. The subsystem is enabled and
healthy; there's simply nothing turned on to capture yet.
oni_debug_why() distinguishes several other reasons in
the same one call: the subsystem is switched off entirely, it's on
and genuinely idle, this session is out of capture scope, the queue
is full, or it's self-disabled after repeated flush failures —
each a different fix, so there's never a guessing game from an
empty log file.
Enable a class, capture, confirm it reached disk
Raise a class to level 3, run some real traffic, and check that records actually queued before flushing:
EXECUTE PROCEDURE oni_debug_class_set('ts', 3);
EXECUTE FUNCTION oni_ts_read(1, '2026-09-22 00:00:00', '2026-09-22 01:00:00', 'cpu.wait');
EXECUTE FUNCTION oni_debug_why();
-- OK
SELECT (oni_debug_status()).queue_count,
(oni_debug_status()).messages_queued,
(oni_debug_status()).debug_file_size
FROM sysmaster:sysdual;
-- queue_count messages_queued debug_file_size
-- 2 2 0
Two records queued (one entry, one completion per call) but
debug_file_size is still 0 — queued isn't
written yet. oni_debug_dump() reaches the same two
records without flushing:
EXECUTE FUNCTION oni_debug_dump();
-- 0 1758578400123 27 193847561 0 2 read start key=1
-- 1 1758578400129 27 193847561 0 1 read done key=1 rows=60 ms=8
These two lines are exactly the two ONI_DEBUG() call
sites in oni_ts_read() that this request passed
through — nothing else queued because every other class
stayed at level 0. The class column (0 above) is
ts's numeric id, not its name; both rows share one
correlation id because oni_debug_set_correlation() was
called once, before either ONI_DEBUG() call, for this
one request. Now flush for real:
EXECUTE PROCEDURE oni_debug_queue_flush();
SELECT (oni_debug_status()).queue_count,
(oni_debug_status()).messages_flushed,
(oni_debug_status()).debug_file_size,
(oni_debug_status()).flush_count
FROM sysmaster:sysdual;
-- queue_count messages_flushed debug_file_size flush_count
-- 0 2 118 1
debug_file_size/flush_count move,
queue_count drops back to 0, and
oni_debug_why() now reports
NOTHING_PENDING rather than OK —
"healthy, currently idle" versus "healthy, currently producing."
Instrument your own call site, end to end
The examples above all read records someone else's C code wrote. Here's the whole loop for a class you add yourself, from source to SQL. Register the class once, at module init:
int myclass = oni_debug_class_register("myclass", /* default_level */ 0);
Gate the call site you care about with ONI_DEBUG(),
following the same shape as the real ts call sites
above:
ONI_DEBUG(myclass, 2, session_id, ("processing order=%d total=%.2f", order_id, total));
Rebuild and redeploy under a fresh version directory, then from SQL:
EXECUTE PROCEDURE oni_debug_class_set('myclass', 2);
-- ... trigger the code path that hits your ONI_DEBUG() call ...
EXECUTE FUNCTION oni_debug_dump();
-- 0 1758581000441 31 0 4 2 processing order=1042 total=59.99
Class id 4 here is whatever position
oni_debug_class_register() assigned it at init —
registration order, not something you choose. The correlation id
is 0 because nothing called
oni_debug_set_correlation() for this request; do that
first, the same way oni_ts_read() does, if you want
records from one logical request to be recoverable out of a queue
shared by every concurrent session.
Troubleshooting
Start with oni_debug_why() whenever nothing seems to
be happening — see above for the full list of reasons it
distinguishes. If flushing has self-disabled,
oni_debug_status() reports the last flush error
((oni_debug_status()).last_flush_error) alongside the
running failure count — fix the underlying disk/permission
issue, and the next successful flush resets the counter
automatically.
A previously-registered class suddenly reporting as
"unregistered" (oni_debug_class_set() raising
ON011) is the signature of something else having
called oni_debug_init() directly with a different
config — test scaffolding does this deliberately sometimes.
Confirm via oni_debug_status() (an unexpected
debug_file/queue_capacity value, e.g. a
/tmp/... path instead of the real deployed one), then
restore the real config without needing a restart:
EXECUTE PROCEDURE oni_debug_config_reload();
Where to go next
The Install guide covers building the module, deploying it, and registering the SQL surface from a clean start. The Usage guide has everything on this page plus more, including the full call reference. ONI UDR Debug overview covers the design rationale in more depth; ONI UDR Debug ships as part of the same Oninit® UDR source distribution that includes the tsapi TimeSeries API it's used to build.