diff --git a/lib/host/otel.sx b/lib/host/otel.sx index 6fabdb2a..4bc562c0 100644 --- a/lib/host/otel.sx +++ b/lib/host/otel.sx @@ -474,20 +474,15 @@ (unquote (str "p99 " (otel/-ms (get r :p99)))))))))) routes))))))))) -(define otel/dashboard +;; the refreshing content — everything that changes as spans arrive. Served on its +;; own at GET /otel/fragment; the poll swaps THIS into #otel-body. +(define otel/-dashboard-body (fn () (let ((m (otel/metrics (otel/recent))) (lt (otel/latest-trace)) (traces (otel/recent-traces))) (quasiquote - (div :id "otel-dashboard" - ;; SPA-native live refresh: the SX engine polls GET /otel every 3s and - ;; swaps this div in place (outerHTML). The poll is a boosted request, so - ;; the route returns the text/sx fragment — no full reload, stays in the - ;; SPA. (No , which would blow away the SPA shell.) - :sx-get "/otel" :sx-trigger "every 3s" :sx-target "#otel-dashboard" :sx-swap "outerHTML" - (h1 "OpenTelemetry") - (p :style "font-size:0.8em;opacity:0.7" "live · refreshes every 3s in-app") + (div (h2 "latency by route") (p :style "font-size:0.75em;opacity:0.7" (span :style "color:#4c9a8f" "▉ p50") " " @@ -504,6 +499,19 @@ (h2 "recent traces") (unquote (otel/-traces-list traces))))))) +(define otel/dashboard + (fn () + (quasiquote + (div :id "otel-dashboard" + (h1 "OpenTelemetry") + (p :style "font-size:0.8em;opacity:0.7" "live · refreshes every 3s in-app") + ;; STABLE polling element: it fetches GET /otel/fragment every 3s and swaps + ;; its OWN inner content (innerHTML), so #otel-body itself is never removed — + ;; its poll interval keeps firing (an outerHTML self-swap would delete the + ;; polling element after one tick). Boosted → text/sx, so no full reload. + (div :id "otel-body" :sx-get "/otel/fragment" :sx-trigger "every 3s" :sx-swap "innerHTML" + (unquote (otel/-dashboard-body))))))) + ;; ── routes ──────────────────────────────────────────────────────────── ;; Dual-mode, wired into the SPA: a boosted (SX-Request) fetch — a link click OR ;; the 3s poll — gets the dashboard as a text/sx fragment the WASM kernel renders @@ -515,11 +523,18 @@ (fn (req) (host/blog--resp req 200 (host/blog--page req "OpenTelemetry" (otel/dashboard)))))) +;; the poll target: always the refreshing body as a text/sx fragment (the WASM +;; kernel swaps it into #otel-body). Two segments, so /:slug can't shadow it. +(define otel/dashboard-fragment-route + (dream-get "/otel/fragment" + (fn (req) + (dream-response 200 {:content-type "text/sx; charset=utf-8"} + (serialize (otel/-dashboard-body)))))) (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)) +(define otel/routes (list otel/dashboard-route otel/dashboard-fragment-route otel/stream-route)) ;; ── P7: OTLP-JSON export ────────────────────────────────────────────── ;; Serialize spans to the OTLP/JSON schema (resourceSpans → scopeSpans → spans) diff --git a/lib/host/tests/otel.sx b/lib/host/tests/otel.sx index cb8fea3e..d0d9b9b7 100644 --- a/lib/host/tests/otel.sx +++ b/lib/host/tests/otel.sx @@ -235,7 +235,16 @@ (host-ot-test "dashboard SSRs the waterfall svg" (contains? host-ot-dash "