nx_log_jsonl.nx source
↩ module page · 361 lines · 13710 B
1// nx_log_jsonl.nx -- structured JSONL log line emitter.
2//
3// One line per log entry, JSON-encoded. Consumable by any log
4// aggregator: Loki / Splunk / Elasticsearch / Datadog / `jq`.
5// Standard S-class hosting log shape.
6//
7// Per cardinal feedback-no-third-party-trust-native-or-nothing:
8// substrate's own log emitter; no logrus, no log4j, no syslog
9// SDK. Substrate produces JSON; any consumer reads.
10//
11// Per cardinal feedback-defensive-at-boundaries-trusting-internally:
12// log values are escaped for JSON safety (string escape per RFC 8259
13// §7) so log-injection attacks via newlines or quotes in user input
14// are structurally prevented.
15//
16// One log line shape:
17//
18// {"ts_ms":12345,"level":"INFO","module":"nx_audit","msg":"hello"}\n
19//
20// Caller can add k=v fields via nx_log_field_str / _int after
21// opening the entry + before closing. Fields are encoded inline:
22//
23// {"ts_ms":1,"level":"WARN","module":"x","msg":"y","retry":3}\n
24//
25// nx_capability_claims:
26// needs: [sealed_enum, bounded_buffer, json_string_escape]
27// provides: [structured_log_emit, log_injection_prevention]
28// safety: [no_unchecked_deref, no_floating_point, no_syscall,
29// bounded_iteration, json_value_escape_at_boundary]
30// verdict: [sealed_enum_4_state_severity + sealed_enum_4_verdict]
31// license: ORIGINAL
32// kind: racing_crew_specialist
33// layer: L3 (algorithm: structured emit)
34
35// ---- Sealed enum: log severity -----------------------------------
36
37const NXL_DEBUG: i64 = 0
38const NXL_INFO: i64 = 1
39const NXL_WARN: i64 = 2
40const NXL_ERROR: i64 = 3
41const NXL_LEVEL_N: i64 = 4
42
43func nxl_level_is_valid(L: i64) -> i64 {
44 if L < 0 { return 0 }
45 if L >= NXL_LEVEL_N { return 0 }
46 return 1
47}
48
49func nxl_level_name(L: i64) -> *u8 {
50 if L == NXL_DEBUG { return "DEBUG" as *u8 }
51 if L == NXL_INFO { return "INFO" as *u8 }
52 if L == NXL_WARN { return "WARN" as *u8 }
53 if L == NXL_ERROR { return "ERROR" as *u8 }
54 return "INVALID" as *u8
55}
56
57func nxl_level_name_len(L: i64) -> i64 {
58 if L == NXL_DEBUG { return 5 }
59 if L == NXL_INFO { return 4 }
60 if L == NXL_WARN { return 4 }
61 if L == NXL_ERROR { return 5 }
62 return 7
63}
64
65// ---- Sealed enum: emit verdict -----------------------------------
66
67const NXL_OK: i64 = 0
68const NXL_OOM_BUFFER: i64 = 1
69const NXL_BAD_LEVEL: i64 = 2
70const NXL_BAD_ARG: i64 = 3
71const NXL_VERDICT_N: i64 = 4
72
73func nxl_verdict_is_valid(v: i64) -> i64 {
74 if v < 0 { return 0 }
75 if v >= NXL_VERDICT_N { return 0 }
76 return 1
77}
78
79func nxl_verdict_name(v: i64) -> *u8 {
80 if v == NXL_OK { return "OK" as *u8 }
81 if v == NXL_OOM_BUFFER { return "OOM_BUFFER" as *u8 }
82 if v == NXL_BAD_LEVEL { return "BAD_LEVEL" as *u8 }
83 if v == NXL_BAD_ARG { return "BAD_ARG" as *u8 }
84 return "INVALID" as *u8
85}
86
87// ---- Byte emit helpers -------------------------------------------
88
89func nxl_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 {
90 if *off >= cap { return NXL_OOM_BUFFER }
91 out[*off] = b as u8
92 *off = *off + 1
93 return NXL_OK
94}
95
96func nxl_put_cstr(out: *u8, off: *i64, cap: i64, s: *u8) -> i64 {
97 var i: i64 = 0
98 while s[i] != 0 {
99 let rc: i64 = nxl_put(out, off, cap, s[i] as i64)
100 if rc != NXL_OK { return rc }
101 i = i + 1
102 }
103 return NXL_OK
104}
105
106func nxl_put_bytes(out: *u8, off: *i64, cap: i64, src: *u8, n: i64) -> i64 {
107 var i: i64 = 0
108 while i < n {
109 let rc: i64 = nxl_put(out, off, cap, src[i] as i64)
110 if rc != NXL_OK { return rc }
111 i = i + 1
112 }
113 return NXL_OK
114}
115
116// ---- JSON string escape (RFC 8259 §7) ----------------------------
117//
118// Required escapes:
119// " -> \"
120// \ -> \\
121// / -> / (optional but tolerated; spec allows both)
122// <0x20 control chars -> \uXXXX (we use the short forms where
123// defined: \b \t \n \r \f)
124//
125// We emit \uXXXX for any other 0x00..0x1f byte. High-bit bytes
126// pass through verbatim (assumed UTF-8; substrate doesn't decode).
127
128func nxl_put_json_str_bytes(out: *u8, off: *i64, cap: i64,
129 src: *u8, n: i64) -> i64 {
130 var i: i64 = 0
131 while i < n {
132 let b: i64 = src[i] as i64
133 var rc: i64 = NXL_OK
134 if b == 0x22 {
135 rc = nxl_put_cstr(out, off, cap, "\\\"" as *u8)
136 } else {
137 if b == 0x5c {
138 rc = nxl_put_cstr(out, off, cap, "\\\\" as *u8)
139 } else {
140 if b == 0x08 {
141 rc = nxl_put_cstr(out, off, cap, "\\b" as *u8)
142 } else {
143 if b == 0x09 {
144 rc = nxl_put_cstr(out, off, cap, "\\t" as *u8)
145 } else {
146 if b == 0x0a {
147 rc = nxl_put_cstr(out, off, cap, "\\n" as *u8)
148 } else {
149 if b == 0x0c {
150 rc = nxl_put_cstr(out, off, cap, "\\f" as *u8)
151 } else {
152 if b == 0x0d {
153 rc = nxl_put_cstr(out, off, cap, "\\r" as *u8)
154 } else {
155 if b >= 0 && b < 0x20 {
156 // \u00XX
157 rc = nxl_put_cstr(out, off, cap, "\\u00" as *u8)
158 if rc != NXL_OK { return rc }
159 let hi: i64 = (b >> 4) & 0xf
160 let lo: i64 = b & 0xf
161 var hex_hi: i64 = 0x30 + hi
162 if hi >= 10 { hex_hi = 0x61 + (hi - 10) }
163 var hex_lo: i64 = 0x30 + lo
164 if lo >= 10 { hex_lo = 0x61 + (lo - 10) }
165 let rh: i64 = nxl_put(out, off, cap, hex_hi)
166 if rh != NXL_OK { return rh }
167 rc = nxl_put(out, off, cap, hex_lo)
168 } else {
169 rc = nxl_put(out, off, cap, b)
170 }
171 }
172 }
173 }
174 }
175 }
176 }
177 }
178 if rc != NXL_OK { return rc }
179 i = i + 1
180 }
181 return NXL_OK
182}
183
184// i64 -> ASCII decimal using caller-supplied scratch.
185func nxl_put_dec(out: *u8, off: *i64, cap: i64,
186 scratch: *u8, v: i64) -> i64 {
187 if v == 0 { return nxl_put(out, off, cap, 0x30) }
188 var n: i64 = v
189 var sign: i64 = 0
190 if n < 0 { sign = 1; n = 0 - n }
191 var k: i64 = 0
192 while n > 0 {
193 scratch[k] = (0x30 + (n - (n / 10) * 10)) as u8
194 n = n / 10
195 k = k + 1
196 }
197 if sign == 1 {
198 let rcs: i64 = nxl_put(out, off, cap, 0x2d)
199 if rcs != NXL_OK { return rcs }
200 }
201 var ri: i64 = k - 1
202 while ri >= 0 {
203 let rc: i64 = nxl_put(out, off, cap, scratch[ri] as i64)
204 if rc != NXL_OK { return rc }
205 ri = ri - 1
206 }
207 return NXL_OK
208}
209
210// ---- Entry: emit a complete log line with no extra fields -------
211//
212// Output:
213// {"ts_ms":1234,"level":"INFO","module":"nx_audit","msg":"started"}\n
214
215func nx_log_emit_line(
216 out: *u8, off: *i64, cap: i64,
217 ts_ms: i64,
218 level: i64,
219 module: *u8, module_n: i64,
220 msg: *u8, msg_n: i64,
221 scratch: *u8) -> i64 {
222 if out == (0 as *u8) { return NXL_BAD_ARG }
223 if off == (0 as *i64) { return NXL_BAD_ARG }
224 if module == (0 as *u8) { return NXL_BAD_ARG }
225 if msg == (0 as *u8) { return NXL_BAD_ARG }
226 if scratch == (0 as *u8) { return NXL_BAD_ARG }
227 if cap <= 0 { return NXL_BAD_ARG }
228 if module_n <= 0 { return NXL_BAD_ARG }
229 if msg_n < 0 { return NXL_BAD_ARG }
230 if nxl_level_is_valid(level) != 1 { return NXL_BAD_LEVEL }
231
232 let r1: i64 = nxl_put_cstr(out, off, cap, "{\"ts_ms\":" as *u8)
233 if r1 != NXL_OK { return r1 }
234 let r2: i64 = nxl_put_dec(out, off, cap, scratch, ts_ms)
235 if r2 != NXL_OK { return r2 }
236 let r3: i64 = nxl_put_cstr(out, off, cap, ",\"level\":\"" as *u8)
237 if r3 != NXL_OK { return r3 }
238 let r4: i64 = nxl_put_bytes(out, off, cap,
239 nxl_level_name(level),
240 nxl_level_name_len(level))
241 if r4 != NXL_OK { return r4 }
242 let r5: i64 = nxl_put_cstr(out, off, cap, "\",\"module\":\"" as *u8)
243 if r5 != NXL_OK { return r5 }
244 let r6: i64 = nxl_put_json_str_bytes(out, off, cap, module, module_n)
245 if r6 != NXL_OK { return r6 }
246 let r7: i64 = nxl_put_cstr(out, off, cap, "\",\"msg\":\"" as *u8)
247 if r7 != NXL_OK { return r7 }
248 let r8: i64 = nxl_put_json_str_bytes(out, off, cap, msg, msg_n)
249 if r8 != NXL_OK { return r8 }
250 return nxl_put_cstr(out, off, cap, "\"}\n" as *u8)
251}
252
253// ---- Entry: emit a log line with a single string-valued extra field ----
254//
255// Output:
256// {"ts_ms":1234,"level":"INFO","module":"x","msg":"y","<field>":"<val>"}\n
257
258func nx_log_emit_line_with_str(
259 out: *u8, off: *i64, cap: i64,
260 ts_ms: i64, level: i64,
261 module: *u8, module_n: i64,
262 msg: *u8, msg_n: i64,
263 field: *u8, field_n: i64,
264 val: *u8, val_n: i64,
265 scratch: *u8) -> i64 {
266 if field == (0 as *u8) { return NXL_BAD_ARG }
267 if field_n <= 0 { return NXL_BAD_ARG }
268 if val == (0 as *u8) { return NXL_BAD_ARG }
269 if val_n < 0 { return NXL_BAD_ARG }
270 if out == (0 as *u8) { return NXL_BAD_ARG }
271 if off == (0 as *i64) { return NXL_BAD_ARG }
272 if module == (0 as *u8) { return NXL_BAD_ARG }
273 if msg == (0 as *u8) { return NXL_BAD_ARG }
274 if scratch == (0 as *u8) { return NXL_BAD_ARG }
275 if cap <= 0 { return NXL_BAD_ARG }
276 if module_n <= 0 { return NXL_BAD_ARG }
277 if msg_n < 0 { return NXL_BAD_ARG }
278 if nxl_level_is_valid(level) != 1 { return NXL_BAD_LEVEL }
279
280 let r1: i64 = nxl_put_cstr(out, off, cap, "{\"ts_ms\":" as *u8)
281 if r1 != NXL_OK { return r1 }
282 let r2: i64 = nxl_put_dec(out, off, cap, scratch, ts_ms)
283 if r2 != NXL_OK { return r2 }
284 let r3: i64 = nxl_put_cstr(out, off, cap, ",\"level\":\"" as *u8)
285 if r3 != NXL_OK { return r3 }
286 let r4: i64 = nxl_put_bytes(out, off, cap,
287 nxl_level_name(level),
288 nxl_level_name_len(level))
289 if r4 != NXL_OK { return r4 }
290 let r5: i64 = nxl_put_cstr(out, off, cap, "\",\"module\":\"" as *u8)
291 if r5 != NXL_OK { return r5 }
292 let r6: i64 = nxl_put_json_str_bytes(out, off, cap, module, module_n)
293 if r6 != NXL_OK { return r6 }
294 let r7: i64 = nxl_put_cstr(out, off, cap, "\",\"msg\":\"" as *u8)
295 if r7 != NXL_OK { return r7 }
296 let r8: i64 = nxl_put_json_str_bytes(out, off, cap, msg, msg_n)
297 if r8 != NXL_OK { return r8 }
298 let r9: i64 = nxl_put_cstr(out, off, cap, "\",\"" as *u8)
299 if r9 != NXL_OK { return r9 }
300 let r10: i64 = nxl_put_json_str_bytes(out, off, cap, field, field_n)
301 if r10 != NXL_OK { return r10 }
302 let r11: i64 = nxl_put_cstr(out, off, cap, "\":\"" as *u8)
303 if r11 != NXL_OK { return r11 }
304 let r12: i64 = nxl_put_json_str_bytes(out, off, cap, val, val_n)
305 if r12 != NXL_OK { return r12 }
306 return nxl_put_cstr(out, off, cap, "\"}\n" as *u8)
307}
308
309// ---- Entry: emit a log line with a single int-valued extra field ----
310//
311// Output:
312// {"ts_ms":1234,"level":"WARN","module":"x","msg":"y","<field>":<int>}\n
313
314func nx_log_emit_line_with_int(
315 out: *u8, off: *i64, cap: i64,
316 ts_ms: i64, level: i64,
317 module: *u8, module_n: i64,
318 msg: *u8, msg_n: i64,
319 field: *u8, field_n: i64,
320 val: i64,
321 scratch: *u8) -> i64 {
322 if field == (0 as *u8) { return NXL_BAD_ARG }
323 if field_n <= 0 { return NXL_BAD_ARG }
324 if out == (0 as *u8) { return NXL_BAD_ARG }
325 if off == (0 as *i64) { return NXL_BAD_ARG }
326 if module == (0 as *u8) { return NXL_BAD_ARG }
327 if msg == (0 as *u8) { return NXL_BAD_ARG }
328 if scratch == (0 as *u8) { return NXL_BAD_ARG }
329 if cap <= 0 { return NXL_BAD_ARG }
330 if module_n <= 0 { return NXL_BAD_ARG }
331 if msg_n < 0 { return NXL_BAD_ARG }
332 if nxl_level_is_valid(level) != 1 { return NXL_BAD_LEVEL }
333
334 let r1: i64 = nxl_put_cstr(out, off, cap, "{\"ts_ms\":" as *u8)
335 if r1 != NXL_OK { return r1 }
336 let r2: i64 = nxl_put_dec(out, off, cap, scratch, ts_ms)
337 if r2 != NXL_OK { return r2 }
338 let r3: i64 = nxl_put_cstr(out, off, cap, ",\"level\":\"" as *u8)
339 if r3 != NXL_OK { return r3 }
340 let r4: i64 = nxl_put_bytes(out, off, cap,
341 nxl_level_name(level),
342 nxl_level_name_len(level))
343 if r4 != NXL_OK { return r4 }
344 let r5: i64 = nxl_put_cstr(out, off, cap, "\",\"module\":\"" as *u8)
345 if r5 != NXL_OK { return r5 }
346 let r6: i64 = nxl_put_json_str_bytes(out, off, cap, module, module_n)
347 if r6 != NXL_OK { return r6 }
348 let r7: i64 = nxl_put_cstr(out, off, cap, "\",\"msg\":\"" as *u8)
349 if r7 != NXL_OK { return r7 }
350 let r8: i64 = nxl_put_json_str_bytes(out, off, cap, msg, msg_n)
351 if r8 != NXL_OK { return r8 }
352 let r9: i64 = nxl_put_cstr(out, off, cap, "\",\"" as *u8)
353 if r9 != NXL_OK { return r9 }
354 let r10: i64 = nxl_put_json_str_bytes(out, off, cap, field, field_n)
355 if r10 != NXL_OK { return r10 }
356 let r11: i64 = nxl_put_cstr(out, off, cap, "\":" as *u8)
357 if r11 != NXL_OK { return r11 }
358 let r12: i64 = nxl_put_dec(out, off, cap, scratch, val)
359 if r12 != NXL_OK { return r12 }
360 return nxl_put_cstr(out, off, cap, "}\n" as *u8)
361}