Wanderland

Wanderland Core: Tracing

Stack-based span tracing. Merged from kremis (context snapshots) and dark-lantern (thread-local stacks for Puma concurrency).

Pattern

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 Structure

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)

Thread Safety

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.

Context Snapshots

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 API

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

Serialization

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.