tracez

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Sep 20, 2025 License: MIT Imports: 8 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
  • Backpressure Protection: Drops spans when buffers full (prevents OOM)
  • High Performance: 1.84M spans/sec single-threaded, 3.92M spans/sec parallel
  • Context Propagation: Parent-child relationships within your process
  • Memory Efficient: Bounded growth with automatic buffer shrinking

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()
    
    // Add collector with buffer size
    collector := tracez.NewCollector("apm-exporter", 100)
    tracer.AddCollector("apm-exporter", collector)
    
    // Collect performance data
    ctx, span := tracer.StartSpan(context.Background(), "validate-token")
    span.SetTag("token.type", "jwt")
    defer span.Finish()
    
    // Child spans track nested operations
    childCtx, childSpan := tracer.StartSpan(ctx, "database-lookup")
    childSpan.SetTag("query", "SELECT * FROM users WHERE token = ?")
    defer childSpan.Finish()
    
    // Export spans for processing
    spans := collector.Export()
    
    // Feed to your APM system
    for _, span := range spans {
        // Send to Datadog, New Relic, Jaeger, etc.
        sendToAPM(span)
    }
}

func sendToAPM(span tracez.Span) {
    // Your APM integration logic
    // Convert span to vendor format
    // Batch and send to APM endpoint
}

Building Observability Systems

tracez provides primitives. You build the system:

Example: Local Development Profiler
// Collect spans during test runs
collector := tracez.NewCollector("profiler", 1000)
tracer.AddCollector("profiler", collector)

// Run your code...

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

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

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
Collector

Buffers spans for batch processing. Foundation for exporters.

collector := tracez.NewCollector("exporter-name", bufferSize)
tracer.AddCollector("exporter", collector)

// Get spans for processing (returns copy)
spans := collector.Export()

// Monitor health
dropped := collector.DroppedCount()
if dropped > 0 {
    log.Printf("Warning: dropped %d spans\n", dropped)
}

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
Export (1000 spans) 485K/sec Deep copy Batch alloc

Backpressure: Automatically drops spans when buffer full (configurable).

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
  • Automatic buffer shrinking after exports
  • Bounded growth with backpressure
  • Deep copies prevent reference leaks
  • Clean shutdown without goroutine leaks
Thread Safety
Component Safety Notes
Tracer ✅ Safe Concurrent span creation
Collector ✅ Safe Concurrent collection/export
ActiveSpan ✅ Safe Concurrent tag operations
Span (exported) ❌ Immutable Read-only after export

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 collection and export without the complexity of. full OpenTelemetry. It's designed for systems that need basic distributed tracing with predictable performance and resource usage.

Core Components:.

  • Tracer: Manages span lifecycle and collection.
  • Span: Represents a single unit of work.
  • ActiveSpan: Thread-safe wrapper for ongoing spans.
  • Collector: Buffers completed spans for export.

Basic Usage:.

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

// 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. Collectors are safe for concurrent span buffering. ActiveSpan SetTag/GetTag operations are safe for concurrent use.

Spans themselves are NOT thread-safe - do not modify the same. Span struct from multiple goroutines simultaneously.

Context Propagation:.

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

Memory Management:.

Collectors automatically manage memory by shrinking buffers after. export operations. Under high load, spans may be dropped to prevent memory exhaustion - use Collector.DroppedCount() to monitor.

Resource Cleanup:.

Call tracer.Close() to properly shut down all background goroutines. Call tracer.Reset() to clear all collectors and spans.

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) 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 Collector

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

Collector buffers completed spans for batch export. Safe for concurrent use by multiple goroutines.

func NewCollector

func NewCollector(name string, bufferSize int) *Collector

NewCollector creates a new collector with the specified name and buffer size. Uses the real clock for production behavior.

func (*Collector) Collect

func (c *Collector) Collect(span *Span)

Collect attempts to buffer a span with backpressure protection. If the internal channel is full, the span is dropped and the drop counter is incremented. In sync mode, spans are collected directly for deterministic testing.

func (*Collector) Count

func (c *Collector) Count() int

Count returns the current number of buffered spans.

func (*Collector) DroppedCount

func (c *Collector) DroppedCount() int64

DroppedCount returns the total number of spans dropped due to backpressure.

func (*Collector) Export

func (c *Collector) Export() []Span

Export returns a copy of all buffered spans and clears the internal buffer. The returned slice is safe to modify without affecting the collector.

func (*Collector) Reset

func (c *Collector) Reset()

Reset clears all buffered spans and resets the drop counter. Does not affect the running goroutine - use close() for that.

func (*Collector) SetSyncMode

func (c *Collector) SetSyncMode(sync bool)

SetSyncMode enables synchronous collection for testing. When enabled, spans are collected directly without using the channel. This makes tests deterministic by eliminating async behavior.

func (*Collector) WithClock

func (c *Collector) WithClock(clock clockz.Clock) *Collector

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

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 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) AddCollector

func (t *Tracer) AddCollector(name string, collector *Collector)

AddCollector registers a new collector with the tracer. Users must track collector names themselves if needed.

func (*Tracer) Close

func (t *Tracer) Close()

Close shuts down all collectors gracefully and cleans up ID pools. This should be called when the tracer is no longer needed.

func (*Tracer) Reset

func (t *Tracer) Reset()

Reset clears all collectors' buffers without destroying them. The collectors remain registered and their goroutines continue running.

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.

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