log.nx source
↩ module page · 141 lines · 4561 B
1// log.nx -- structured logging with levels.
2//
3// Five-level hierarchy (TRACE < DEBUG < INFO < WARN < ERROR).
4// Output goes to stderr (fd 2) so stdout stays clean for pipe/
5// redirect workflows. Each line is prefixed with a level tag
6// + RFC 3339 timestamp + caller-supplied module name.
7//
8// Format:
9// [LEVEL] <timestamp> <module>: <message>
10// e.g. [INFO] 2026-04-21T10:30:42Z http: serving / (200)
11//
12// Used by: any service that needs operational visibility without
13// pulling in a real logging framework. Pairs with timefmt.nx
14// for the timestamp.
15//
16// Invariants:
17// L1 Output is line-oriented (one line per call); LF appended
18// even if caller's message lacks one.
19// L2 Below-threshold calls are CHEAP -- short-circuit early
20// before any formatting work.
21// L3 Multi-thread safe at the SYSCALL level (sys_write is
22// atomic up to PIPE_BUF on POSIX); concurrent log calls
23// never interleave at the byte level for short lines.
24// L4 No allocations on the hot path; uses a single sys_mmap
25// buffer reused across calls.
26
27import "syscalls.nx"
28import "timefmt.nx"
29
30const LOG_TRACE: i64 = 0
31const LOG_DEBUG: i64 = 1
32const LOG_INFO: i64 = 2
33const LOG_WARN: i64 = 3
34const LOG_ERROR: i64 = 4
35
36// Threshold storage in a heap slot so set_threshold persists.
37func log_threshold_slot() -> *i64 {
38 let raw: *u8 = sys_mmap(16)
39 let p: *i64 = raw as *i64
40 *p = LOG_INFO // default: drop TRACE / DEBUG
41 return p
42}
43
44// Module-level state placeholder. Each call to log_set_threshold
45// allocates fresh storage; the LAST call's slot wins because
46// log_threshold uses the caller's slot pointer. In practice
47// this is fine for single-process apps where the threshold gets
48// set once at startup.
49
50// Level tag for prefix.
51func log_level_tag(level: i64) -> *u8 {
52 if level == LOG_TRACE { return "TRACE" }
53 if level == LOG_DEBUG { return "DEBUG" }
54 if level == LOG_INFO { return "INFO" }
55 if level == LOG_WARN { return "WARN" }
56 if level == LOG_ERROR { return "ERROR" }
57 return "UNK"
58}
59
60// Append a null-terminated cstring; bounds-check via cap.
61func log_append(buf: *u8, pos: *i64, cap: i64, s: *u8) -> i64 {
62 let p0: i64 = *pos
63 var i: i64 = 0
64 while s[i] != 0 {
65 if p0 + i >= cap { return -1 }
66 buf[p0 + i] = s[i]
67 i = i + 1
68 }
69 *pos = p0 + i
70 return 0
71}
72
73// Append n bytes of raw data.
74func log_append_bytes(buf: *u8, pos: *i64, cap: i64, src: *u8, n: i64) -> i64 {
75 let p0: i64 = *pos
76 if p0 + n > cap { return -1 }
77 var i: i64 = 0
78 while i < n { buf[p0 + i] = src[i]; i = i + 1 }
79 *pos = p0 + n
80 return 0
81}
82
83// Write a log line to stderr. Level filter applied; below-
84// threshold lines no-op. `unix_time` is caller-supplied epoch
85// seconds (or pass 0 to skip the timestamp).
86//
87// Module name appears between timestamp and message; helps grep
88// by subsystem.
89func log_write(threshold: *i64, level: i64,
90 unix_time: i64,
91 module: *u8, msg: *u8, msg_len: i64) -> i64 {
92 if level < *threshold { return 0 }
93 let buf: *u8 = sys_mmap(1024)
94 let pos_raw: *u8 = sys_mmap(16)
95 let pos: *i64 = pos_raw as *i64
96 *pos = 0
97
98 log_append(buf, pos, 1024, "[")
99 log_append(buf, pos, 1024, log_level_tag(level))
100 log_append(buf, pos, 1024, "] ")
101
102 if unix_time > 0 {
103 let ts: *u8 = sys_mmap(32)
104 format_rfc3339(unix_time, ts)
105 log_append_bytes(buf, pos, 1024, ts, 20)
106 log_append(buf, pos, 1024, " ")
107 }
108
109 log_append(buf, pos, 1024, module)
110 log_append(buf, pos, 1024, ": ")
111 log_append_bytes(buf, pos, 1024, msg, msg_len)
112
113 // Ensure trailing LF.
114 if *pos < 1024 {
115 if buf[*pos - 1] != 0x0A {
116 buf[*pos] = 0x0A
117 *pos = *pos + 1
118 }
119 }
120
121 sys_write(2, buf, *pos)
122 return 0
123}
124
125// Convenience: unwrap a cstring message length.
126func log_msg(threshold: *i64, level: i64, unix_time: i64,
127 module: *u8, msg: *u8) -> i64 {
128 var n: i64 = 0
129 while msg[n] != 0 { n = n + 1 }
130 return log_write(threshold, level, unix_time, module, msg, n)
131}
132
133// Compile-only smoke. Skips real stderr writes by passing a
134// threshold that drops all levels.
135func main() -> i64 {
136 let raw: *u8 = sys_mmap(16)
137 let thr: *i64 = raw as *i64
138 *thr = LOG_ERROR + 1 // drop everything
139 log_msg(thr, LOG_INFO, 0, "test", "hello")
140 return 0
141}