obs.nx source
↩ module page · 133 lines · 4453 B
1// obs.nx -- observability primitives (spans, structured logs).
2//
3// EFFICIENCY_ROADMAP ยง5.2. OpenTelemetry-style distributed
4// tracing + structured logging. Today's observability is
5// ad-hoc sys_write; this module lands a proper API with
6// trace IDs, parent-child span relationships, + KV attributes.
7//
8// Phase A: runtime spans + log records serialised to stdout as
9// JSON (or future Protobuf via protobuf_wire.nx). Phase B:
10// `@trace` function attribute auto-instruments every call.
11//
12// Designed to match OpenTelemetry data model so future bridges
13// to Jaeger / Honeycomb / Datadog are clean byte-copies.
14//
15// Invariants:
16// OB1 Span IDs are 128-bit; collision probability at one
17// million spans/sec for a year < 1 in 10^30.
18// OB2 Parent-child relationships form a tree per trace_id
19// (enforced by caller; runtime doesn't verify).
20// OB3 Log records are structured: all fields get key+value;
21// no printf-style interpolation (bad for aggregation).
22
23import "syscalls.nx"
24import "rand.nx"
25
26// Severity levels (matches syslog + tracing standards).
27const LOG_TRACE: i64 = 0
28const LOG_DEBUG: i64 = 1
29const LOG_INFO: i64 = 2
30const LOG_WARN: i64 = 3
31const LOG_ERROR: i64 = 4
32const LOG_FATAL: i64 = 5
33
34struct Span {
35 // 128-bit IDs stored as pairs of i64 for simplicity.
36 trace_id_hi: i64, trace_id_lo: i64,
37 span_id: i64,
38 parent_id: i64,
39 start_ns: i64, // caller-supplied clock reading
40 end_ns: i64, // 0 until closed
41 status: i64, // 0 ok, nonzero = error code
42 name_off: *u8,
43 name_len: i64,
44}
45
46func span_new(name: *u8, name_len: i64,
47 trace_hi: i64, trace_lo: i64,
48 parent_id: i64, start_ns: i64) -> *Span {
49 let raw: *u8 = sys_mmap(96)
50 let s: *Span = raw as *Span
51 s.trace_id_hi = trace_hi
52 s.trace_id_lo = trace_lo
53
54 // span_id = 64-bit random. rand_bytes gives crypto entropy.
55 let buf: *u8 = sys_mmap(16)
56 rand_bytes(buf, 8)
57 var sid: i64 = 0
58 var i: i64 = 0
59 while i < 8 {
60 sid = (sid << 8) | buf[i]
61 i = i + 1
62 }
63 s.span_id = sid & 0x7FFFFFFFFFFFFFFF
64
65 s.parent_id = parent_id
66 s.start_ns = start_ns
67 s.end_ns = 0
68 s.status = 0
69 s.name_off = name
70 s.name_len = name_len
71 return s
72}
73
74// Fresh root span -- generates new trace_id + parent=0.
75func span_new_root(name: *u8, name_len: i64, start_ns: i64) -> *Span {
76 let buf: *u8 = sys_mmap(32)
77 rand_bytes(buf, 16)
78 var hi: i64 = 0; var lo: i64 = 0
79 var i: i64 = 0
80 while i < 8 { hi = (hi << 8) | buf[i]; i = i + 1 }
81 while i < 16 { lo = (lo << 8) | buf[i]; i = i + 1 }
82 return span_new(name, name_len, hi & 0x7FFFFFFFFFFFFFFF,
83 lo & 0x7FFFFFFFFFFFFFFF, 0, start_ns)
84}
85
86// Close a span by recording end time + status.
87func span_close(s: *Span, end_ns: i64, status: i64) -> i64 {
88 s.end_ns = end_ns
89 s.status = status
90 return 0
91}
92
93func span_duration_ns(s: *Span) -> i64 {
94 return s.end_ns - s.start_ns
95}
96
97// === structured log records ==========================================
98
99// Write a log record to fd. Phase A uses a compact tab-delim
100// format; Phase B emits proper JSON / Protobuf.
101func log_write(fd: i64, level: i64,
102 msg: *u8, msg_len: i64) -> i64 {
103 // Format: "L<level>\t<msg>\n"
104 let buf: *u8 = sys_mmap(msg_len + 16)
105 buf[0] = 0x4C // 'L'
106 buf[1] = 0x30 + level // digit
107 buf[2] = 0x09 // tab
108 var i: i64 = 0
109 while i < msg_len { buf[3 + i] = msg[i]; i = i + 1 }
110 buf[3 + msg_len] = 0x0A // newline
111 sys_write(fd, buf, msg_len + 4)
112 return 0
113}
114
115// Compile-only smoke.
116func main() -> i64 {
117 let root: *Span = span_new_root("test", 4, 1000000000)
118 if root.parent_id != 0 { return 1 }
119 if root.start_ns != 1000000000 { return 2 }
120
121 let child: *Span = span_new("child", 5,
122 root.trace_id_hi, root.trace_id_lo,
123 root.span_id, 1000100000)
124 if child.parent_id != root.span_id { return 3 }
125 if child.trace_id_hi != root.trace_id_hi { return 4 }
126
127 span_close(child, 1000200000, 0)
128 if span_duration_ns(child) != 100000 { return 5 }
129
130 span_close(root, 1000500000, 0)
131 if span_duration_ns(root) != 500000 { return 6 }
132 return 0
133}