otel P5: metrics aggregate-fold (per-route counts + p50/p95/p99)
otel/metrics folds spans → {:total-requests :routes}; each route carries a
request count and nearest-rank latency percentiles over its durations. Route
key is the http.route attr (falls back to span name). Includes a small
insertion sort (no sort primitive) and order-preserving distinct.
This commit is contained in:
@@ -214,3 +214,62 @@
|
|||||||
(let ((self (if (and (not (empty? tree)) (= (str (first tree)) head)) 1 0)))
|
(let ((self (if (and (not (empty? tree)) (= (str (first tree)) head)) 1 0)))
|
||||||
(reduce (fn (acc n) (+ acc (otel/-tree-count n head))) self tree))
|
(reduce (fn (acc n) (+ acc (otel/-tree-count n head))) self tree))
|
||||||
0)))
|
0)))
|
||||||
|
|
||||||
|
;; ── P5: metrics (aggregate-fold) ──────────────────────────────────────
|
||||||
|
;; Fold the recent spans into per-route request counts + a latency histogram
|
||||||
|
;; (p50/p95/p99 from durations). No `sort` primitive here, so percentiles ride a
|
||||||
|
;; tiny insertion sort; nearest-rank keeps the maths exact for the tests.
|
||||||
|
(define otel/-insert
|
||||||
|
(fn (x sorted)
|
||||||
|
(if (empty? sorted)
|
||||||
|
(list x)
|
||||||
|
(if (<= x (first sorted))
|
||||||
|
(cons x sorted)
|
||||||
|
(cons (first sorted) (otel/-insert x (rest sorted)))))))
|
||||||
|
(define otel/-sort-nums
|
||||||
|
(fn (lst) (reduce (fn (acc x) (otel/-insert x acc)) (list) lst)))
|
||||||
|
|
||||||
|
;; nearest-rank percentile of an ASCENDING list: rank = ceil(p/100 · N), 1-based.
|
||||||
|
(define otel/-percentile
|
||||||
|
(fn (sorted p)
|
||||||
|
(if (empty? sorted)
|
||||||
|
0
|
||||||
|
(let ((n (len sorted)))
|
||||||
|
(let ((idx (- (ceil (* (/ p 100) n)) 1)))
|
||||||
|
(let ((i (if (< idx 0) 0 (if (>= idx n) (- n 1) idx))))
|
||||||
|
(nth sorted i)))))))
|
||||||
|
|
||||||
|
;; a span's route label: the http.route attr, else the span name.
|
||||||
|
(define otel/-span-route
|
||||||
|
(fn (s) (or (get (get s :attrs) :http.route) (get s :name))))
|
||||||
|
|
||||||
|
(define otel/-span-dur (fn (s) (- (get s :t1) (get s :t0))))
|
||||||
|
|
||||||
|
;; distinct values, order-preserving.
|
||||||
|
(define otel/-distinct
|
||||||
|
(fn (lst)
|
||||||
|
(reduce
|
||||||
|
(fn (acc x) (if (some (fn (y) (= y x)) acc) acc (append acc (list x))))
|
||||||
|
(list)
|
||||||
|
lst)))
|
||||||
|
|
||||||
|
;; the aggregate for one route: count + latency percentiles over its durations.
|
||||||
|
(define otel/route-metrics
|
||||||
|
(fn (spans route)
|
||||||
|
(let ((rs (filter (fn (s) (= (otel/-span-route s) route)) spans)))
|
||||||
|
(let ((durs (otel/-sort-nums (map otel/-span-dur rs))))
|
||||||
|
{:route route
|
||||||
|
:count (len rs)
|
||||||
|
:p50 (otel/-percentile durs 50)
|
||||||
|
:p95 (otel/-percentile durs 95)
|
||||||
|
:p99 (otel/-percentile durs 99)}))))
|
||||||
|
|
||||||
|
;; fold spans → {:total-requests N :routes (per-route metrics …)}.
|
||||||
|
(define otel/metrics
|
||||||
|
(fn (spans)
|
||||||
|
(let ((routes (otel/-distinct (map otel/-span-route spans))))
|
||||||
|
{:total-requests (len spans)
|
||||||
|
:routes (map (fn (r) (otel/route-metrics spans r)) routes)})))
|
||||||
|
|
||||||
|
;; convenience: metrics over the current ring.
|
||||||
|
(define otel/metrics-recent (fn () (otel/metrics (otel/recent))))
|
||||||
|
|||||||
@@ -165,6 +165,43 @@
|
|||||||
(host-ot-test "unknown trace still yields an svg"
|
(host-ot-test "unknown trace still yields an svg"
|
||||||
(str (first (otel/waterfall "no-such-trace"))) "svg")
|
(str (first (otel/waterfall "no-such-trace"))) "svg")
|
||||||
|
|
||||||
|
;; ── P5: metrics (aggregate-fold) ───────────────────────────────────
|
||||||
|
;; Fold recent spans → per-route counters + latency percentiles (nearest-rank).
|
||||||
|
;; Build spans with KNOWN durations so the percentiles are deterministic.
|
||||||
|
(otel/reset!)
|
||||||
|
(for-each
|
||||||
|
(fn (d)
|
||||||
|
(otel/record! {:trace "t" :span (str "s" d) :parent nil :name "GET /feed"
|
||||||
|
:t0 0 :t1 d :attrs {:http.route "/feed"} :events (list)}))
|
||||||
|
(list 30 10 50 20 40)) ;; unsorted on purpose — the fold must sort
|
||||||
|
(otel/record! {:trace "t" :span "h" :parent nil :name "GET /health"
|
||||||
|
:t0 0 :t1 5 :attrs {:http.route "/health"} :events (list)})
|
||||||
|
|
||||||
|
(define host-ot-m (otel/metrics (otel/recent)))
|
||||||
|
(host-ot-test "total requests" (get host-ot-m :total-requests) 6)
|
||||||
|
(host-ot-test "two routes" (len (get host-ot-m :routes)) 2)
|
||||||
|
|
||||||
|
(define host-ot-feed
|
||||||
|
(first (filter (fn (r) (= (get r :route) "/feed")) (get host-ot-m :routes))))
|
||||||
|
(host-ot-test "feed count" (get host-ot-feed :count) 5)
|
||||||
|
(host-ot-test "feed p50" (get host-ot-feed :p50) 30) ;; ceil(.5*5)=3 → sorted[2]=30
|
||||||
|
(host-ot-test "feed p95" (get host-ot-feed :p95) 50) ;; ceil(.95*5)=5 → sorted[4]=50
|
||||||
|
(host-ot-test "feed p99" (get host-ot-feed :p99) 50)
|
||||||
|
|
||||||
|
(define host-ot-health
|
||||||
|
(first (filter (fn (r) (= (get r :route) "/health")) (get host-ot-m :routes))))
|
||||||
|
(host-ot-test "health count" (get host-ot-health :count) 1)
|
||||||
|
(host-ot-test "health p50 of a single sample" (get host-ot-health :p50) 5)
|
||||||
|
|
||||||
|
;; the sort helper on its own
|
||||||
|
(host-ot-test "sort-nums ascending" (otel/-sort-nums (list 3 1 2 5 4)) (list 1 2 3 4 5))
|
||||||
|
|
||||||
|
;; empty ring → zeroed metrics, no routes
|
||||||
|
(otel/reset!)
|
||||||
|
(define host-ot-me (otel/metrics (otel/recent)))
|
||||||
|
(host-ot-test "empty metrics total 0" (get host-ot-me :total-requests) 0)
|
||||||
|
(host-ot-test "empty metrics no routes" (len (get host-ot-me :routes)) 0)
|
||||||
|
|
||||||
(define
|
(define
|
||||||
host-ot-tests-run!
|
host-ot-tests-run!
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
Reference in New Issue
Block a user