otel P4: render-fold → SVG waterfall

otel/waterfall-rects folds a trace's spans into rect geometry (x by start
offset, width by duration, y by depth via parent-link ancestor count);
otel/waterfall folds those into an inline <svg> (one <rect>+<text> per span).
Renders to real SVG markup via the html tag registry.
This commit is contained in:
2026-07-01 14:55:13 +00:00
parent 06294e964c
commit 5f06b5e8e0
2 changed files with 127 additions and 0 deletions

View File

@@ -126,3 +126,91 @@
;; Flatten nested route groups and instrument each route — the host/make-app seam.
(define otel/instrument-routes
(fn (routes) (map otel/instrument-route (dr/flatten-routes routes))))
;; ── P4: render-fold → SVG waterfall ───────────────────────────────────
;; A trace is a tree of spans; a waterfall is a fold over that tree into SVG (the
;; same discipline as the compose render-fold: walk the structure, emit per node).
;; Layout: x ∝ (t0 trace.t0), width ∝ duration, y ∝ depth (ancestor count via
;; parent links). Geometry is computed as plain rect data first (otel/waterfall-
;; rects, testable), then folded into <svg> markup (otel/waterfall).
(define otel/-row-h 18)
(define otel/-svg-w 640)
(define otel/-pad 4)
;; all spans belonging to one trace, from the ring.
(define otel/trace-spans
(fn (trace-id) (filter (fn (s) (= (get s :trace) trace-id)) (otel/recent))))
(define otel/-span-by-id
(fn (spans id) (first (filter (fn (s) (= (get s :span) id)) spans))))
;; depth = number of ancestors reachable via :parent within this trace's spans.
(define otel/-depth
(fn (s spans)
(if (nil? (get s :parent))
0
(let ((p (otel/-span-by-id spans (get s :parent))))
(if (nil? p) 0 (+ 1 (otel/-depth p spans)))))))
;; trace start = earliest t0; end = latest t1 (folds over the spans).
(define otel/-min-t0
(fn (spans) (reduce (fn (m s) (min m (get s :t0))) (get (first spans) :t0) spans)))
(define otel/-max-t1
(fn (spans) (reduce (fn (m s) (max m (get s :t1))) (get (first spans) :t1) spans)))
;; a rect per span: {:span :name :depth :x :y :w}. x/width are scaled to the
;; drawable width; a zero-duration span still gets a 1px sliver so it's visible.
(define otel/waterfall-rects
(fn (trace-id)
(let ((spans (otel/trace-spans trace-id)))
(if (empty? spans)
(list)
(let ((t0 (otel/-min-t0 spans))
(t1 (otel/-max-t1 spans))
(iw (- otel/-svg-w (* 2 otel/-pad))))
(let ((total (if (> (- t1 t0) 0) (- t1 t0) 1)))
(map
(fn (s)
(let ((d (otel/-depth s spans))
(dur (- (get s :t1) (get s :t0))))
(let ((raw-w (* (/ dur total) iw)))
{:span (get s :span)
:name (get s :name)
:depth d
:x (+ otel/-pad (* (/ (- (get s :t0) t0) total) iw))
:y (+ otel/-pad (* d otel/-row-h))
:w (if (> raw-w 1) raw-w 1)})))
spans)))))))
;; one <g> (bar + label) per rect.
(define otel/-rect->g
(fn (r)
(quasiquote
(g
(rect :x (unquote (get r :x)) :y (unquote (get r :y))
:width (unquote (get r :w)) :height (unquote (- otel/-row-h 2))
:fill "#4c78a8" :rx 2)
(text :x (unquote (+ (get r :x) 3)) :y (unquote (+ (get r :y) 12))
:font-size 10 :fill "#ffffff" (unquote (get r :name)))))))
;; max depth across rects (0 when empty) → svg height.
(define otel/-max-depth
(fn (rects) (reduce (fn (m r) (max m (get r :depth))) 0 rects)))
;; the trace as an inline <svg> waterfall.
(define otel/waterfall
(fn (trace-id)
(let ((rects (otel/waterfall-rects trace-id)))
(let ((h (+ (* 2 otel/-pad) (* (+ (otel/-max-depth rects) 1) otel/-row-h))))
(quasiquote
(svg :width (unquote otel/-svg-w) :height (unquote h)
:xmlns "http://www.w3.org/2000/svg"
(splice-unquote (map otel/-rect->g rects))))))))
;; count nodes whose head symbol prints as `head` — a small SVG-tree assertion aid.
(define otel/-tree-count
(fn (tree head)
(if (list? tree)
(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))
0)))