Frontend Labs
⋯

Day 109 — Observability: Traces & Spans

Learning objective Implement W3C Trace Context propagation with in-memory span recording
Key packages net/http, context, encoding/hex, crypto/rand, encoding/json, sync
Security context Distributed tracing exposes security middleware latency per layer — required for SLA debugging

Key Concepts

W3C Trace Context traceparent header format:

traceparent: 00-{traceId 32 hex}-{parentId 16 hex}-{flags 2 hex}
             00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
  • traceId: 128-bit random ID (same across entire request chain)
  • parentId: 64-bit ID for current span
  • flags: 01 = sampled

Exercise — Manual Tracer with Ring Buffer

package tracing

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

type contextKey int

const traceKey contextKey = 0

type SpanContext struct {
    TraceID  string
    SpanID   string
    ParentID string
    Sampled  bool
}

type Span struct {
    SpanContext
    Name       string
    StartTime  time.Time
    Duration   time.Duration
    Attributes map[string]string
    Error      string
}

func newID(n int) string {
    b := make([]byte, n)
    rand.Read(b)
    return hex.EncodeToString(b)
}

// Tracer records spans in a ring buffer
type Tracer struct {
    mu      sync.Mutex
    spans   []*Span
    head    int
    size    int
    sampleRate float64 // 0.0–1.0
}

func NewTracer(bufferSize int, sampleRate float64) *Tracer {
    return &Tracer{
        spans:      make([]*Span, bufferSize),
        size:       bufferSize,
        sampleRate: sampleRate,
    }
}

func (t *Tracer) record(s *Span) {
    t.mu.Lock()
    t.spans[t.head%t.size] = s
    t.head++
    t.mu.Unlock()
}

// StartSpan creates a new span, returning a finish function
func (t *Tracer) StartSpan(ctx context.Context, name string) (context.Context, func(attrs map[string]string, errMsg string)) {
    parent, _ := ctx.Value(traceKey).(*SpanContext)

    span := &Span{
        Name:       name,
        StartTime:  time.Now(),
        Attributes: make(map[string]string),
    }

    if parent != nil {
        span.TraceID = parent.TraceID
        span.ParentID = parent.SpanID
        span.Sampled = parent.Sampled
    } else {
        span.TraceID = newID(16)
        span.Sampled = true // simplification: sample all root spans
    }
    span.SpanID = newID(8)

    childCtx := context.WithValue(ctx, traceKey, &span.SpanContext)

    finish := func(attrs map[string]string, errMsg string) {
        span.Duration = time.Since(span.StartTime)
        for k, v := range attrs {
            span.Attributes[k] = v
        }
        if errMsg != "" {
            span.Error = errMsg
        }
        if span.Sampled {
            t.record(span)
        }
    }

    return childCtx, finish
}

// Middleware injects/propagates trace context
func (t *Tracer) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var sc *SpanContext

        // Try to propagate incoming traceparent
        tp := r.Header.Get("Traceparent")
        if tp != "" {
            sc = parseTraceparent(tp)
        }

        if sc == nil {
            sc = &SpanContext{
                TraceID: newID(16),
                SpanID:  newID(8),
                Sampled: true,
            }
        }

        ctx := context.WithValue(r.Context(), traceKey, sc)
        r = r.WithContext(ctx)

        // Set traceparent on response for downstream propagation
        w.Header().Set("Traceparent", formatTraceparent(sc))

        ctx, finish := t.StartSpan(ctx, "http.request")
        defer finish(map[string]string{
            "http.method": r.Method,
            "http.path":   r.URL.Path,
        }, "")
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

func parseTraceparent(tp string) *SpanContext {
    // 00-{32hex}-{16hex}-{2hex}
    if len(tp) != 55 || tp[2] != '-' || tp[35] != '-' || tp[52] != '-' {
        return nil
    }
    return &SpanContext{
        TraceID:  tp[3:35],
        ParentID: tp[36:52],
        Sampled:  tp[53:55] == "01",
    }
}

func formatTraceparent(sc *SpanContext) string {
    flags := "00"
    if sc.Sampled {
        flags = "01"
    }
    spanID := sc.SpanID
    if spanID == "" {
        spanID = newID(8)
    }
    return fmt.Sprintf("00-%s-%s-%s", sc.TraceID, spanID, flags)
}

// TracesHandler serves recent spans at /debug/traces
func (t *Tracer) TracesHandler(w http.ResponseWriter, r *http.Request) {
    t.mu.Lock()
    result := make([]*Span, 0, t.size)
    for i := 0; i < t.size; i++ {
        idx := (t.head - 1 - i + t.size*2) % t.size
        if t.spans[idx] != nil {
            result = append(result, t.spans[idx])
        }
    }
    t.mu.Unlock()
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(result)
}

Generated from week15.md · 8 Oct 2026 16:36