tracez

package module
v0.0.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 2, 2025 License: MIT Imports: 10 Imported by: 1

README

tracez

CI Status codecov Go Report Card CodeQL Go Reference License Go Version Release

A minimal span collection library for Go applications - a building block for observability systems.

What tracez Actually Is

tracez collects spans within your Go application for local performance analysis or export to APM systems. It's a primitive - the foundation you build observability on, not a complete tracing solution.

What span collection enables:

  • Feed APM systems (Datadog, New Relic, Jaeger) with performance data
  • Local performance analysis during development
  • Identify slow operations and bottlenecks
  • Understand code execution paths
  • Measure resource usage patterns

tracez is NOT:

  • Distributed tracing (no cross-service correlation)
  • An APM system (no UI, no analysis tools)
  • A metrics system (spans only, not counters/gauges)
  • A logging framework (structured performance data only)

When to Use tracez

Use tracez when building:

  • Custom APM integrations
  • Performance monitoring tools
  • Development profiling utilities
  • Lightweight observability for libraries
  • Systems where you control the entire span pipeline

Use OpenTelemetry instead when:

  • You need actual distributed tracing across services
  • Vendor-specific integrations are required
  • Automatic instrumentation is needed
  • Standards compliance (W3C Trace Context) is critical
  • Cross-process correlation is required
  • You want a complete solution, not a building block

Core Features

  • Minimal Dependencies: Standard library only
  • Thread-Safe: Safe concurrent operations across goroutines
  • Zero Buffering: Direct callback execution (no memory overhead)
  • High Performance: 1.84M spans/sec single-threaded, 3.92M spans/sec parallel
  • Context Propagation: Parent-child relationships within your process
  • Memory Efficient: No internal buffering or queuing

Quick Start

package main

import (
    "context"
    "fmt"
    "github.com/zoobzio/tracez"
)

func main() {
    // Create tracer for your library/component
    tracer := tracez.New("auth-component")  // Component name, not service
    defer tracer.Close()
    
    // Register callback for span completion
    tracer.OnSpanFinish(func(span tracez.Span) {
        // Send to your APM system
        sendToAPM(span)
    })
    
    // Collect performance data
    ctx, span := tracer.StartSpan(context.Background(), "validate-token")
    span.SetTag("token.type", "jwt")
    defer span.Finish()  // Triggers callback
    
    // Child spans track nested operations
    childCtx, childSpan := tracer.StartSpan(ctx, "database-lookup")
    childSpan.SetTag("query", "SELECT * FROM users WHERE token = ?")
    defer childSpan.Finish()  // Triggers callback
}

func sendToAPM(span tracez.Span) {
    // Your APM integration logic
    // Convert span to vendor format
    // Send to APM endpoint (consider batching)
}

Building Observability Systems

tracez provides primitives. You build the system:

Example: Local Development Profiler
// Collect spans during test runs
var spans []tracez.Span
var mu sync.Mutex

tracer.OnSpanFinish(func(span tracez.Span) {
    mu.Lock()
    spans = append(spans, span)
    mu.Unlock()
})

// Run your code...

// Analyze performance locally
mu.Lock()
analyzer := NewPerformanceAnalyzer(spans)
slowOps := analyzer.FindSlowOperations(100 * time.Millisecond)
fmt.Printf("Found %d slow operations\n", len(slowOps))
mu.Unlock()
Example: Production APM Integration
// Batch spans for APM export
type APMExporter struct {
    spans    []tracez.Span
    mu       sync.Mutex
    client   *http.Client
    endpoint string
}

func (e *APMExporter) CollectSpan(span tracez.Span) {
    e.mu.Lock()
    e.spans = append(e.spans, span)
    e.mu.Unlock()
}

func (e *APMExporter) Run(ctx context.Context) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            e.mu.Lock()
            if len(e.spans) > 0 {
                batch := e.spans
                e.spans = nil
                e.mu.Unlock()
                e.sendBatch(batch)
            } else {
                e.mu.Unlock()
            }
        case <-ctx.Done():
            return
        }
    }
}

// Register with tracer
exporter := &APMExporter{client: http.DefaultClient, endpoint: "..."}
tracer.OnSpanFinish(exporter.CollectSpan)

Components

Tracer

Manages span lifecycle within your Go application. One per library/component.

tracer := tracez.New("component-name")  // Not service name
ctx, span := tracer.StartSpan(context.Background(), "operation")
Span & ActiveSpan
  • Span: Immutable completed span data
  • ActiveSpan: Thread-safe wrapper for spans being recorded
span.SetTag("cache.hit", "true")     // Thread-safe
span.SetTag("cache.key", key)        // Concurrent safe
span.Finish()                         // Idempotent, triggers callbacks
Callbacks

Register functions to process spans on completion. Foundation for exporters.

// Single callback
tracer.OnSpanFinish(func(span tracez.Span) {
    // Process completed span
    exportToAPM(span)
})

// Multiple callbacks supported
tracer.OnSpanFinish(logSpan)
tracer.OnSpanFinish(metricCollector.Record)
tracer.OnSpanFinish(apmExporter.Send)

// Callbacks receive immutable span data
// Called synchronously on span.Finish()

Performance Characteristics

Measured with race detection enabled:

Operation Throughput Memory Allocations
Span Creation 1.84M/sec (single) 344 B/op 8 allocs
Span Creation 3.92M/sec (parallel) 344 B/op 8 allocs
Tag Addition - ~20 B/tag 1 alloc
Callback Execution Synchronous No overhead 0 allocs

Callbacks execute synchronously on span.Finish() with no buffering overhead.

Documentation

Learn the Primitives
Integration Examples

Architecture Principles

tracez follows visible complexity - no hidden behavior:

  • No Magic: No reflection, code generation, or hidden abstractions
  • Predictable: Linear performance, bounded memory
  • Testable: Every path has unit tests
  • Composable: Simple primitives build complex systems
Memory Management
  • No internal buffering or queuing
  • Direct callback execution
  • Immutable spans prevent reference leaks
  • Clean shutdown without goroutine leaks
Thread Safety
Component Safety Notes
Tracer ✅ Safe Concurrent span creation
Callbacks ✅ Safe Thread-safe registration
ActiveSpan ✅ Safe Concurrent tag operations
Span (completed) ❌ Immutable Read-only after finish

Installation

go get github.com/zoobzio/tracez

Requirements:

  • Go 1.21 or later
  • No external dependencies

Testing

# Run tests with race detection
make test

# Coverage report (95.9%)
make coverage

# Linting
make lint

# Full CI suite
make check

Contributing

See CONTRIBUTING.md for guidelines.

Quick start:

  1. Fork repository
  2. Create feature branch
  3. Write tests (maintain >95% coverage)
  4. Run make ci
  5. Submit pull request

License

MIT License - see LICENSE file.

Design Philosophy

tracez is a primitive, not a platform:

  1. Primitives Over Frameworks: Building blocks, not solutions
  2. Explicit Over Automatic: You control what happens
  3. Performance Over Features: Predictable resource usage
  4. Visibility Over Convenience: See how everything works
  5. Composition Over Configuration: Build what you need

This makes tracez ideal when you need to build custom observability solutions or integrate with specific APM systems without framework overhead.

Documentation

Overview

Package tracez provides a minimal, primitive distributed tracing library.

tracez focuses on span creation and processing without the complexity of full OpenTelemetry. It's designed for systems that need basic distributed tracing with predictable performance and zero memory overhead when unused.

Core Components:

  • Tracer: Manages span lifecycle and handlers.
  • Span: Represents a single unit of work.
  • ActiveSpan: Thread-safe wrapper for ongoing spans.
  • SpanHandler: Callback function invoked when spans complete.

Basic Usage:

tracer := tracez.New()
defer tracer.Close()

// Register a handler for completed spans.
tracer.OnSpanComplete(func(span Span) {
    log.Printf("%s: %v", span.Name, span.Duration)
})

// Start a new span.
ctx, span := tracer.StartSpan(ctx, "operation-name")
defer span.Finish()

// Add metadata.
span.SetTag("user.id", "123")

// Pass context to child operations.
childCtx, childSpan := tracer.StartSpan(ctx, "child-operation")
defer childSpan.Finish()

Thread Safety:

Tracer is safe for concurrent use by multiple goroutines. Handler registration/removal is thread-safe. ActiveSpan SetTag/GetTag operations are safe for concurrent use. Handlers receive immutable span copies, safe for any use.

Context Propagation:

Spans are automatically linked via context.Context. Child spans inherit their parent's TraceID and reference the parent's SpanID.

Memory Management:

Zero memory overhead when no handlers are registered. Handlers receive span copies by value to prevent data races. Optional worker pool for bounded async handler execution.

Resource Cleanup:

Call tracer.Close() to properly shut down worker pools and handlers.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ActiveSpan

type ActiveSpan struct {
	// contains filtered or unexported fields
}

ActiveSpan wraps a Span with thread-safe tag operations and lifecycle management. Safe for concurrent use by multiple goroutines.

func (*ActiveSpan) Context

func (a *ActiveSpan) Context(parent context.Context) context.Context

Context creates a new context with this span embedded. The returned context can be used to start child spans.

func (*ActiveSpan) Finish

func (a *ActiveSpan) Finish()

Finish completes the span and sends it to the tracer for collection. Safe to call multiple times - subsequent calls are no-ops.

func (*ActiveSpan) GetTag

func (a *ActiveSpan) GetTag(key Tag) (string, bool)

GetTag retrieves a tag value by key. Thread-safe for concurrent access.

func (*ActiveSpan) SetBoolTag added in v0.0.7

func (a *ActiveSpan) SetBoolTag(key Tag, value bool)

SetBoolTag adds a boolean key-value pair to the span. The boolean value is converted to a string for storage. Thread-safe for concurrent access. No-op if span is already finished.

func (*ActiveSpan) SetIntTag added in v0.0.7

func (a *ActiveSpan) SetIntTag(key Tag, value int)

SetIntTag adds an integer key-value pair to the span. The integer value is converted to a string for storage. Thread-safe for concurrent access. No-op if span is already finished.

func (*ActiveSpan) SetTag

func (a *ActiveSpan) SetTag(key Tag, value string)

SetTag adds a key-value pair to the span. Thread-safe for concurrent access. No-op if span is already finished.

func (*ActiveSpan) SpanID

func (a *ActiveSpan) SpanID() string

SpanID returns the span ID of this span. Thread-safe for concurrent access.

func (*ActiveSpan) TraceID

func (a *ActiveSpan) TraceID() string

TraceID returns the trace ID of this span. Thread-safe for concurrent access.

type IDPool

type IDPool struct {
	// contains filtered or unexported fields
}

IDPool manages a pool of pre-generated IDs to amortize crypto/rand overhead.

func NewIDPool

func NewIDPool(capacity int, factory func() string) *IDPool

NewIDPool creates a new ID pool with the specified capacity.

func (*IDPool) Close

func (p *IDPool) Close()

Close shuts down the ID pool gracefully.

func (*IDPool) Get

func (p *IDPool) Get() string

Get retrieves an ID from the pool or generates one if pool is empty.

type Key

type Key string

Key represents a span operation name.

type Span

type Span struct {
	Tags      map[Tag]string `json:"tags,omitempty"`
	StartTime time.Time      `json:"start_time"`
	EndTime   time.Time      `json:"end_time,omitempty"`
	Duration  time.Duration  `json:"duration"`
	TraceID   string         `json:"trace_id"`
	SpanID    string         `json:"span_id"`
	ParentID  string         `json:"parent_id,omitempty"`
	Name      string         `json:"name"`
}

Span represents a single unit of work in a distributed trace. Spans are NOT thread-safe - do not modify from multiple goroutines.

func GetSpan

func GetSpan(ctx context.Context) *Span

GetSpan extracts the current span from a context. Returns nil if no span is present.

type SpanHandler added in v0.0.6

type SpanHandler func(span Span)

SpanHandler is called when a span completes.

type Tag

type Tag = string

Tag represents a span tag key.

type Tracer

type Tracer struct {
	// contains filtered or unexported fields
}

Tracer manages span lifecycle and collection. Safe for concurrent use by multiple goroutines.

func New

func New() *Tracer

New creates a new tracer. Uses the real clock for production behavior.

func (*Tracer) Close

func (t *Tracer) Close()

Close shuts down the tracer gracefully and cleans up resources. This should be called when the tracer is no longer needed.

func (*Tracer) DroppedSpans added in v0.0.6

func (t *Tracer) DroppedSpans() uint64

DroppedSpans returns the number of spans dropped due to full worker queue.

func (*Tracer) EnableWorkerPool added in v0.0.6

func (t *Tracer) EnableWorkerPool(workers, queueSize int) error

EnableWorkerPool creates a bounded worker pool for async handlers.

func (*Tracer) HasHandlers added in v0.0.9

func (t *Tracer) HasHandlers() bool

HasHandlers returns true if any handlers are registered. Useful for checking if tracing is actively being collected.

func (*Tracer) OnSpanComplete added in v0.0.6

func (t *Tracer) OnSpanComplete(handler SpanHandler) uint64

OnSpanComplete registers a synchronous handler called when spans complete.

func (*Tracer) OnSpanCompleteAsync added in v0.0.6

func (t *Tracer) OnSpanCompleteAsync(handler SpanHandler) uint64

OnSpanCompleteAsync registers an asynchronous handler called when spans complete.

func (*Tracer) RemoveHandler added in v0.0.6

func (t *Tracer) RemoveHandler(id uint64)

RemoveHandler removes a handler by ID.

func (*Tracer) SetPanicHook added in v0.0.6

func (t *Tracer) SetPanicHook(hook func(handlerID uint64, r interface{}))

SetPanicHook sets a function to be called when a handler panics.

func (*Tracer) StartSpan

func (t *Tracer) StartSpan(ctx context.Context, operation Key) (context.Context, *ActiveSpan)

StartSpan creates a new span and returns it wrapped in an ActiveSpan. If the context contains an existing span, the new span will be its child. If no handlers are registered, returns a no-op span to avoid any overhead.

func (*Tracer) WithClock

func (*Tracer) WithClock(clock clockz.Clock) *Tracer

WithClock returns a new tracer with the specified clock. Enables clock injection for deterministic testing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL