otel P6: live dashboard — GET /otel SSR + /otel/stream SSE

otel/dashboard SSRs the metrics strip + latest-trace waterfall + recent-traces
list as HTML carrying Datastar-style data-on-load subscribing to /otel/stream,
the SSE feed of SXTP otel.span events. Routes otel/dashboard-route +
otel/stream-route (otel/routes) mount via make-app. recent-traces/latest-trace
+ otel/span-event helpers.
This commit is contained in:
2026-07-01 15:32:14 +00:00
parent 296fa45bea
commit 4400870abe
2 changed files with 167 additions and 0 deletions

View File

@@ -273,3 +273,113 @@
;; convenience: metrics over the current ring.
(define otel/metrics-recent (fn () (otel/metrics (otel/recent))))
;; ── P6: live dashboard (SSR + SSE) ────────────────────────────────────
;; GET /otel renders a dashboard — metrics strip + the latest trace's waterfall +
;; a recent-traces list — as server-rendered HTML carrying Datastar-style reactive
;; attributes that subscribe to GET /otel/stream, the SSE feed of new span events
;; (SXTP events, the host's Datastar-borrowed wire format). SSR + declarative
;; reactive attrs + SSE patches IS the reactive-island model here. (Live client
;; hydration is a deploy concern; SSR, the event feed, and the data are tested.)
;; recent traces, newest-first: {:trace :name :spans}. root name = the parentless
;; span (fallback: first recorded).
(define otel/-trace-ids
(fn () (otel/-distinct (map (fn (s) (get s :trace)) (otel/recent)))))
(define otel/-trace-root-name
(fn (trace-id)
(let ((spans (otel/trace-spans trace-id)))
(let ((roots (filter (fn (s) (nil? (get s :parent))) spans)))
(cond
((not (empty? roots)) (get (first roots) :name))
((not (empty? spans)) (get (first spans) :name))
(else ""))))))
(define otel/trace-summary
(fn (trace-id)
{:trace trace-id
:name (otel/-trace-root-name trace-id)
:spans (len (otel/trace-spans trace-id))}))
(define otel/recent-traces
(fn () (reverse (map otel/trace-summary (otel/-trace-ids)))))
(define otel/latest-trace
(fn ()
(let ((r (otel/recent)))
(if (empty? r) nil (get (last r) :trace)))))
;; ── SSE span events (SXTP) ────────────────────────────────────────────
(define otel/span-event
(fn (s)
(sxtp/event "otel.span"
{:id (get s :span)
:body {:trace (get s :trace) :span (get s :span) :parent (get s :parent)
:name (get s :name) :t0 (get s :t0) :t1 (get s :t1)
:attrs (get s :attrs)}
:time (get s :t1)})))
(define otel/latest-span-event
(fn ()
(let ((r (otel/recent)))
(if (empty? r) nil (otel/span-event (last r))))))
;; one SSE frame: `event: otel.span\n data: <sxtp event>\n\n`; "" when no spans.
(define otel/-stream-body
(fn ()
(let ((e (otel/latest-span-event)))
(if (nil? e)
""
(str "event: otel.span\ndata: " (sxtp/serialize e) "\n\n")))))
;; ── dashboard markup (plain HTML tags → render-to-html SSRs cleanly) ──
(define otel/-metrics-strip
(fn (m)
(quasiquote
(table :class "otel-metrics"
(tr (th "route") (th "count") (th "p50") (th "p95") (th "p99"))
(splice-unquote
(map
(fn (r)
(quasiquote
(tr (td (unquote (str (get r :route))))
(td (unquote (str (get r :count))))
(td (unquote (str (get r :p50))))
(td (unquote (str (get r :p95))))
(td (unquote (str (get r :p99)))))))
(get m :routes)))))))
(define otel/-traces-list
(fn (traces)
(quasiquote
(ul :class "otel-traces"
(splice-unquote
(map
(fn (t)
(quasiquote
(li :data-trace (unquote (str (get t :trace)))
(unquote (str (get t :name) " — " (get t :spans) " spans")))))
traces))))))
(define otel/dashboard
(fn ()
(let ((m (otel/metrics (otel/recent)))
(lt (otel/latest-trace))
(traces (otel/recent-traces)))
(quasiquote
(div :id "otel-dashboard" :data-on-load "@get('/otel/stream')"
(h1 "OpenTelemetry")
(h2 "metrics")
(unquote (otel/-metrics-strip m))
(h2 "latest trace")
(unquote
(if (nil? lt)
(quasiquote (p :class "otel-empty" "no traces yet"))
(otel/waterfall lt)))
(h2 "recent traces")
(unquote (otel/-traces-list traces)))))))
;; ── routes ────────────────────────────────────────────────────────────
(define otel/dashboard-route
(dream-get "/otel"
(fn (req) (dream-html (render-to-html (otel/dashboard) {})))))
(define otel/stream-route
(dream-get "/otel/stream"
(fn (req)
(dream-response 200 {:content-type "text/event-stream"} (otel/-stream-body)))))
(define otel/routes (list otel/dashboard-route otel/stream-route))