code wiki / (root) / nx_http_metrics.nx

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}