From 7754666de1e888056d4049c507ab151f4343aba2 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 1 Jul 2026 20:18:40 +0000 Subject: [PATCH] otel: waterfall time ruler + recent-traces show actual target path & duration --- lib/host/otel.sx | 71 ++++++++++++++++++++++++++++++++++++------ lib/host/tests/otel.sx | 9 +++++- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/lib/host/otel.sx b/lib/host/otel.sx index b264f93f..9f69e532 100644 --- a/lib/host/otel.sx +++ b/lib/host/otel.sx @@ -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 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 +;; "ms" label showing the offset from the trace start. Chrome is / so +;; the per-span 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 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 diff --git a/lib/host/tests/otel.sx b/lib/host/tests/otel.sx index 49c8933d..3897b249 100644 --- a/lib/host/tests/otel.sx +++ b/lib/host/tests/otel.sx @@ -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")