Oninit® UDR — ONI UDR Debug: Usage
Configuring and operating ONI UDR Debug day to day, once it's built and registered — shown here as it's actually deployed for the tsapi project. Building and registering the module itself is on the Install guide.
Configure
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 — ts,
cache, rowtype, json — 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.
From C — how it's instrumented
Everything on this page so far is the SQL side: an operator turning classes on and reading back what got captured. The other half is the C side — how a UDR module's own code decides what to write. 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)));
Three things worth noticing:
oni_udr_debug_class("ts")resolves a class name to its numeric id once, up front — everyONI_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 anyONI_DEBUG()call — every record this thread writes afterward (start, completion, and anything in between, including across other instrumented functions it calls into) carries that same id, so one request's records can be pulled back out of a queue shared by every concurrent session.- 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, not just C
— 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).
Worked examples
Real command/output pairs against a live instance, walked through as an operator would actually run them.
"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: the deployed config above ships every class at level 0. The subsystem is enabled and healthy; there's simply nothing turned on to capture yet.
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() (src/oni_ts_read_udr.c) that
this request passed through — nothing else queued because every
other class stayed at level 0. Message text and format-string arguments
line up directly:
ONI_DEBUG(ts_class, 2, (uint32_t)dbg_session_id, ("read start key=%d", (int)key));
/* ... the scan happens here, then on the way out ... */
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)));
The class column (0) is ts's numeric id, not
its name — oni_udr_debug_class("ts") resolved that once
at the top of the function, the same lookup that would return the
always-gated-off sentinel id if ts weren't a registered
class at all. 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."
Per-class activity with oni_debug_classes()
Useful before touching levels on a shared instance, and after, to confirm a change actually took:
EXECUTE FUNCTION oni_debug_classes(); -- ts:0:0 -- cache:0:0 -- rowtype:0:0 -- json:0:0 -- ... after the two calls above, with ts at level 3 ... EXECUTE FUNCTION oni_debug_classes(); -- ts:3:2 -- cache:0:0 -- rowtype:0:0 -- json:0:0
The count keeps accumulating even after the class is set back to 0
— captured_count tracks everything captured since the
last initialization, not since the class was last enabled, so it's not a
"since I turned this on" counter across a level toggle.
Instrument your own call site, end to end
The three 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 (Install guide, step 2), 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.
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. In one call it distinguishes several reasons: the subsystem is
switched off, no class is enabled, 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, and
oni_debug_why() tells you which one applies rather than
leaving you to guess from an empty log file.
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 —
tsapi's own disposable verify scripts do this deliberately as test
scaffolding. 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();