Stack-based span tracing. Merged from kremis (context snapshots) and dark-lantern (thread-local stacks for Puma concurrency).
Every boundary call is wrapped in a span. Spans nest via a stack. The outermost span is the root. When the root completes, the full span tree is available for serialization, logging, or rendering into a trace panel.
include Wanderland::Tracing
trace("boundary:route-match", meta: { method: "GET", path: path }) do
record(:matched, true)
record(:pattern, "/node/:slug")
trace("sub-operation:compile-pattern") do
record(:param_count, 1)
end
end
Span.new(name:, parent:, meta:)
.id # UUID
.name # "boundary:route-match"
.parent # parent Span or nil
.children # Array[Span]
.records # Hash — key/value pairs recorded during the span
.meta # Hash — metadata set at span creation
.started_at # Time
.finished_at # Time
.duration # Float (seconds, rounded to 4 decimals)
.to_h # Serializes full tree to a hash (JSON-ready)
The span stack is thread-local via Thread.current[:wanderland_trace_stack]. Each Puma worker thread gets its own stack. Concurrent requests don't collide. Completed root spans are collected into a mutex-protected array.
From kremis: if you pass context: to trace, the tracer snapshots context.to_h before the block and records the full context + delta (added keys) on the span after. This is how pipeline stages record before/after state.
trace("stage:build", context: pipeline_context) do
pipeline_context.merge(image_tag: "my-image:abc123")
end
# span.records[:context] = full context after
# span.records[:added] = [:image_tag]
tracer = Wanderland.tracer
tracer.trace(name, meta: {}, context: nil, &block) # open span
tracer.record(key, value) # record on current span
tracer.complete_span # manually close (auto if block)
tracer.current_span # top of stack
tracer.active? # any open spans?
tracer.current_address # "parent/child/grandchild"
tracer.completed # all completed root spans
tracer.last_completed # most recent root span
tracer.reset! # clear (for testing)
tracer.write(output_dir) # write last root to JSON file
span.to_h produces a nested hash suitable for JSON.pretty_generate. Times are ISO8601. Duration is seconds. Children nest recursively. This is the same format the trace panel renders in the browser.