nx_http_metrics.nx source
↩ module page · 330 lines · 11197 B
1// nx_http_metrics.nx -- Prometheus-text /metrics endpoint emitter.
2//
3// Closes the observability triad: health (liveness) + ready
4// (readiness) + metrics (instrumentation). Companion to
5// nx_http_health.nx.
6//
7// Per cardinal feedback-no-third-party-trust-native-or-nothing:
8// substrate's own metrics emitter following the Prometheus
9// exposition format (text/plain v0.0.4 per
10// prometheus.io/docs/instrumenting/exposition_formats/). No
11// Prometheus SDK; no OpenTelemetry SDK; no statsd. Substrate
12// produces the wire bytes; any Prometheus / Grafana / Mimir / Loki
13// consumer can scrape.
14//
15// Per cardinal feedback-defensive-at-boundaries-trusting-internally:
16// metric NAMES are validated (ASCII alnum + underscore per spec);
17// metric VALUES are i64 (Q10 fixed-point friendly; substrate has
18// no FP). Float values would need nx_q10_to_fp_text (queued).
19//
20// Metric format (Prometheus exposition v0.0.4):
21//
22// # HELP <name> <human-text>
23// # TYPE <name> {counter|gauge|histogram|summary}
24// <name> <value>
25// <name>{label="value"} <value>
26//
27// Substrate emits:
28// - counter monotonically-increasing i64
29// - gauge arbitrary i64 (may go up or down)
30// - (histogram + summary queued)
31//
32// nx_capability_claims:
33// needs: [sealed_enum, bounded_buffer]
34// provides: [prometheus_text_metric_emit, metrics_observability]
35// safety: [no_unchecked_deref, no_floating_point, no_syscall,
36// bounded_iteration, bit_equal_reproducible,
37// target_agnostic, metric_name_validated]
38// verdict: [sealed_enum_5_state]
39// license: ORIGINAL
40// kind: racing_crew_specialist
41// layer: L3 (algorithm: structured metrics emit)
42
43// ---- Sealed enum: metric emit verdict ----------------------------
44
45const NXM_OK: i64 = 0
46const NXM_OOM_BUFFER: i64 = 1
47const NXM_BAD_METRIC_NAME: i64 = 2
48const NXM_BAD_TYPE: i64 = 3
49const NXM_BAD_ARG: i64 = 4
50const NXM_VERDICT_N: i64 = 5
51
52func nxm_verdict_is_valid(v: i64) -> i64 {
53 if v < 0 { return 0 }
54 if v >= NXM_VERDICT_N { return 0 }
55 return 1
56}
57
58func nxm_verdict_name(v: i64) -> *u8 {
59 if v == NXM_OK { return "OK" as *u8 }
60 if v == NXM_OOM_BUFFER { return "OOM_BUFFER" as *u8 }
61 if v == NXM_BAD_METRIC_NAME { return "BAD_METRIC_NAME" as *u8 }
62 if v == NXM_BAD_TYPE { return "BAD_TYPE" as *u8 }
63 if v == NXM_BAD_ARG { return "BAD_ARG" as *u8 }
64 return "INVALID" as *u8
65}
66
67// ---- Sealed enum: metric type ------------------------------------
68
69const NXM_TYPE_COUNTER: i64 = 0
70const NXM_TYPE_GAUGE: i64 = 1
71const NXM_TYPE_HISTOGRAM: i64 = 2
72const NXM_TYPE_SUMMARY: i64 = 3
73const NXM_TYPE_N: i64 = 4
74
75func nxm_type_is_valid(t: i64) -> i64 {
76 if t < 0 { return 0 }
77 if t >= NXM_TYPE_N { return 0 }
78 return 1
79}
80
81func nxm_type_name(t: i64) -> *u8 {
82 if t == NXM_TYPE_COUNTER { return "counter" as *u8 }
83 if t == NXM_TYPE_GAUGE { return "gauge" as *u8 }
84 if t == NXM_TYPE_HISTOGRAM { return "histogram" as *u8 }
85 if t == NXM_TYPE_SUMMARY { return "summary" as *u8 }
86 return "invalid" as *u8
87}
88
89func nxm_type_name_len(t: i64) -> i64 {
90 if t == NXM_TYPE_COUNTER { return 7 }
91 if t == NXM_TYPE_GAUGE { return 5 }
92 if t == NXM_TYPE_HISTOGRAM { return 9 }
93 if t == NXM_TYPE_SUMMARY { return 7 }
94 return 7 // "invalid"
95}
96
97// ---- Validators --------------------------------------------------
98//
99// Prometheus metric-name regex: [a-zA-Z_:][a-zA-Z0-9_:]*
100// Substrate sub-set: [a-zA-Z_][a-zA-Z0-9_]* (drop colons; colons are
101// reserved for federation per spec §4.2 and substrate doesn't
102// federate).
103
104func nxm_is_name_start(b: i64) -> i64 {
105 if b >= 0x41 && b <= 0x5a { return 1 } // A-Z
106 if b >= 0x61 && b <= 0x7a { return 1 } // a-z
107 if b == 0x5f { return 1 } // _
108 return 0
109}
110
111func nxm_is_name_cont(b: i64) -> i64 {
112 if nxm_is_name_start(b) == 1 { return 1 }
113 if b >= 0x30 && b <= 0x39 { return 1 } // 0-9
114 return 0
115}
116
117func nxm_validate_name(name: *u8, name_n: i64) -> i64 {
118 if name == (0 as *u8) { return 0 }
119 if name_n <= 0 { return 0 }
120 if nxm_is_name_start(name[0] as i64) == 0 { return 0 }
121 var i: i64 = 1
122 while i < name_n {
123 if nxm_is_name_cont(name[i] as i64) == 0 { return 0 }
124 i = i + 1
125 }
126 return 1
127}
128
129// ---- Byte emit helpers ------------------------------------------
130
131func nxm_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 {
132 if *off >= cap { return NXM_OOM_BUFFER }
133 out[*off] = b as u8
134 *off = *off + 1
135 return NXM_OK
136}
137
138func nxm_put_cstr(out: *u8, off: *i64, cap: i64, s: *u8) -> i64 {
139 var i: i64 = 0
140 while s[i] != 0 {
141 let rc: i64 = nxm_put(out, off, cap, s[i] as i64)
142 if rc != NXM_OK { return rc }
143 i = i + 1
144 }
145 return NXM_OK
146}
147
148func nxm_put_bytes(out: *u8, off: *i64, cap: i64, src: *u8, n: i64) -> i64 {
149 var i: i64 = 0
150 while i < n {
151 let rc: i64 = nxm_put(out, off, cap, src[i] as i64)
152 if rc != NXM_OK { return rc }
153 i = i + 1
154 }
155 return NXM_OK
156}
157
158// i64 -> ASCII decimal using caller-supplied scratch (>= 24 bytes).
159func nxm_put_dec(out: *u8, off: *i64, cap: i64,
160 scratch: *u8, v: i64) -> i64 {
161 if v == 0 { return nxm_put(out, off, cap, 0x30) }
162 var n: i64 = v
163 var sign: i64 = 0
164 if n < 0 { sign = 1; n = 0 - n }
165 var k: i64 = 0
166 while n > 0 {
167 scratch[k] = (0x30 + (n - (n / 10) * 10)) as u8
168 n = n / 10
169 k = k + 1
170 }
171 if sign == 1 {
172 let rcs: i64 = nxm_put(out, off, cap, 0x2d)
173 if rcs != NXM_OK { return rcs }
174 }
175 var ri: i64 = k - 1
176 while ri >= 0 {
177 let rc: i64 = nxm_put(out, off, cap, scratch[ri] as i64)
178 if rc != NXM_OK { return rc }
179 ri = ri - 1
180 }
181 return NXM_OK
182}
183
184// ---- Single-metric block emitter --------------------------------
185//
186// Emits a 3-line block:
187// # HELP <name> <help>\n
188// # TYPE <name> <type>\n
189// <name> <value>\n
190//
191// Caller supplies metric name (validated), help text (ASCII, no
192// newlines or backslashes per spec §4.2.4), type, value, scratch.
193
194func nx_http_metrics_emit_one(
195 out: *u8, off: *i64, cap: i64,
196 name: *u8, name_n: i64,
197 help: *u8, help_n: i64,
198 type_id: i64,
199 value: i64,
200 scratch: *u8) -> i64 {
201 if out == (0 as *u8) { return NXM_BAD_ARG }
202 if off == (0 as *i64) { return NXM_BAD_ARG }
203 if cap <= 0 { return NXM_BAD_ARG }
204 if scratch == (0 as *u8) { return NXM_BAD_ARG }
205 if nxm_validate_name(name, name_n) != 1 { return NXM_BAD_METRIC_NAME }
206 if nxm_type_is_valid(type_id) != 1 { return NXM_BAD_TYPE }
207 if help_n < 0 { return NXM_BAD_ARG }
208
209 // # HELP <name> <help>\n
210 let r1: i64 = nxm_put_cstr(out, off, cap, "# HELP " as *u8)
211 if r1 != NXM_OK { return r1 }
212 let r2: i64 = nxm_put_bytes(out, off, cap, name, name_n)
213 if r2 != NXM_OK { return r2 }
214 let r3: i64 = nxm_put(out, off, cap, 0x20)
215 if r3 != NXM_OK { return r3 }
216 if help_n > 0 {
217 let r4: i64 = nxm_put_bytes(out, off, cap, help, help_n)
218 if r4 != NXM_OK { return r4 }
219 }
220 let r5: i64 = nxm_put(out, off, cap, 0x0a)
221 if r5 != NXM_OK { return r5 }
222
223 // # TYPE <name> <type>\n
224 let r6: i64 = nxm_put_cstr(out, off, cap, "# TYPE " as *u8)
225 if r6 != NXM_OK { return r6 }
226 let r7: i64 = nxm_put_bytes(out, off, cap, name, name_n)
227 if r7 != NXM_OK { return r7 }
228 let r8: i64 = nxm_put(out, off, cap, 0x20)
229 if r8 != NXM_OK { return r8 }
230 let r9: i64 = nxm_put_bytes(out, off, cap,
231 nxm_type_name(type_id),
232 nxm_type_name_len(type_id))
233 if r9 != NXM_OK { return r9 }
234 let r10: i64 = nxm_put(out, off, cap, 0x0a)
235 if r10 != NXM_OK { return r10 }
236
237 // <name> <value>\n
238 let r11: i64 = nxm_put_bytes(out, off, cap, name, name_n)
239 if r11 != NXM_OK { return r11 }
240 let r12: i64 = nxm_put(out, off, cap, 0x20)
241 if r12 != NXM_OK { return r12 }
242 let r13: i64 = nxm_put_dec(out, off, cap, scratch, value)
243 if r13 != NXM_OK { return r13 }
244 return nxm_put(out, off, cap, 0x0a)
245}
246
247// ---- Top-level HTTP response emitter ----------------------------
248//
249// Builds the full /metrics HTTP response in one shot. Caller
250// supplies a callback-free "tape" of metrics via the (name, help,
251// type, value) parallel arrays. Substrate emits headers + each
252// metric block.
253//
254// Caller pattern:
255//
256// let names_buf: *u8 = ... (packed)
257// let name_offs: *i64 = ... (per-metric offset into names_buf)
258// let name_lens: *i64 = ...
259// let helps_buf, help_offs, help_lens (same shape)
260// let types: *i64 = ... (sealed-enum per metric)
261// let values: *i64 = ...
262// let n_metrics: i64 = ...
263//
264// Output shape:
265// HTTP/1.1 200 OK\r\n
266// Content-Type: text/plain; version=0.0.4; charset=utf-8\r\n
267// Content-Length: N\r\n
268// \r\n
269// # HELP nishi_uptime_ms Daemon uptime
270// # TYPE nishi_uptime_ms gauge
271// nishi_uptime_ms 12345
272// # HELP nishi_requests_total Request count
273// # TYPE nishi_requests_total counter
274// nishi_requests_total 42
275// ...
276
277func nx_http_metrics_emit_response(
278 out: *u8, cap: i64, out_n: *i64,
279 names_buf: *u8, name_offs: *i64, name_lens: *i64,
280 helps_buf: *u8, help_offs: *i64, help_lens: *i64,
281 types: *i64, values: *i64, n_metrics: i64,
282 scratch: *u8) -> i64 {
283 if out == (0 as *u8) { return NXM_BAD_ARG }
284 if out_n == (0 as *i64) { return NXM_BAD_ARG }
285 if scratch == (0 as *u8) { return NXM_BAD_ARG }
286 if cap <= 0 { return NXM_BAD_ARG }
287 if n_metrics < 0 { return NXM_BAD_ARG }
288
289 // Pre-build body into out[256..] so we know its length.
290 let body_start: i64 = 256
291 if cap < body_start + 32 { return NXM_OOM_BUFFER }
292 var body_off: i64 = body_start
293 var i: i64 = 0
294 while i < n_metrics {
295 let n_off: i64 = name_offs[i]
296 let n_len: i64 = name_lens[i]
297 let h_off: i64 = help_offs[i]
298 let h_len: i64 = help_lens[i]
299 let n_ptr: *u8 = ((names_buf as i64) + n_off) as *u8
300 let h_ptr: *u8 = ((helps_buf as i64) + h_off) as *u8
301 let rc: i64 = nx_http_metrics_emit_one(
302 out, &body_off, cap,
303 n_ptr, n_len,
304 h_ptr, h_len,
305 types[i], values[i],
306 scratch)
307 if rc != NXM_OK { return rc }
308 i = i + 1
309 }
310 let body_n: i64 = body_off - body_start
311
312 // Emit headers at out[0..].
313 var h_off: i64 = 0
314 let rh: i64 = nxm_put_cstr(out, &h_off, body_start,
315 "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\nContent-Length: " as *u8)
316 if rh != NXM_OK { return rh }
317 let rc_l: i64 = nxm_put_dec(out, &h_off, body_start, scratch, body_n)
318 if rc_l != NXM_OK { return rc_l }
319 let rb: i64 = nxm_put_cstr(out, &h_off, body_start, "\r\n\r\n" as *u8)
320 if rb != NXM_OK { return rb }
321
322 // Move body bytes from out[body_start..body_off] down to out[h_off..].
323 var mi: i64 = 0
324 while mi < body_n {
325 out[h_off + mi] = out[body_start + mi]
326 mi = mi + 1
327 }
328 *out_n = h_off + body_n
329 return NXM_OK
330}