nx_log.nx source
↩ module page · 320 lines · 11162 B
1// nx_log.nx -- structured logging framework.
2//
3// Enterprise-grade replacement for ad-hoc `sys_write(2, "tag"...)`.
4// Design draws from:
5//
6// Google glog / Abseil LOG(INFO)
7// Rust tracing crate (levels, spans, structured fields)
8// Microsoft ETW (schema-typed events, high-throughput capture)
9// syslog RFC 5424 (level taxonomy, tag conventions)
10// OpenTelemetry logs data model (severity + body + attributes)
11//
12// What enterprise-grade means here:
13// * Levels -- TRACE/DEBUG/INFO/WARN/ERROR/FATAL. Compile-time
14// AND runtime gated; call sites below min_level compile to a
15// no-op branch, runtime filters emit to fd or drop.
16// * Tags -- every event carries a module-identifier tag for
17// grep / filter / routing. Mandatory, not optional.
18// * Structured output -- each line is
19// '<seq> <level> <tag> <msg>\n'
20// stable format, greppable, machine-parseable. No ANSI
21// escapes, no timestamps that change builds, no env-specific
22// formatting: F6-reproducible.
23// * Deterministic by default -- monotonic sequence counter, not
24// wall-clock time. Tests can snapshot logs byte-for-byte.
25// * Context object -- multiple logger instances can coexist
26// (e.g., parser log, codegen log, test log). Default global
27// context routes to stderr.
28// * Fatal path -- nx_log_fatal exits with NX_ASSERT_EXIT=200,
29// same code as nx_assert so harnesses handle uniformly.
30//
31// Not yet (staged for follow-ups):
32// * Binary encoding option (ETW-style schema-typed events)
33// * Ring buffer capture + post-mortem dump
34// * Span / scope tracking (entry/exit markers with id)
35// * Field name = value pairs (structured attributes)
36// * Remote sink via socket / shared ring
37//
38// These ship in nx_log_span.nx + nx_log_sink.nx when needed.
39
40// nx_safety_envelope:
41// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
42// sil_target: SIL1
43// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
44// verdict: NOT_YET_EVALUATED
45
46import "syscalls.nx"
47const NX_MAGIC_10000000: i64 = 10000000
48
49// --- levels ---------------------------------------------------------
50
51const NX_LOG_TRACE: i64 = 0 // ultra-verbose, per-instruction tracing
52const NX_LOG_DEBUG: i64 = 1 // diagnostic, devs-only
53const NX_LOG_INFO: i64 = 2 // normal operational events
54const NX_LOG_WARN: i64 = 3 // unexpected but recoverable
55const NX_LOG_ERROR: i64 = 4 // failure, caller should handle
56const NX_LOG_FATAL: i64 = 5 // panic + exit
57
58// Fatal exit code -- matches nx_assert's NX_ASSERT_EXIT.
59const NX_LOG_EXIT: i64 = 200
60
61// --- context --------------------------------------------------------
62//
63// Every process has a default global context. Subsystems may
64// allocate their own context with different filters, sinks, etc.
65// Pattern: start every module with
66//
67// let log: *NxLogContext = nx_log_global()
68//
69// then call log methods. The global can be retuned at runtime by
70// writing to the struct (min_level, out_fd). For tests that want
71// deterministic sequence numbers, allocate a fresh context with
72// nx_log_new(NX_LOG_TRACE, fd).
73
74struct NxLogContext {
75 min_level: i64, // emit events with level >= this
76 out_fd: i64, // write target (default 2 = stderr)
77 seq: i64, // monotonic sequence counter
78 drop_cnt: i64, // events below min_level (visibility metric)
79}
80
81const NX_LOG_CTX_BYTES: i64 = 32
82
83// Forward decls -- keep implementations below in a readable order
84// (helpers first, core emit, convenience wrappers, self-test last).
85func nx_log_puts(fd: i64, s: *u8) -> i64;
86func nx_log_puti(fd: i64, v: i64) -> i64;
87func nx_log_level_name(lvl: i64) -> *u8;
88func ipow10(n: i64) -> i64;
89func nx_log_new(min_level: i64, out_fd: i64) -> *NxLogContext;
90
91// Global singleton, lazy-init on first call to nx_log_global().
92// Stored as a pointer slot (zero-initialised by BSS). NishiLang
93// statics don't yet take initializers, so we rely on the implicit
94// zero to detect the uninitialised state in nx_log_global().
95
96static NX_LOG_GLOBAL_PTR: *i64
97
98func nx_log_new(min_level: i64, out_fd: i64) -> *NxLogContext {
99 let raw: *u8 = sys_mmap(NX_LOG_CTX_BYTES)
100 let c: *NxLogContext = raw as *NxLogContext
101 c.min_level = min_level
102 c.out_fd = out_fd
103 c.seq = 0
104 c.drop_cnt = 0
105 return c
106}
107
108// Return the process-global logger; lazy-init on first call.
109// Default: INFO level, stderr. Tests / tools can retune via
110// direct struct write.
111func nx_log_global() -> *NxLogContext {
112 if NX_LOG_GLOBAL_PTR == (0 as *i64) {
113 let slot_raw: *u8 = sys_mmap(8)
114 let slot: *i64 = slot_raw as *i64
115 let ctx: *NxLogContext = nx_log_new(NX_LOG_INFO, 2)
116 *slot = ctx as i64
117 NX_LOG_GLOBAL_PTR = slot
118 }
119 let c: *NxLogContext = (*NX_LOG_GLOBAL_PTR) as *NxLogContext
120 return c
121}
122
123// --- write helpers --------------------------------------------------
124//
125// Minimal string helpers mirrored from nx_assert.nx. Duplicated so
126// nx_log.nx has zero cycle with nx_assert.nx (nx_assert may log on
127// failure; nx_log must not assert on failure).
128
129func nx_log_puts(fd: i64, s: *u8) -> i64 {
130 var n: i64 = 0
131 while s[n] != 0 { n = n + 1 }
132 sys_write(fd, s, n)
133 return 0
134}
135
136func nx_log_puti(fd: i64, v: i64) -> i64 {
137 let buf: *u8 = sys_mmap(32)
138 var n: i64 = v
139 var i: i64 = 0
140 var neg: i64 = 0
141 if n < 0 { neg = 1; n = 0 - n }
142 if n == 0 { buf[0] = 0x30; i = 1 }
143 while n > 0 {
144 buf[i] = 0x30 + (n - (n / 10) * 10)
145 n = n / 10
146 i = i + 1
147 }
148 if neg == 1 { buf[i] = 0x2D; i = i + 1 }
149 var j: i64 = 0
150 var k: i64 = i - 1
151 while j < k {
152 let tmp: i64 = buf[j]
153 buf[j] = buf[k]
154 buf[k] = tmp
155 j = j + 1
156 k = k - 1
157 }
158 sys_write(fd, buf, i)
159 return 0
160}
161
162// --- level -> short name --------------------------------------------
163
164func nx_log_level_name(lvl: i64) -> *u8 {
165 if lvl == NX_LOG_TRACE { return "TRACE" as *u8 }
166 if lvl == NX_LOG_DEBUG { return "DEBUG" as *u8 }
167 if lvl == NX_LOG_INFO { return "INFO " as *u8 }
168 if lvl == NX_LOG_WARN { return "WARN " as *u8 }
169 if lvl == NX_LOG_ERROR { return "ERROR" as *u8 }
170 if lvl == NX_LOG_FATAL { return "FATAL" as *u8 }
171 return "?????" as *u8
172}
173
174// --- core emit ------------------------------------------------------
175//
176// Format: '<seq> <LEVEL> <tag> <msg>\n'
177// Example: '00000123 INFO parse.sys_read_file entered'
178//
179// Sequence zero-padded to 8 digits so sort / diff produce stable
180// output across runs of similar length.
181
182func nx_log_emit(ctx: *NxLogContext, level: i64,
183 tag: *u8, msg: *u8) -> i64 {
184 if level < ctx.min_level {
185 ctx.drop_cnt = ctx.drop_cnt + 1
186 return 0
187 }
188 let fd: i64 = ctx.out_fd
189 let seq: i64 = ctx.seq
190 ctx.seq = seq + 1
191
192 // 8-digit zero-padded seq -- cheap + fixed width for grep/sort.
193 let sbuf: *u8 = sys_mmap(16)
194 var i: i64 = 0
195 while i < 8 {
196 let digit: i64 = (seq / ipow10(7 - i)) - (seq / ipow10(8 - i)) * 10
197 sbuf[i] = 0x30 + digit
198 i = i + 1
199 }
200 sbuf[8] = 0x20
201 sys_write(fd, sbuf, 9)
202
203 nx_log_puts(fd, nx_log_level_name(level))
204 sys_write(fd, " " as *u8, 1)
205 nx_log_puts(fd, tag)
206 sys_write(fd, " " as *u8, 1)
207 nx_log_puts(fd, msg)
208 sys_write(fd, "\n" as *u8, 1)
209 return 0
210}
211
212// Integer power of 10. Bounded to 18 to stay inside i64 range.
213func ipow10(n: i64) -> i64 {
214 var r: i64 = 1
215 var i: i64 = 0
216 while i < n {
217 r = r * 10
218 i = i + 1
219 }
220 return r
221}
222
223// --- per-level convenience wrappers ---------------------------------
224//
225// Standard pattern: pick the matching level function, pass tag+msg.
226// Fatal also exits with NX_LOG_EXIT so the process dies cleanly.
227
228func nx_log_trace(tag: *u8, msg: *u8) -> i64 {
229 nx_log_emit(nx_log_global(), NX_LOG_TRACE, tag, msg)
230 return 0
231}
232
233func nx_log_debug(tag: *u8, msg: *u8) -> i64 {
234 nx_log_emit(nx_log_global(), NX_LOG_DEBUG, tag, msg)
235 return 0
236}
237
238func nx_log_info(tag: *u8, msg: *u8) -> i64 {
239 nx_log_emit(nx_log_global(), NX_LOG_INFO, tag, msg)
240 return 0
241}
242
243func nx_log_warn(tag: *u8, msg: *u8) -> i64 {
244 nx_log_emit(nx_log_global(), NX_LOG_WARN, tag, msg)
245 return 0
246}
247
248func nx_log_error(tag: *u8, msg: *u8) -> i64 {
249 nx_log_emit(nx_log_global(), NX_LOG_ERROR, tag, msg)
250 return 0
251}
252
253func nx_log_fatal(tag: *u8, msg: *u8) -> i64 {
254 nx_log_emit(nx_log_global(), NX_LOG_FATAL, tag, msg)
255 __syscall(93, NX_LOG_EXIT, 0, 0, 0, 0, 0)
256 return 0 // unreachable
257}
258
259// --- structured append helpers --------------------------------------
260//
261// For log lines that need values inline with a message, these emit
262// additional tokens AFTER nx_log_emit's tag+msg, space-separated.
263// Intentionally separate API so the log line structure stays
264// predictable (message is always before values).
265
266func nx_log_append_i(ctx: *NxLogContext, v: i64) -> i64 {
267 sys_write(ctx.out_fd, " " as *u8, 1)
268 nx_log_puti(ctx.out_fd, v)
269 return 0
270}
271
272func nx_log_append_s(ctx: *NxLogContext, s: *u8) -> i64 {
273 sys_write(ctx.out_fd, " " as *u8, 1)
274 nx_log_puts(ctx.out_fd, s)
275 return 0
276}
277
278// --- self-test ------------------------------------------------------
279
280func main() -> i64 {
281 // Use a fresh context routed to stderr for visibility + an
282 // in-test toggle to verify level gating.
283 let ctx: *NxLogContext = nx_log_new(NX_LOG_DEBUG, 2)
284
285 // INFO + WARN + ERROR should fire; TRACE < DEBUG should drop.
286 nx_log_emit(ctx, NX_LOG_TRACE, "test" as *u8, "should drop" as *u8)
287 if ctx.drop_cnt != 1 {
288 return __syscall(93, 10, 0, 0, 0, 0, 0)
289 }
290 nx_log_emit(ctx, NX_LOG_DEBUG, "test" as *u8, "should fire (DEBUG)" as *u8)
291 nx_log_emit(ctx, NX_LOG_INFO, "test" as *u8, "should fire (INFO)" as *u8)
292 nx_log_emit(ctx, NX_LOG_WARN, "test" as *u8, "should fire (WARN)" as *u8)
293 nx_log_emit(ctx, NX_LOG_ERROR, "test" as *u8, "should fire (ERROR)" as *u8)
294 if ctx.seq != 4 {
295 return __syscall(93, 11, 0, 0, 0, 0, 0)
296 }
297
298 // Level name table round-trip. Must assign to var first --
299 // NishiLang parser doesn't accept subscript on a call-expr.
300 let n_trace: *u8 = nx_log_level_name(NX_LOG_TRACE)
301 if n_trace[0] != 0x54 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
302 let n_info: *u8 = nx_log_level_name(NX_LOG_INFO)
303 if n_info[0] != 0x49 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
304 let n_fatal: *u8 = nx_log_level_name(NX_LOG_FATAL)
305 if n_fatal[0] != 0x46 { return __syscall(93, 22, 0, 0, 0, 0, 0) }
306 let n_unk: *u8 = nx_log_level_name(99)
307 if n_unk[0] != 0x3F { return __syscall(93, 23, 0, 0, 0, 0, 0) }
308
309 // ipow10 sanity
310 if ipow10(0) != 1 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
311 if ipow10(1) != 10 { return __syscall(93, 31, 0, 0, 0, 0, 0) }
312 if ipow10(7) != NX_MAGIC_10000000 { return __syscall(93, 32, 0, 0, 0, 0, 0) }
313
314 // Append helpers -- log a structured value line.
315 nx_log_emit(ctx, NX_LOG_INFO, "count" as *u8, "items=" as *u8)
316 nx_log_append_i(ctx, 42)
317 sys_write(ctx.out_fd, "\n" as *u8, 1)
318
319 return 0
320}