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 spanflags: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)
}