otel P1: span model + API (with-span, parent stack, ring buffer)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 14:18:34 +00:00
parent 5535acf4e9
commit 087c01e890
3 changed files with 165 additions and 0 deletions

74
lib/host/tests/otel.sx Normal file
View File

@@ -0,0 +1,74 @@
;; 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)
(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}))