tracing

package
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

README

Tracing Package

OpenTelemetry distributed tracing for ResolveSpec.

Quick Start

import "github.com/bitechdev/ResolveSpec/pkg/tracing"

// Initialize tracer
config := tracing.Config{
    ServiceName:    "my-api",
    ServiceVersion: "1.0.0",
    Endpoint:       "localhost:4317", // OTLP collector
    Enabled:        true,
}

shutdown, err := tracing.InitTracer(config)
if err != nil {
    log.Fatal(err)
}
defer shutdown(context.Background())

// Apply middleware
router.Use(tracing.Middleware)

Configuration

type Config struct {
    ServiceName    string  // Service identifier
    ServiceVersion string  // Version for tracking deployments
    Endpoint       string  // OTLP collector endpoint (e.g., "localhost:4317")
    Enabled        bool    // Enable/disable tracing
    Insecure       bool    // Plaintext gRPC export (default: TLS)
    TLSConfig      *tls.Config      // Optional TLS customisation
    Headers        map[string]string // OTLP auth headers
    SampleRate     float64 // Root-trace sampling fraction (default 0.1)
}
Environment-based Configuration
import "os"

config := tracing.Config{
    ServiceName:    os.Getenv("SERVICE_NAME"),
    ServiceVersion: os.Getenv("VERSION"),
    Endpoint:       getEnv("OTEL_ENDPOINT", "localhost:4317"),
    Enabled:        getEnv("TRACING_ENABLED", "true") == "true",
}

Automatic HTTP Tracing

The middleware automatically creates spans for all HTTP requests:

router.Use(tracing.Middleware)

Captured attributes:

  • HTTP method
  • HTTP URL
  • HTTP path
  • HTTP scheme
  • Host name
  • Span kind (server)

Manual Span Creation

Basic Span
import "go.opentelemetry.io/otel/attribute"

func processOrder(ctx context.Context, orderID string) error {
    ctx, span := tracing.StartSpan(ctx, "process-order",
        attribute.String("order.id", orderID),
    )
    defer span.End()

    // Your logic here...
    return nil
}
Nested Spans
func handleRequest(ctx context.Context) error {
    ctx, span := tracing.StartSpan(ctx, "handle-request")
    defer span.End()

    // Child span 1
    if err := validateInput(ctx); err != nil {
        return err
    }

    // Child span 2
    if err := processData(ctx); err != nil {
        return err
    }

    return nil
}

func validateInput(ctx context.Context) error {
    ctx, span := tracing.StartSpan(ctx, "validate-input")
    defer span.End()

    // Validation logic...
    return nil
}

func processData(ctx context.Context) error {
    ctx, span := tracing.StartSpan(ctx, "process-data")
    defer span.End()

    // Processing logic...
    return nil
}

Adding Attributes

import "go.opentelemetry.io/otel/attribute"

ctx, span := tracing.StartSpan(ctx, "database-query",
    attribute.String("db.table", "users"),
    attribute.String("db.operation", "SELECT"),
    attribute.Int("user.id", 123),
)
defer span.End()

Or add attributes later:

tracing.SetAttributes(ctx,
    attribute.String("result.status", "success"),
    attribute.Int("result.count", 42),
)

Recording Events

tracing.AddEvent(ctx, "cache-miss",
    attribute.String("cache.key", cacheKey),
)

tracing.AddEvent(ctx, "retry-attempt",
    attribute.Int("attempt", 2),
    attribute.String("reason", "timeout"),
)

Error Recording

result, err := someOperation()
if err != nil {
    tracing.RecordError(ctx, err)
    return err
}

With additional context:

if err != nil {
    span := tracing.SpanFromContext(ctx)
    span.RecordError(err)
    span.SetAttributes(
        attribute.String("error.type", "database"),
        attribute.Bool("error.retriable", true),
    )
    return err
}

Complete Example

package main

import (
    "context"
    "database/sql"
    "log"
    "net/http"
    "time"

    "github.com/bitechdev/ResolveSpec/pkg/tracing"
    "github.com/gorilla/mux"
    "go.opentelemetry.io/otel/attribute"
)

func main() {
    // Initialize tracing
    config := tracing.Config{
        ServiceName:    "user-service",
        ServiceVersion: "1.0.0",
        Endpoint:       "localhost:4317",
        Enabled:        true,
    }

    shutdown, err := tracing.InitTracer(config)
    if err != nil {
        log.Fatal(err)
    }
    defer shutdown(context.Background())

    // Create router
    router := mux.NewRouter()

    // Apply tracing middleware
    router.Use(tracing.Middleware)

    // Routes
    router.HandleFunc("/users/{id}", getUserHandler)

    log.Fatal(http.ListenAndServe(":8080", router))
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Extract user ID from path
    vars := mux.Vars(r)
    userID := vars["id"]

    // Create span for this operation
    ctx, span := tracing.StartSpan(ctx, "get-user",
        attribute.String("user.id", userID),
    )
    defer span.End()

    // Fetch user
    user, err := fetchUser(ctx, userID)
    if err != nil {
        tracing.RecordError(ctx, err)
        http.Error(w, "Internal Server Error", 500)
        return
    }

    // Record success
    tracing.SetAttributes(ctx,
        attribute.String("user.name", user.Name),
        attribute.Bool("user.active", user.Active),
    )

    // Return user...
}

func fetchUser(ctx context.Context, userID string) (*User, error) {
    // Create database span
    ctx, span := tracing.StartSpan(ctx, "db.query",
        attribute.String("db.system", "postgresql"),
        attribute.String("db.operation", "SELECT"),
        attribute.String("db.table", "users"),
    )
    defer span.End()

    start := time.Now()

    // Execute query
    user, err := queryUser(ctx, userID)

    // Record duration
    duration := time.Since(start)
    span.SetAttributes(
        attribute.Int64("db.duration_ms", duration.Milliseconds()),
    )

    if err != nil {
        tracing.RecordError(ctx, err)
        return nil, err
    }

    return user, nil
}

OpenTelemetry Collector Setup

Docker Compose
version: '3'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - OTEL_ENDPOINT=otel-collector:4317
    depends_on:
      - otel-collector

  otel-collector:
    image: otel/opentelemetry-collector:latest
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Jaeger UI
      - "14250:14250" # Jaeger gRPC
Collector Configuration

otel-collector-config.yaml:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true

  logging:
    loglevel: debug

processors:
  batch:
    timeout: 10s

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger, logging]

Viewing Traces

Jaeger UI

Access at http://localhost:16686

Finding traces:

  1. Select service: "my-api"
  2. Select operation: "GET /users/:id"
  3. Click "Find Traces"
Sample Trace
GET /users/123 (200ms)
├── get-user (180ms)
│   ├── validate-permissions (20ms)
│   ├── db.query (150ms)
│   │   └── SELECT FROM users WHERE id = 123
│   └── transform-response (10ms)
└── send-response (20ms)

Best Practices

1. Span Naming

Good:

tracing.StartSpan(ctx, "database.query.users")
tracing.StartSpan(ctx, "http.request.external-api")
tracing.StartSpan(ctx, "cache.get")

Bad:

tracing.StartSpan(ctx, "DoStuff")           // Too vague
tracing.StartSpan(ctx, "user_123_query")     // User-specific (high cardinality)
2. Attribute Keys

Follow OpenTelemetry semantic conventions:

// HTTP
attribute.String("http.method", "GET")
attribute.String("http.url", url)
attribute.Int("http.status_code", 200)

// Database
attribute.String("db.system", "postgresql")
attribute.String("db.table", "users")
attribute.String("db.operation", "SELECT")

// Custom
attribute.String("user.id", userID)
attribute.String("order.status", "pending")
3. Error Handling

Always record errors:

if err != nil {
    tracing.RecordError(ctx, err)
    // Also add context
    tracing.SetAttributes(ctx,
        attribute.Bool("error.retriable", isRetriable(err)),
        attribute.String("error.type", errorType(err)),
    )
    return err
}
4. Sampling

For high-traffic services, configure sampling:

// Default is ParentBased(TraceIDRatioBased(0.1)); set Config.SampleRate (0-1) to change.
// Query strings are never exported; span names use the matched route pattern
// (Request.Pattern, or MiddlewareWithRoute for other routers). Install the
// middleware inside the panic-recovery middleware.
5. Context Propagation

Always pass context through the call chain:

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()  // Get context from request
    processRequest(ctx) // Pass it down
}

func processRequest(ctx context.Context) {
    // Context carries trace information
    ctx, span := tracing.StartSpan(ctx, "process")
    defer span.End()

    // Pass to next function
    saveData(ctx)
}

Performance Impact

  • Overhead: <1% CPU, <5MB memory
  • Latency: <100μs per span
  • Safe for production at high throughput

Troubleshooting

Traces Not Appearing
  1. Check collector is running:

    podman compose ps
    
  2. Verify endpoint:

    Endpoint: "localhost:4317"  // Correct
    Endpoint: "http://localhost:4317"  // Wrong (no http://)
    
  3. Check logs:

    podman compose logs otel-collector
    
Disable Tracing
config := tracing.Config{
    Enabled: false, // Tracing disabled
}
TLS in Production

Update tracing.go line with TLS credentials:

client := otlptracegrpc.NewClient(
    otlptracegrpc.WithEndpoint(config.Endpoint),
    otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")),
)

Integration with Metrics

Combine with metrics for full observability:

import (
    "github.com/bitechdev/ResolveSpec/pkg/metrics"
    "github.com/bitechdev/ResolveSpec/pkg/tracing"
)

// Apply both
router.Use(metrics.GetProvider().Middleware)
router.Use(tracing.Middleware)

Distributed Tracing

Traces automatically propagate across services via HTTP headers:

Service A:

// Create request with trace context
req, _ := http.NewRequestWithContext(ctx, "GET", "http://service-b/api", nil)
resp, _ := client.Do(req)

Service B:

// Trace context automatically extracted by middleware
router.Use(tracing.Middleware)

The trace ID propagates across both services, creating a unified trace.

Documentation

Index

Constants

View Source
const (
	// DefaultSampleRate is used when Config.SampleRate is zero.
	DefaultSampleRate = 0.1
	// DefaultAttributeValueLimit caps the length of any span attribute value.
	DefaultAttributeValueLimit = 1024
	// DefaultInitTimeout bounds exporter and resource creation.
	DefaultInitTimeout = 10 * time.Second
)

Variables

This section is empty.

Functions

func AddEvent

func AddEvent(ctx context.Context, name string, attrs ...attribute.KeyValue)

AddEvent adds an event to the current span

func InitTracer

func InitTracer(config Config) (func(context.Context) error, error)

InitTracer initializes the OpenTelemetry tracer using a background context.

func InitTracerContext added in v1.2.0

func InitTracerContext(ctx context.Context, config Config) (func(context.Context) error, error)

InitTracerContext initializes the OpenTelemetry tracer. ctx bounds startup work. Calling it again while a tracer is active returns an error; call the returned shutdown function first.

func Middleware

func Middleware(next http.Handler) http.Handler

Middleware returns an HTTP middleware that creates spans for requests. The span name uses the matched route pattern (http.ServeMux's Request.Pattern); use MiddlewareWithRoute for other routers. It should be installed inside the panic recovery middleware: panics are recorded on the span and re-raised.

func MiddlewareWithRoute added in v1.2.0

func MiddlewareWithRoute(fn func(*http.Request) string) func(http.Handler) http.Handler

MiddlewareWithRoute is like Middleware but resolves the low-cardinality route template with fn, called after the handler ran (so the router has matched). A nil fn falls back to Request.Pattern.

func RecordError

func RecordError(ctx context.Context, err error)

RecordError records an error on the current span

func SetAttributes

func SetAttributes(ctx context.Context, attrs ...attribute.KeyValue)

SetAttributes sets attributes on the current span

func SpanFromContext

func SpanFromContext(ctx context.Context) trace.Span

SpanFromContext returns the current span from the context

func StartSpan

func StartSpan(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span)

StartSpan starts a new span with the given name

Types

type Config

type Config struct {
	ServiceName    string
	ServiceVersion string
	Endpoint       string // OTLP endpoint (e.g., "localhost:4317")
	Enabled        bool

	// Insecure exports traces over plaintext gRPC. TLS is used unless this is set.
	Insecure bool
	// TLSConfig customises TLS for the exporter. Nil uses the system roots. Ignored when Insecure.
	TLSConfig *tls.Config
	// Headers are sent with every export request (e.g. an authorization token).
	Headers map[string]string

	// SampleRate is the fraction of new root traces sampled (0 < rate <= 1).
	// Zero selects DefaultSampleRate. Upstream sampling decisions are respected.
	SampleRate float64
	// AttributeValueLimit caps span attribute value length. Zero selects DefaultAttributeValueLimit.
	AttributeValueLimit int
	// InitTimeout bounds exporter/resource creation. Zero selects DefaultInitTimeout.
	InitTimeout time.Duration
}

Config holds tracing configuration

Jump to

Keyboard shortcuts

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