otel: waterfall time ruler + recent-traces show actual target path & duration

This commit is contained in:
2026-07-01 20:18:40 +00:00
parent 322ff4f691
commit 7754666de1
2 changed files with 70 additions and 10 deletions

View File

@@ -131,9 +131,12 @@
(handler (dream-route-handler r)))
(dream-route method path
(fn (req)
;; :http.route is the PATTERN (groups metrics: all posts under /:slug);
;; :http.target is the ACTUAL path this request hit (e.g. /welcome), so
;; the trace view can show which concrete resource was served.
(otel/-timed
(str method " " path)
{:http.method method :http.route path}
{:http.method method :http.route path :http.target (dream-path req)}
(fn (resp) {:http.status (dream-status (dream-coerce-response resp))})
(fn () (handler req))))))))
@@ -226,15 +229,54 @@
(define otel/-max-depth
(fn (rects) (reduce (fn (m r) (max m (get r :depth))) 0 rects)))
;; the trace as an inline <svg> waterfall.
;; (0 1 … n-1)
(define otel/-range
(fn (n) (if (<= n 0) (list) (append (otel/-range (- n 1)) (list (- n 1))))))
;; a trace's wall span (ns): latest end earliest start.
(define otel/-trace-dur
(fn (trace-id)
(let ((spans (otel/trace-spans trace-id)))
(if (empty? spans) 0 (- (otel/-max-t1 spans) (otel/-min-t0 spans))))))
;; a time ruler across the top: N vertical gridlines spanning the body, each with a
;; "<t>ms" label showing the offset from the trace start. Chrome is <line>/<text> so
;; the per-span <rect> count is unaffected.
(define otel/-ruler
(fn (dur ruler-h total-h)
(let ((inner (- otel/-svg-w (* 2 otel/-pad)))
(n 6))
(quasiquote
(g
(line :x1 (unquote otel/-pad) :y1 (unquote (- ruler-h 3))
:x2 (unquote (- otel/-svg-w otel/-pad)) :y2 (unquote (- ruler-h 3))
:stroke "#ccc" :stroke-width 1)
(splice-unquote
(map
(fn (i)
(let ((x (+ otel/-pad (quotient (* inner i) (- n 1))))
(ms (quotient (* dur i) (* (- n 1) 1000000))))
(quasiquote
(g
(line :x1 (unquote x) :y1 (unquote (- ruler-h 3)) :x2 (unquote x)
:y2 (unquote total-h) :stroke "#eee" :stroke-width 1)
(text :x (unquote (+ x 2)) :y 10 :font-size 9 :fill "#999"
(unquote (str ms "ms")))))))
(otel/-range n))))))))
;; the trace as an inline <svg> waterfall, with a time ruler above the bars.
(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))))))))
(let ((rects (otel/waterfall-rects trace-id))
(ruler-h 16))
(let ((body-h (* (+ (otel/-max-depth rects) 1) otel/-row-h)))
(let ((h (+ (* 2 otel/-pad) ruler-h body-h)))
(quasiquote
(svg :width (unquote otel/-svg-w) :height (unquote h)
:xmlns "http://www.w3.org/2000/svg" :font-family "monospace"
(unquote (otel/-ruler (otel/-trace-dur trace-id) ruler-h h))
(g :transform (unquote (str "translate(0," (+ otel/-pad ruler-h) ")"))
(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
@@ -323,10 +365,17 @@
((not (empty? roots)) (get (first roots) :name))
((not (empty? spans)) (get (first spans) :name))
(else ""))))))
;; the actual target path of a trace's root request (nil if not an http trace).
(define otel/-trace-target
(fn (trace-id)
(let ((roots (filter (fn (s) (nil? (get s :parent))) (otel/trace-spans trace-id))))
(if (empty? roots) nil (get (get (first roots) :attrs) :http.target)))))
(define otel/trace-summary
(fn (trace-id)
{:trace trace-id
:name (otel/-trace-root-name trace-id)
:target (otel/-trace-target trace-id)
:dur (otel/-trace-dur trace-id)
:spans (len (otel/trace-spans trace-id))}))
(define otel/recent-traces
(fn () (reverse (map otel/trace-summary (otel/-trace-ids)))))
@@ -382,7 +431,11 @@
(fn (t)
(quasiquote
(li :data-trace (unquote (str (get t :trace)))
(unquote (str (get t :name) " — " (get t :spans) " spans")))))
;; show the concrete target (/welcome) if known, else the span
;; name, then the wall duration and span count.
(span :style "font-weight:600"
(unquote (or (get t :target) (get t :name))))
(unquote (str " · " (otel/-ms (get t :dur)) " · " (get t :spans) " spans")))))
traces))))))
;; a per-route latency bar chart: each row is a nested bar — teal p50 inside amber

View File

@@ -158,7 +158,8 @@
(define host-ot-svg (otel/waterfall host-ot-tid))
(host-ot-test "waterfall head is svg" (str (first host-ot-svg)) "svg")
(host-ot-test "svg has one rect per span" (otel/-tree-count host-ot-svg "rect") 4)
(host-ot-test "svg has one label per span" (otel/-tree-count host-ot-svg "text") 4)
(host-ot-test "svg has at least one label per span" (>= (otel/-tree-count host-ot-svg "text") 4) true)
(host-ot-test "waterfall has a time ruler (gridlines)" (> (otel/-tree-count host-ot-svg "line") 0) true)
;; unknown trace → empty waterfall, still a valid svg
(host-ot-test "unknown trace has no rects" (len (otel/waterfall-rects "no-such-trace")) 0)
@@ -218,6 +219,12 @@
(host-ot-test "recent-traces lists both" (len (otel/recent-traces)) 2)
(host-ot-test "recent-traces newest first"
(get (first (otel/recent-traces)) :name) "GET /health")
(host-ot-test "recent-traces carries the actual target path"
(get (first (otel/recent-traces)) :target) "/health")
(host-ot-test "recent-traces carries a wall duration"
(>= (get (first (otel/recent-traces)) :dur) 0) true)
(host-ot-test "request span records http.target"
(get (get (last (otel/recent)) :attrs) :http.target) "/health")
(host-ot-test "latest-trace is the /health trace"
(otel/-trace-root-name (otel/latest-trace)) "GET /health")