otel/instrument-routes wraps each flattened Dream route's handler in a timed
span named METHOD /route with {:http.method :http.route :http.status} attrs;
host/make-app applies it so every matched request becomes a trace. Refactored
with-span onto a shared otel/-timed core that takes a finalize fn for
result-derived attrs (the http.status only known post-handler).
137 lines
6.5 KiB
Plaintext
137 lines
6.5 KiB
Plaintext
;; lib/host/tests/otel.sx — P1: span model + API. A span dict
|
|
;; {:trace :span :parent :name :t0 :t1 :attrs :events}; otel/with-span records
|
|
;; t0/t1 and pushes/pops a dynamic parent stack so nesting builds the tree; a
|
|
;; bounded ring buffer (record!/recent, cap, drop-oldest); current-span/current-trace.
|
|
|
|
(define host-ot-pass 0)
|
|
(define host-ot-fail 0)
|
|
(define host-ot-fails (list))
|
|
|
|
(define
|
|
host-ot-test
|
|
(fn
|
|
(name actual expected)
|
|
(if
|
|
(= actual expected)
|
|
(set! host-ot-pass (+ host-ot-pass 1))
|
|
(begin
|
|
(set! host-ot-fail (+ host-ot-fail 1))
|
|
(append! host-ot-fails {:name name :actual actual :expected expected})))))
|
|
|
|
;; ── nested with-span builds the parent tree ─────────────────────────
|
|
(otel/reset!)
|
|
(otel/with-span "root" {}
|
|
(fn () (otel/with-span "child" {} (fn () 42))))
|
|
|
|
(define host-ot-sp (otel/recent))
|
|
(define host-ot-child (first host-ot-sp)) ;; inner span completes+records first
|
|
(define host-ot-root (nth host-ot-sp 1))
|
|
|
|
(host-ot-test "two spans recorded" (len host-ot-sp) 2)
|
|
(host-ot-test "child name" (get host-ot-child :name) "child")
|
|
(host-ot-test "root name" (get host-ot-root :name) "root")
|
|
(host-ot-test "child parent is root span" (get host-ot-child :parent) (get host-ot-root :span))
|
|
(host-ot-test "root has no parent" (get host-ot-root :parent) nil)
|
|
(host-ot-test "same trace" (= (get host-ot-child :trace) (get host-ot-root :trace)) true)
|
|
(host-ot-test "root t1 >= t0" (>= (get host-ot-root :t1) (get host-ot-root :t0)) true)
|
|
(host-ot-test "root has attrs" (get host-ot-root :attrs) {})
|
|
(host-ot-test "root has events list" (get host-ot-root :events) (list))
|
|
|
|
;; ── attrs are carried through ───────────────────────────────────────
|
|
(otel/reset!)
|
|
(otel/with-span "req" {:http.method "GET" :http.route "/feed"} (fn () nil))
|
|
(host-ot-test "attrs carried"
|
|
(get (get (first (otel/recent)) :attrs) :http.method) "GET")
|
|
|
|
;; ── current-span / current-trace track the dynamic stack ────────────
|
|
(otel/reset!)
|
|
(host-ot-test "no current span outside" (otel/current-span) nil)
|
|
(host-ot-test "no current trace outside" (otel/current-trace) nil)
|
|
(otel/with-span "x" {}
|
|
(fn ()
|
|
(begin
|
|
(host-ot-test "current span set inside" (not (= (otel/current-span) nil)) true)
|
|
(host-ot-test "current trace set inside" (not (= (otel/current-trace) nil)) true)
|
|
nil)))
|
|
(host-ot-test "no current span after" (otel/current-span) nil)
|
|
|
|
;; ── ring buffer caps at N, drops oldest ─────────────────────────────
|
|
(otel/reset!)
|
|
(otel/set-cap! 3)
|
|
(for-each (fn (i) (otel/record! {:span i :name "s"})) (list 1 2 3 4 5))
|
|
(host-ot-test "ring capped at 3" (len (otel/recent)) 3)
|
|
(host-ot-test "oldest dropped" (get (first (otel/recent)) :span) 3)
|
|
(host-ot-test "newest kept" (get (last (otel/recent)) :span) 5)
|
|
(otel/set-cap! 1000)
|
|
|
|
;; ── P2: now-ns wraps the host monotonic clock ──────────────────────
|
|
;; now-ns is real epoch NANOSECONDS (clock-milliseconds * 1e6), clamped so it
|
|
;; never goes backwards. Non-negative, non-decreasing, nanosecond-scale.
|
|
(define host-ot-n0 (otel/now-ns))
|
|
(define host-ot-n1 (otel/now-ns))
|
|
(host-ot-test "now-ns non-negative" (>= host-ot-n0 0) true)
|
|
(host-ot-test "now-ns monotonic non-decreasing" (>= host-ot-n1 host-ot-n0) true)
|
|
(host-ot-test "now-ns is nanosecond-scale" (> host-ot-n0 1000000000000000) true)
|
|
|
|
;; a real with-span straddles the host clock
|
|
(otel/reset!)
|
|
(otel/with-span "timed" {} (fn () nil))
|
|
(define host-ot-timed (first (otel/recent)))
|
|
(host-ot-test "timed span t1 >= t0" (>= (get host-ot-timed :t1) (get host-ot-timed :t0)) true)
|
|
(host-ot-test "timed span t0 nanosecond-scale" (> (get host-ot-timed :t0) 1000000000000000) true)
|
|
|
|
;; ── P3: auto-instrument handlers — a request becomes a trace ────────
|
|
;; otel/instrument-routes wraps each route's handler so dispatching a request
|
|
;; records a root span named "METHOD /route" with http.method/route/status attrs.
|
|
(otel/reset!)
|
|
(define host-ot-routes
|
|
(list
|
|
(dream-get "/feed" (fn (req) (dream-response 200 {} "ok")))
|
|
(dream-post "/feed" (fn (req) (dream-response 201 {} "made")))))
|
|
(define host-ot-iapp (dream-router (otel/instrument-routes host-ot-routes)))
|
|
(host-ot-iapp (dream-request "GET" "/feed" {} ""))
|
|
(host-ot-test "one span for one request" (len (otel/recent)) 1)
|
|
(define host-ot-is (first (otel/recent)))
|
|
(host-ot-test "span name is method+route" (get host-ot-is :name) "GET /feed")
|
|
(host-ot-test "http.method attr" (get (get host-ot-is :attrs) :http.method) "GET")
|
|
(host-ot-test "http.route attr" (get (get host-ot-is :attrs) :http.route) "/feed")
|
|
(host-ot-test "http.status attr" (get (get host-ot-is :attrs) :http.status) 200)
|
|
(host-ot-test "request span is a root (no parent)" (get host-ot-is :parent) nil)
|
|
(host-ot-test "request span has a trace id" (not (= (get host-ot-is :trace) nil)) true)
|
|
|
|
;; a second request → its own span + trace, status from its response
|
|
(host-ot-iapp (dream-request "POST" "/feed" {} "x"))
|
|
(host-ot-test "two requests two spans" (len (otel/recent)) 2)
|
|
(define host-ot-is2 (last (otel/recent)))
|
|
(host-ot-test "post span name" (get host-ot-is2 :name) "POST /feed")
|
|
(host-ot-test "post status attr" (get (get host-ot-is2 :attrs) :http.status) 201)
|
|
(host-ot-test "distinct trace per request"
|
|
(not (= (get host-ot-is :trace) (get host-ot-is2 :trace))) true)
|
|
|
|
;; bare-string handler results still get a status (coerced to 200)
|
|
(otel/reset!)
|
|
(define host-ot-sapp
|
|
(dream-router (otel/instrument-routes
|
|
(list (dream-get "/plain" (fn (req) "hello"))))))
|
|
(host-ot-sapp (dream-request "GET" "/plain" {} ""))
|
|
(host-ot-test "string handler status coerced to 200"
|
|
(get (get (first (otel/recent)) :attrs) :http.status) 200)
|
|
|
|
;; ── P3 integration: make-app traces every request ──────────────────
|
|
(otel/reset!)
|
|
(feed/reset!)
|
|
(define host-ot-happ (host/make-app (list host/feed-routes)))
|
|
(host-ot-happ (dream-request "GET" "/health" {} ""))
|
|
(define host-ot-hs (first (filter (fn (s) (= (get s :name) "GET /health")) (otel/recent))))
|
|
(host-ot-test "make-app traces the health request" (not (= host-ot-hs nil)) true)
|
|
(host-ot-test "make-app health status 200" (get (get host-ot-hs :attrs) :http.status) 200)
|
|
|
|
(define
|
|
host-ot-tests-run!
|
|
(fn
|
|
()
|
|
{:total (+ host-ot-pass host-ot-fail)
|
|
:passed host-ot-pass
|
|
:failed host-ot-fail
|
|
:fails host-ot-fails}))
|