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:
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user