The Rivaas Tracing package provides OpenTelemetry-based distributed tracing. Supports various exporters and integrates with HTTP frameworks. Enables observability best practices with minimal configuration.
Features
OpenTelemetry Integration: Full OpenTelemetry tracing support
Context Propagation: Automatic trace context propagation across services
Span Management: Easy span creation and management with lifecycle hooks
HTTP Middleware: Standalone middleware for any HTTP framework
Multiple Providers: Stdout, OTLP (gRPC and HTTP), and Noop exporters
Path Filtering: Exclude specific paths from tracing via middleware options
Consistent API: Same design patterns as the metrics package
Thread-Safe: All operations safe for concurrent use
Quick Start
packagemainimport("context""log""os/signal""rivaas.dev/tracing")funcmain(){ctx,cancel:=signal.NotifyContext(context.Background(),os.Interrupt)defercancel()tracer,err:=tracing.New(tracing.WithServiceName("my-service"),tracing.WithServiceVersion("v1.0.0"),tracing.WithOTLP("localhost:4317"),)iferr!=nil{log.Fatal(err)}iferr:=tracer.Start(ctx);err!=nil{log.Fatal(err)}defertracer.Shutdown(context.Background())// Traces exported via OTLP gRPCctx,span:=tracer.StartSpan(ctx,"operation")defertracer.FinishSpan(span,200)}
packagemainimport("context""log""os/signal""rivaas.dev/tracing")funcmain(){ctx,cancel:=signal.NotifyContext(context.Background(),os.Interrupt)defercancel()tracer,err:=tracing.New(tracing.WithServiceName("my-service"),tracing.WithServiceVersion("v1.0.0"),tracing.WithOTLPHTTP("http://localhost:4318"),)iferr!=nil{log.Fatal(err)}iferr:=tracer.Start(ctx);err!=nil{log.Fatal(err)}defertracer.Shutdown(context.Background())// Traces exported via OTLP HTTPctx,span:=tracer.StartSpan(ctx,"operation")defertracer.FinishSpan(span,200)}
packagemainimport("context""rivaas.dev/tracing")funcmain(){tracer:=tracing.MustNew(tracing.WithServiceName("my-service"),tracing.WithStdout(),)defertracer.Shutdown(context.Background())ctx:=context.Background()// Traces printed to stdoutctx,span:=tracer.StartSpan(ctx,"operation")defertracer.FinishSpan(span,200)}
How It Works
Providers determine where traces are exported (Stdout, OTLP, Noop)
Lifecycle management ensures proper initialization and graceful shutdown
HTTP middleware creates spans for requests automatically
Custom spans can be created for detailed operation tracing
Context propagation enables distributed tracing across services
Learning Path
Follow these guides to learn distributed tracing with Rivaas:
Installation - Get started with the tracing package
Basic Usage - Learn tracer creation and span management
Providers - Understand Stdout, OTLP, and Noop exporters
Configuration - Configure service metadata, sampling, and hooks
Middleware - Integrate HTTP tracing with your application
Learn the fundamentals of creating tracers and managing spans
Learn how to create tracers, manage spans, and add tracing to your Go applications.
Creating a Tracer
The Tracer is the main entry point for distributed tracing. Create one using functional options:
With Error Handling
tracer,err:=tracing.New(tracing.WithServiceName("my-service"),tracing.WithServiceVersion("v1.0.0"),tracing.WithStdout(),)iferr!=nil{log.Fatalf("Failed to create tracer: %v",err)}defertracer.Shutdown(context.Background())
Panic on Error
For convenience, use MustNew which panics if initialization fails:
For OTLP providers (gRPC and HTTP), you must call Start() before tracing:
tracer:=tracing.MustNew(tracing.WithServiceName("my-service"),tracing.WithOTLP("localhost:4317"),)// Start is required for OTLP providersiferr:=tracer.Start(context.Background());err!=nil{log.Fatal(err)}defertracer.Shutdown(context.Background())
For Stdout and Noop providers, `Start()` is optional (they initialize immediately in `New()`).
Shutting Down
Always shut down the tracer to flush pending spans:
deferfunc(){ctx,cancel:=context.WithTimeout(context.Background(),5*time.Second)defercancel()iferr:=tracer.Shutdown(ctx);err!=nil{log.Printf("Error shutting down tracer: %v",err)}}()
Manual Span Management
Create and manage spans manually for detailed tracing:
Basic Span Creation
funcprocessData(ctxcontext.Context,tracer*tracing.Tracer){// Start a spanctx,span:=tracer.StartSpan(ctx,"process-data")defertracer.FinishSpan(span,http.StatusOK)// Your code here...}
Adding Attributes
Add attributes to provide context about the operation:
ctx,span:=tracer.StartSpan(ctx,"database-query")defertracer.FinishSpan(span,http.StatusOK)// Add attributestracer.SetSpanAttribute(span,"db.system","postgresql")tracer.SetSpanAttribute(span,"db.query","SELECT * FROM users")tracer.SetSpanAttribute(span,"db.rows_returned",42)
Supported attribute types:
string
int, int64
float64
bool
Other types (converted to string)
Adding Events
Record significant moments in a span’s lifetime:
import"go.opentelemetry.io/otel/attribute"ctx,span:=tracer.StartSpan(ctx,"cache-lookup")defertracer.FinishSpan(span,http.StatusOK)// Add an eventtracer.AddSpanEvent(span,"cache_hit",attribute.String("key","user:123"),attribute.Int("ttl_seconds",300),)
Error Handling
Use the status code to indicate span success or failure:
funcfetchUser(ctxcontext.Context,tracer*tracing.Tracer,userIDstring)error{ctx,span:=tracer.StartSpan(ctx,"fetch-user")deferfunc(){iferr!=nil{tracer.FinishSpan(span,http.StatusInternalServerError)}else{tracer.FinishSpan(span,http.StatusOK)}}()tracer.SetSpanAttribute(span,"user.id",userID)// Fetch user logic...returnnil}
Context Helpers
Work with spans through the context without direct span references:
Set Attributes from Context
funchandleRequest(ctxcontext.Context){// Add attribute to the current span in contexttracing.SetSpanAttributeFromContext(ctx,"user.role","admin")tracing.SetSpanAttributeFromContext(ctx,"user.id",12345)}
Add Events from Context
funcprocessEvent(ctxcontext.Context){// Add event to the current span in contexttracing.AddSpanEventFromContext(ctx,"event_processed",attribute.String("event_type","user_login"),attribute.String("ip_address","192.168.1.1"),)}
Use defer to ensure spans are finished even if errors occur:
ctx,span:=tracer.StartSpan(ctx,"operation")defertracer.FinishSpan(span,http.StatusOK)// Always close
Propagate Context
Always pass the context returned by StartSpan to child operations:
ctx,span:=tracer.StartSpan(ctx,"parent")defertracer.FinishSpan(span,http.StatusOK)// Pass the new context to childrenchildOperation(ctx)// ✓ CorrectchildOperation(oldCtx)// ✗ Wrong - breaks trace chain
No persistence: Traces are only printed, not stored
No visualization: Use an actual backend for trace visualization
OTLP Provider (gRPC)
The OTLP gRPC provider exports traces to an OpenTelemetry collector using the gRPC protocol.
When to Use
Production environments
OpenTelemetry collector infrastructure
Jaeger, Zipkin, or other OTLP-compatible backends
Best performance and reliability
Basic Configuration
tracer:=tracing.MustNew(tracing.WithServiceName("my-service"),tracing.WithServiceVersion("v1.0.0"),tracing.WithOTLP("localhost:4317"),)// Start is required for OTLP providersiferr:=tracer.Start(context.Background());err!=nil{log.Fatal(err)}defertracer.Shutdown(context.Background())
Secure Connection (TLS)
By default, OTLP uses TLS:
tracer:=tracing.MustNew(tracing.WithServiceName("my-service"),tracing.WithOTLP("collector.example.com:4317"),// TLS is enabled by default)
The OTLP HTTP provider exports traces to an OpenTelemetry collector using the HTTP protocol.
When to Use
Alternative to gRPC when firewalls block gRPC
Simpler infrastructure without gRPC support
HTTP-only environments
Debugging with curl/httpie
Configuration
tracer:=tracing.MustNew(tracing.WithServiceName("my-service"),tracing.WithServiceVersion("v1.0.0"),tracing.WithOTLPHTTP("http://localhost:4318"),)// Start is required for OTLP providersiferr:=tracer.Start(context.Background());err!=nil{log.Fatal(err)}defertracer.Shutdown(context.Background())
// HTTP (insecure - development only)tracing.WithOTLPHTTP("http://localhost:4318")// HTTPS (secure - production)tracing.WithOTLPHTTP("https://collector.example.com:4318")
Provider Comparison
Performance
Provider
Latency
Throughput
CPU
Memory
Noop
~10ns
Unlimited
Minimal
Minimal
Stdout
~100µs
Low
Low
Low
OTLP (gRPC)
~1-2ms
High
Low
Medium
OTLP (HTTP)
~2-3ms
Medium
Low
Medium
Use Case Matrix
// Developmenttracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithStdout(),// ← See traces in console)// Testingtracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithNoop(),// ← No tracing overhead)// Production (recommended)tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithOTLP("collector:4317"),// ← gRPC to collector)// Production (HTTP alternative)tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithOTLPHTTP("https://collector:4318"),// ← HTTP to collector)
Switching Providers
Only one provider can be configured at a time. Attempting to configure multiple providers results in a validation error:
Control which requests are traced to reduce overhead and costs.
Sample Rate
Set the percentage of requests to trace (0.0 to 1.0):
tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSampleRate(0.1),// Trace 10% of requeststracing.WithOTLP("collector:4317"),)
Sample rates:
1.0: 100% sampling. All requests traced.
0.5: 50% sampling.
0.1: 10% sampling.
0.01: 1% sampling.
0.0: 0% sampling (no traces)
Sampling Examples
// Development: trace everythingtracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSampleRate(1.0),tracing.WithStdout(),)// Production: trace 10% of requeststracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSampleRate(0.1),tracing.WithOTLP("collector:4317"),)// High-traffic: trace 1% of requeststracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSampleRate(0.01),tracing.WithOTLP("collector:4317"),)
Sampling Behavior
Probabilistic: Uses deterministic hashing for consistent sampling
Request-level: Decision made once per request, all child spans included
Zero overhead: Non-sampled requests skip span creation entirely
When to Sample
Traffic Level
Recommended Sample Rate
< 100 req/s
1.0 (100%)
100-1000 req/s
0.5 (50%)
1000-10000 req/s
0.1 (10%)
> 10000 req/s
0.01 (1%)
Adjust based on:
Trace backend capacity
Storage costs
Desired trace coverage
Debug vs production needs
Span Lifecycle Hooks
Add custom logic when spans start or finish.
Span Start Hook
Execute code when a request span is created:
import("context""net/http""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/trace""rivaas.dev/tracing")startHook:=func(ctxcontext.Context,spantrace.Span,req*http.Request){// Add custom attributesiftenantID:=req.Header.Get("X-Tenant-ID");tenantID!=""{span.SetAttributes(attribute.String("tenant.id",tenantID))}// Add user informationifuserID:=req.Header.Get("X-User-ID");userID!=""{span.SetAttributes(attribute.String("user.id",userID))}// Record custom business contextspan.SetAttributes(attribute.String("request.region",getRegionFromIP(req)),attribute.Bool("request.is_mobile",isMobileRequest(req)),)}tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSpanStartHook(startHook),tracing.WithOTLP("collector:4317"),)
Use cases:
Add tenant/user identifiers
Record business context
Integrate with feature flags
Custom sampling decisions
APM tool integration
Span Finish Hook
Execute code when a request span completes:
import("go.opentelemetry.io/otel/trace""rivaas.dev/tracing")finishHook:=func(spantrace.Span,statusCodeint){// Record custom metricsifstatusCode>=500{metrics.IncrementServerErrors()}// Log slow requestsifspan.SpanContext().IsValid(){// Calculate duration and log if > threshold}// Send alerts for errorsifstatusCode>=500{alerting.SendAlert("Server error",statusCode)}}tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSpanFinishHook(finishHook),tracing.WithOTLP("collector:4317"),)
Use cases:
Record custom metrics
Log slow requests
Send error alerts
Update counters
Cleanup resources
Combined Hooks Example
tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSpanStartHook(func(ctxcontext.Context,spantrace.Span,req*http.Request){// Enrich span with business contextspan.SetAttributes(attribute.String("tenant.id",extractTenant(req)),attribute.String("feature.flags",getFeatureFlags(req)),)}),tracing.WithSpanFinishHook(func(spantrace.Span,statusCodeint){// Record completion metricsrecordRequestMetrics(statusCode)}),tracing.WithOTLP("collector:4317"),)
Logging Integration
Integrate tracing with your logging infrastructure.
import"rivaas.dev/tracing"eventHandler:=func(etracing.Event){switche.Type{casetracing.EventError:// Send to error tracking (e.g., Sentry)sentry.CaptureMessage(e.Message)myLogger.Error(e.Message,e.Args...)casetracing.EventWarning:myLogger.Warn(e.Message,e.Args...)casetracing.EventInfo:myLogger.Info(e.Message,e.Args...)casetracing.EventDebug:myLogger.Debug(e.Message,e.Args...)}}tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithEventHandler(eventHandler),tracing.WithOTLP("collector:4317"),)
Use cases:
Integrate with non-slog loggers (zap, zerolog, logrus)
Send errors to Sentry/Rollbar
Custom alerting
Audit logging
Metrics from events
No Logging
To disable all internal logging:
tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),// No WithLogger or WithEventHandler = no loggingtracing.WithOTLP("collector:4317"),)
Advanced Configuration
Custom Propagator
Use a custom trace context propagation format:
import("go.opentelemetry.io/otel/propagation""rivaas.dev/tracing")// Use B3 propagation format (Zipkin)b3Propagator:=propagation.NewCompositeTextMapPropagator(propagation.B3{},)tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithCustomPropagator(b3Propagator),tracing.WithOTLP("collector:4317"),)
Custom Tracer Provider
Provide your own OpenTelemetry tracer provider:
import(sdktrace"go.opentelemetry.io/otel/sdk/trace""rivaas.dev/tracing")// Create custom tracer providertp:=sdktrace.NewTracerProvider(// Your custom configurationsdktrace.WithSampler(sdktrace.AlwaysSample()),)tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithTracerProvider(tp),)// You manage tp.Shutdown() yourselfdefertp.Shutdown(context.Background())
Note: When using WithTracerProvider, you’re responsible for shutting down the provider.
Global Tracer Provider
Register as the global OpenTelemetry tracer provider:
tracer:=tracing.MustNew(tracing.WithServiceName("user-api"),tracing.WithServiceVersion(version),// From buildtracing.WithOTLP(otlpEndpoint),// From envtracing.WithSampleRate(0.1),// 10% samplingtracing.WithSpanStartHook(enrichSpan),tracing.WithSpanFinishHook(recordMetrics),)
The tracing package provides HTTP middleware for automatic request tracing with any HTTP framework.
Basic Usage
Wrap your HTTP handler with tracing middleware:
import("net/http""rivaas.dev/tracing")funcmain(){tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithOTLP("localhost:4317"),)tracer.Start(context.Background())defertracer.Shutdown(context.Background())mux:=http.NewServeMux()mux.HandleFunc("/api/users",handleUsers)// Wrap with middlewarehandler:=tracing.Middleware(tracer)(mux)http.ListenAndServe(":8080",handler)}
Middleware Functions
Two functions are available for creating middleware:
Headers are recorded as: http.request.header.{name}
Example span attributes:
http.request.header.x-request-id: "abc123"
http.request.header.x-correlation-id: "xyz789"
Security
Sensitive headers are automatically filtered and never recorded:
Authorization
Cookie
Set-Cookie
X-API-Key
X-Auth-Token
Proxy-Authorization
WWW-Authenticate
This protects against accidental credential exposure in traces.
// This is safe - Authorization header is filteredhandler:=tracing.Middleware(tracer,tracing.WithHeaders("X-Request-ID","Authorization",// ← Automatically filtered, won't be recorded"X-Correlation-ID",),)(mux)
Header Name Normalization
Header names are case-insensitive and normalized to lowercase:
Useful when parameters may contain sensitive data.
Combined Parameter Options
// Record only safe parameters, explicitly exclude sensitive oneshandler:=tracing.Middleware(tracer,tracing.WithRecordParams("page","limit","sort"),tracing.WithExcludeParams("api_key","token"),// Takes precedence)(mux)
Behavior: Blacklist takes precedence. Even if api_key is in the whitelist, it won’t be recorded.
Complete Middleware Example
packagemainimport("context""log""net/http""os""os/signal""rivaas.dev/tracing")funcmain(){ctx,cancel:=signal.NotifyContext(context.Background(),os.Interrupt)defercancel()// Create tracertracer:=tracing.MustNew(tracing.WithServiceName("user-api"),tracing.WithServiceVersion("v1.2.3"),tracing.WithOTLP("localhost:4317"),tracing.WithSampleRate(0.1),// 10% sampling)iferr:=tracer.Start(ctx);err!=nil{log.Fatal(err)}defertracer.Shutdown(context.Background())// Create HTTP handlersmux:=http.NewServeMux()mux.HandleFunc("/api/users",handleUsers)mux.HandleFunc("/api/orders",handleOrders)mux.HandleFunc("/health",handleHealth)mux.HandleFunc("/metrics",handleMetrics)// Wrap with tracing middlewarehandler:=tracing.MustMiddleware(tracer,// Exclude health/metrics endpointstracing.WithExcludePaths("/health","/metrics","/ready","/live"),// Exclude debug and internal routestracing.WithExcludePrefixes("/debug/","/internal/"),// Record correlation headerstracing.WithHeaders("X-Request-ID","X-Correlation-ID","User-Agent"),// Whitelist safe parameterstracing.WithRecordParams("page","limit","sort","filter"),// Blacklist sensitive parameterstracing.WithExcludeParams("password","token","api_key"),)(mux)log.Println("Server starting on :8080")log.Fatal(http.ListenAndServe(":8080",handler))}funchandleUsers(whttp.ResponseWriter,r*http.Request){w.Header().Set("Content-Type","application/json")w.Write([]byte(`{"users": []}`))}funchandleOrders(whttp.ResponseWriter,r*http.Request){w.Header().Set("Content-Type","application/json")w.Write([]byte(`{"orders": []}`))}funchandleHealth(whttp.ResponseWriter,r*http.Request){w.WriteHeader(http.StatusOK)w.Write([]byte("OK"))}funchandleMetrics(whttp.ResponseWriter,r*http.Request){w.Header().Set("Content-Type","text/plain")w.Write([]byte("# Metrics"))}
Integration with Custom Context
Access the span from within your handlers:
funchandleUser(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()// Add custom attributes to the current spantracing.SetSpanAttributeFromContext(ctx,"user.action","view_profile")tracing.SetSpanAttributeFromContext(ctx,"user.id",getUserID(r))// Add eventstracing.AddSpanEventFromContext(ctx,"profile_viewed",attribute.String("profile_id","123"),)// Your handler logic...}
Comparison with Metrics Middleware
The tracing middleware follows the same pattern as the metrics middleware:
Prevents accidental exposure of credentials in traces.
Combine with Span Hooks
startHook:=func(ctxcontext.Context,spantrace.Span,req*http.Request){// Add business context from requestiftenantID:=extractTenant(req);tenantID!=""{span.SetAttributes(attribute.String("tenant.id",tenantID))}}tracer:=tracing.MustNew(tracing.WithServiceName("my-api"),tracing.WithSpanStartHook(startHook),tracing.WithOTLP("collector:4317"),)
Compatible: Works with Jaeger, Zipkin, OpenTelemetry, and more.
Extracting Trace Context
Extract trace context from incoming HTTP requests.
Automatic Extraction (Middleware)
The middleware automatically extracts trace context:
handler:=tracing.Middleware(tracer)(mux)// Context extraction is automatic
No additional code needed - spans automatically become part of the parent trace.
Manual Extraction
For manual span creation or custom HTTP handlers:
funchandleRequest(whttp.ResponseWriter,r*http.Request){// Extract trace context from request headersctx:=tracer.ExtractTraceContext(r.Context(),r.Header)// Create span with propagated contextctx,span:=tracer.StartSpan(ctx,"process-request")defertracer.FinishSpan(span,http.StatusOK)// Span is now part of the distributed trace}
The ExtractTraceContext method reads these headers and links the new span to the parent trace.
Injecting Trace Context
Inject trace context into outgoing HTTP requests.
Manual Injection
When making HTTP calls to other services:
funccallDownstreamService(ctxcontext.Context,tracer*tracing.Tracer)error{// Create outgoing requestreq,err:=http.NewRequestWithContext(ctx,"GET","http://downstream/api",nil)iferr!=nil{returnerr}// Inject trace context into request headerstracer.InjectTraceContext(ctx,req.Header)// Make the requestresp,err:=http.DefaultClient.Do(req)iferr!=nil{returnerr}deferresp.Body.Close()returnnil}
What Gets Injected
The InjectTraceContext method adds headers to propagate the trace:
// Before injectionreq.Header:{}// After injectionreq.Header:{"Traceparent":["00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"],"Tracestate":["vendor1=value1"],}
Complete Distributed Tracing Example
Here’s a complete example showing service-to-service tracing:
Service A (Frontend)
packagemainimport("context""io""log""net/http""rivaas.dev/tracing")funcmain(){tracer:=tracing.MustNew(tracing.WithServiceName("frontend-api"),tracing.WithOTLP("localhost:4317"),)tracer.Start(context.Background())defertracer.Shutdown(context.Background())mux:=http.NewServeMux()// Handler that calls downstream servicemux.HandleFunc("/api/process",func(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()// Create span for this service's workctx,span:=tracer.StartSpan(ctx,"frontend-process")defertracer.FinishSpan(span,http.StatusOK)// Call downstream service with trace propagationresult,err:=callBackendService(ctx,tracer)iferr!=nil{http.Error(w,err.Error(),http.StatusInternalServerError)return}w.Write([]byte(result))})handler:=tracing.Middleware(tracer)(mux)log.Fatal(http.ListenAndServe(":8080",handler))}funccallBackendService(ctxcontext.Context,tracer*tracing.Tracer)(string,error){// Create span for outgoing callctx,span:=tracer.StartSpan(ctx,"call-backend-service")defertracer.FinishSpan(span,http.StatusOK)// Create HTTP requestreq,err:=http.NewRequestWithContext(ctx,"GET","http://localhost:8081/api/data",nil)iferr!=nil{return"",err}// Inject trace context for propagationtracer.InjectTraceContext(ctx,req.Header)// Make the requestresp,err:=http.DefaultClient.Do(req)iferr!=nil{return"",err}deferresp.Body.Close()body,_:=io.ReadAll(resp.Body)returnstring(body),nil}
Service B (Backend)
packagemainimport("context""log""net/http""rivaas.dev/tracing")funcmain(){tracer:=tracing.MustNew(tracing.WithServiceName("backend-api"),tracing.WithOTLP("localhost:4317"),)tracer.Start(context.Background())defertracer.Shutdown(context.Background())mux:=http.NewServeMux()// Handler automatically receives trace context via middlewaremux.HandleFunc("/api/data",func(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()// This span is automatically part of the distributed tracectx,span:=tracer.StartSpan(ctx,"fetch-data")defertracer.FinishSpan(span,http.StatusOK)tracer.SetSpanAttribute(span,"data.source","database")// Simulate workdata:=fetchFromDatabase(ctx,tracer)w.Write([]byte(data))})// Middleware automatically extracts trace contexthandler:=tracing.Middleware(tracer)(mux)log.Fatal(http.ListenAndServe(":8081",handler))}funcfetchFromDatabase(ctxcontext.Context,tracer*tracing.Tracer)string{// Nested span - all part of the same tracectx,span:=tracer.StartSpan(ctx,"database-query")defertracer.FinishSpan(span,http.StatusOK)tracer.SetSpanAttribute(span,"db.system","postgresql")tracer.SetSpanAttribute(span,"db.query","SELECT * FROM data")return"data from database"}
funcprocessOrder(ctxcontext.Context,orderIDstring){// Add attributes to current span in contexttracing.SetSpanAttributeFromContext(ctx,"order.id",orderID)tracing.SetSpanAttributeFromContext(ctx,"order.status","processing")}
No-op if no active span.
Add Events from Context
Add events to the current span:
import"go.opentelemetry.io/otel/attribute"funcvalidatePayment(ctxcontext.Context,amountfloat64){// Add event to current spantracing.AddSpanEventFromContext(ctx,"payment_validated",attribute.Float64("amount",amount),attribute.String("currency","USD"),)}
Get Trace Context
The context already contains trace information:
funcpassContextToWorker(ctxcontext.Context){// Context already has trace info - just pass itgoprocessInBackground(ctx)}funcprocessInBackground(ctxcontext.Context){// Trace context is preservedtraceID:=tracing.TraceID(ctx)log.Printf("Background work [trace=%s]",traceID)}
// ✓ Good - context propagatesfunchandler(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()result:=doWork(ctx)// Pass context}funcdoWork(ctxcontext.Context)string{ctx,span:=tracer.StartSpan(ctx,"do-work")defertracer.FinishSpan(span,http.StatusOK)returndoMoreWork(ctx)// Pass context}// ✗ Bad - context lostfunchandler(whttp.ResponseWriter,r*http.Request){result:=doWork(context.Background())// Lost trace context!}
Use Context for HTTP Clients
Always use http.NewRequestWithContext:
// ✓ Goodreq,_:=http.NewRequestWithContext(ctx,"GET",url,nil)tracer.InjectTraceContext(ctx,req.Header)// ✗ Bad - no contextreq,_:=http.NewRequest("GET",url,nil)tracer.InjectTraceContext(ctx,req.Header)// Won't have span info
Inject Before Making Requests
Always inject trace context before sending requests:
req,_:=http.NewRequestWithContext(ctx,"GET",url,nil)// Inject trace contexttracer.InjectTraceContext(ctx,req.Header)// Then make requestresp,_:=http.DefaultClient.Do(req)
Extract in Custom Handlers
If not using middleware, extract context manually:
funccustomHandler(whttp.ResponseWriter,r*http.Request){// Extract trace contextctx:=tracer.ExtractTraceContext(r.Context(),r.Header)// Use propagated contextctx,span:=tracer.StartSpan(ctx,"custom-handler")defertracer.FinishSpan(span,http.StatusOK)}
Troubleshooting
Traces Not Connected Across Services
Problem: Each service shows separate traces instead of one distributed trace.
Solutions:
Ensure both services use the same propagator format (default: W3C Trace Context)
Verify InjectTraceContext is called before making requests
Verify ExtractTraceContext is called when receiving requests
Check that context is passed through the call chain
Verify both services send to the same OTLP collector
Missing Spans in Distributed Trace
Problem: Some spans appear but others are missing.
Problem: Background goroutines don’t have trace context.
Solution: Pass context explicitly to goroutines:
funchandler(ctxcontext.Context){// ✓ Good - pass contextgofunc(ctxcontext.Context){ctx,span:=tracer.StartSpan(ctx,"background-work")defertracer.FinishSpan(span,http.StatusOK)}(ctx)// ✗ Bad - lost contextgofunc(){ctx:=context.Background()// Lost trace context!ctx,span:=tracer.StartSpan(ctx,"background-work")defertracer.FinishSpan(span,http.StatusOK)}()}
Test your tracing implementation with provided utilities
The tracing package provides testing utilities to help you write tests for traced applications.
Testing Utilities
Three helper functions are provided for testing:
Function
Purpose
Provider
TestingTracer()
Create tracer for tests.
Noop
TestingTracerWithStdout()
Create tracer with output.
Stdout
TestingMiddleware()
Create test middleware.
Noop
TestingTracer
Create a tracer configured for unit tests.
Basic Usage
import("testing""rivaas.dev/tracing")funcTestSomething(t*testing.T){t.Parallel()tracer:=tracing.TestingTracer(t)// Use tracer in test...}
Features
Noop provider: No actual tracing, minimal overhead.
Automatic cleanup: Shutdown() called via t.Cleanup().
Safe for parallel tests: Each test gets its own tracer.
Default configuration:
Service name: "test-service".
Service version: "v1.0.0".
Sample rate: 1.0 (100%).
With Custom Options
Override defaults with your own options.
funcTestWithCustomConfig(t*testing.T){tracer:=tracing.TestingTracer(t,tracing.WithServiceName("my-test-service"),tracing.WithSampleRate(0.5),)// Use tracer...}
Complete Test Example
funcTestProcessOrder(t*testing.T){t.Parallel()tracer:=tracing.TestingTracer(t)ctx:=context.Background()// Test your traced functionresult,err:=processOrder(ctx,tracer,"order-123")assert.NoError(t,err)assert.Equal(t,"success",result)}funcprocessOrder(ctxcontext.Context,tracer*tracing.Tracer,orderIDstring)(string,error){ctx,span:=tracer.StartSpan(ctx,"process-order")defertracer.FinishSpan(span,200)tracer.SetSpanAttribute(span,"order.id",orderID)return"success",nil}
TestingTracerWithStdout
Create a tracer that prints traces to stdout for debugging.
funcTestDebugWithOptions(t*testing.T){tracer:=tracing.TestingTracerWithStdout(t,tracing.WithServiceName("debug-service"),tracing.WithSampleRate(1.0),)// Use tracer...}
TestingMiddleware
Create HTTP middleware for testing traced handlers.
Basic Usage
import("net/http""net/http/httptest""testing""rivaas.dev/tracing")funcTestHTTPHandler(t*testing.T){t.Parallel()// Create test middlewaremiddleware:=tracing.TestingMiddleware(t)// Wrap your handlerhandler:=middleware(http.HandlerFunc(myHandler))// Test the handlerreq:=httptest.NewRequest("GET","/api/users",nil)rec:=httptest.NewRecorder()handler.ServeHTTP(rec,req)assert.Equal(t,http.StatusOK,rec.Code)}funcmyHandler(whttp.ResponseWriter,r*http.Request){w.WriteHeader(http.StatusOK)w.Write([]byte("OK"))}
funcTestPathExclusion(t*testing.T){middleware:=tracing.TestingMiddleware(t,tracing.WithExcludePaths("/health"),)handler:=middleware(http.HandlerFunc(func(whttp.ResponseWriter,r*http.Request){// This handler should not create a span for /healthw.WriteHeader(http.StatusOK)}))// Request to excluded pathreq:=httptest.NewRequest("GET","/health",nil)rec:=httptest.NewRecorder()handler.ServeHTTP(rec,req)assert.Equal(t,http.StatusOK,rec.Code)}
TestingMiddlewareWithTracer
Use a custom tracer with test middleware.
When to Use
Need specific tracer configuration
Testing with stdout output
Custom sampling rates
Specific provider behavior
Basic Usage
funcTestWithCustomTracer(t*testing.T){// Create custom tracertracer:=tracing.TestingTracer(t,tracing.WithSampleRate(0.5),)// Create middleware with custom tracermiddleware:=tracing.TestingMiddlewareWithTracer(t,tracer,tracing.WithExcludePaths("/metrics"),)handler:=middleware(http.HandlerFunc(myHandler))// Test...}
With Stdout Output
funcTestDebugMiddleware(t*testing.T){// Create tracer with stdouttracer:=tracing.TestingTracerWithStdout(t)// Create middleware with that tracermiddleware:=tracing.TestingMiddlewareWithTracer(t,tracer)handler:=middleware(http.HandlerFunc(myHandler))// Test and see trace outputreq:=httptest.NewRequest("GET","/api/users",nil)rec:=httptest.NewRecorder()handler.ServeHTTP(rec,req)}
funcTestSpanAttributes(t*testing.T){t.Parallel()tracer:=tracing.TestingTracer(t)ctx:=context.Background()// Create span and add attributesctx,span:=tracer.StartSpan(ctx,"test-span")tracer.SetSpanAttribute(span,"user.id","123")tracer.SetSpanAttribute(span,"user.role","admin")tracer.FinishSpan(span,200)// With noop provider, this doesn't record anything,// but ensures the code doesn't panic or error}
Testing Context Propagation
funcTestContextPropagation(t*testing.T){t.Parallel()tracer:=tracing.TestingTracer(t)ctx:=context.Background()// Create parent spanctx,parentSpan:=tracer.StartSpan(ctx,"parent")defertracer.FinishSpan(parentSpan,200)// Get trace IDtraceID:=tracing.TraceID(ctx)assert.NotEmpty(t,traceID)// Create child span - should have same trace IDctx,childSpan:=tracer.StartSpan(ctx,"child")defertracer.FinishSpan(childSpan,200)childTraceID:=tracing.TraceID(ctx)assert.Equal(t,traceID,childTraceID,"child should have same trace ID")}
Testing Trace Injection/Extraction
funcTestTraceInjection(t*testing.T){t.Parallel()tracer:=tracing.TestingTracer(t)ctx:=context.Background()// Create spanctx,span:=tracer.StartSpan(ctx,"test")defertracer.FinishSpan(span,200)// Inject into headersheaders:=http.Header{}tracer.InjectTraceContext(ctx,headers)// Verify headers were setassert.NotEmpty(t,headers.Get("Traceparent"))// Extract from headersnewCtx:=context.Background()newCtx=tracer.ExtractTraceContext(newCtx,headers)// Both contexts should have the same trace IDoriginalTraceID:=tracing.TraceID(ctx)extractedTraceID:=tracing.TraceID(newCtx)assert.Equal(t,originalTraceID,extractedTraceID)}
Integration Test Example
funcTestAPIWithTracing(t*testing.T){t.Parallel()// Create tracertracer:=tracing.TestingTracer(t)// Create test server with tracingmux:=http.NewServeMux()mux.HandleFunc("/api/users",func(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()// Add attributes from contexttracing.SetSpanAttributeFromContext(ctx,"handler","users")w.WriteHeader(http.StatusOK)w.Write([]byte(`{"users": []}`))})handler:=tracing.TestingMiddlewareWithTracer(t,tracer)(mux)server:=httptest.NewServer(handler)deferserver.Close()// Make requestresp,err:=http.Get(server.URL+"/api/users")require.NoError(t,err)deferresp.Body.Close()assert.Equal(t,http.StatusOK,resp.StatusCode)}
funcTestSomething(t*testing.T){t.Parallel()// Safe - each test gets its own tracertracer:=tracing.TestingTracer(t)// Test...}
Don’t Call Shutdown Manually
The test utilities handle cleanup automatically:
// ✓ Good - automatic cleanupfuncTestGood(t*testing.T){tracer:=tracing.TestingTracer(t)// No need to call Shutdown()}// ✗ Bad - redundant manual cleanupfuncTestBad(t*testing.T){tracer:=tracing.TestingTracer(t)defertracer.Shutdown(context.Background())// Unnecessary}
Use Stdout for Debugging Only
Don’t use TestingTracerWithStdout for regular tests:
// ✓ Good - stdout only when debuggingfuncTestDebug(t*testing.T){iftesting.Verbose(){tracer:=tracing.TestingTracerWithStdout(t)}else{tracer:=tracing.TestingTracer(t)}}// ✗ Bad - noisy test outputfuncTestRegular(t*testing.T){tracer:=tracing.TestingTracerWithStdout(t)// Too verbose}
Explore complete examples and best practices for production-ready tracing configurations.
Production Configuration
A production-ready tracing setup with all recommended settings.
packagemainimport("context""log""log/slog""net/http""os""os/signal""time""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/trace""rivaas.dev/tracing")funcmain(){// Create context for graceful shutdownctx,cancel:=signal.NotifyContext(context.Background(),os.Interrupt)defercancel()// Create logger for internal eventslogger:=slog.New(slog.NewJSONHandler(os.Stdout,&slog.HandlerOptions{Level:slog.LevelInfo,}))// Create tracer with production settingstracer,err:=tracing.New(tracing.WithServiceName("user-api"),tracing.WithServiceVersion(os.Getenv("VERSION")),tracing.WithOTLP(os.Getenv("OTLP_ENDPOINT")),tracing.WithSampleRate(0.1),// 10% samplingtracing.WithLogger(logger),tracing.WithSpanStartHook(enrichSpan),tracing.WithSpanFinishHook(recordMetrics),)iferr!=nil{log.Fatalf("Failed to initialize tracing: %v",err)}// Start tracer (required for OTLP)iferr:=tracer.Start(ctx);err!=nil{log.Fatalf("Failed to start tracer: %v",err)}// Ensure graceful shutdowndeferfunc(){shutdownCtx,shutdownCancel:=context.WithTimeout(context.Background(),5*time.Second)defershutdownCancel()iferr:=tracer.Shutdown(shutdownCtx);err!=nil{log.Printf("Error shutting down tracer: %v",err)}}()// Create HTTP handlersmux:=http.NewServeMux()mux.HandleFunc("/api/users",handleUsers)mux.HandleFunc("/api/orders",handleOrders)mux.HandleFunc("/health",handleHealth)mux.HandleFunc("/metrics",handleMetrics)// Wrap with tracing middlewarehandler:=tracing.MustMiddleware(tracer,// Exclude observability endpointstracing.WithExcludePaths("/health","/metrics","/ready","/live"),// Exclude debug endpointstracing.WithExcludePrefixes("/debug/","/internal/"),// Record correlation headerstracing.WithHeaders("X-Request-ID","X-Correlation-ID"),// Whitelist safe parameterstracing.WithRecordParams("page","limit","sort"),// Blacklist sensitive parameterstracing.WithExcludeParams("password","token","api_key"),)(mux)// Start serverlog.Printf("Server starting on :8080")iferr:=http.ListenAndServe(":8080",handler);err!=nil{log.Fatal(err)}}// enrichSpan adds custom business context to spansfuncenrichSpan(ctxcontext.Context,spantrace.Span,req*http.Request){// Add tenant identifieriftenantID:=req.Header.Get("X-Tenant-ID");tenantID!=""{span.SetAttributes(attribute.String("tenant.id",tenantID))}// Add user informationifuserID:=req.Header.Get("X-User-ID");userID!=""{span.SetAttributes(attribute.String("user.id",userID))}// Add deployment informationspan.SetAttributes(attribute.String("deployment.region",os.Getenv("REGION")),attribute.String("deployment.environment",os.Getenv("ENVIRONMENT")),)}// recordMetrics records custom metrics based on span completionfuncrecordMetrics(spantrace.Span,statusCodeint){// Record error metricsifstatusCode>=500{// metrics.IncrementServerErrors()}// Record slow request metrics// Could calculate duration and record if above threshold}funchandleUsers(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()// Add custom span attributestracing.SetSpanAttributeFromContext(ctx,"handler","users")tracing.SetSpanAttributeFromContext(ctx,"operation","list")w.Header().Set("Content-Type","application/json")w.Write([]byte(`{"users": []}`))}funchandleOrders(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()tracing.SetSpanAttributeFromContext(ctx,"handler","orders")w.Header().Set("Content-Type","application/json")w.Write([]byte(`{"orders": []}`))}funchandleHealth(whttp.ResponseWriter,r*http.Request){w.WriteHeader(http.StatusOK)w.Write([]byte("OK"))}funchandleMetrics(whttp.ResponseWriter,r*http.Request){w.Header().Set("Content-Type","text/plain")w.Write([]byte("# Metrics"))}
Development Configuration
A development setup with verbose output for debugging.
packagemainimport("context""log""log/slog""net/http""os""rivaas.dev/tracing")funcmain(){// Create logger with debug levellogger:=slog.New(slog.NewTextHandler(os.Stdout,&slog.HandlerOptions{Level:slog.LevelDebug,}))// Create tracer with development settingstracer:=tracing.MustNew(tracing.WithServiceName("user-api"),tracing.WithServiceVersion("dev"),tracing.WithStdout(),// Print traces to consoletracing.WithSampleRate(1.0),// Trace everythingtracing.WithLogger(logger),// Verbose logging)defertracer.Shutdown(context.Background())// Create simple handlermux:=http.NewServeMux()mux.HandleFunc("/",func(whttp.ResponseWriter,r*http.Request){w.Write([]byte("Hello, World!"))})// Minimal middleware - trace everythinghandler:=tracing.MustMiddleware(tracer)(mux)log.Println("Development server on :8080")log.Fatal(http.ListenAndServe(":8080",handler))}
Microservices Example
Complete distributed tracing across multiple services.
Service A (API Gateway)
packagemainimport("context""io""log""net/http""rivaas.dev/tracing")funcmain(){tracer:=tracing.MustNew(tracing.WithServiceName("api-gateway"),tracing.WithServiceVersion("v1.0.0"),tracing.WithOTLP("localhost:4317"),)tracer.Start(context.Background())defertracer.Shutdown(context.Background())mux:=http.NewServeMux()mux.HandleFunc("/api/users",func(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()// Call user serviceusers,err:=callUserService(ctx,tracer)iferr!=nil{http.Error(w,err.Error(),http.StatusInternalServerError)return}w.Header().Set("Content-Type","application/json")w.Write([]byte(users))})handler:=tracing.MustMiddleware(tracer,tracing.WithExcludePaths("/health"),)(mux)log.Fatal(http.ListenAndServe(":8080",handler))}funccallUserService(ctxcontext.Context,tracer*tracing.Tracer)(string,error){// Create span for outgoing callctx,span:=tracer.StartSpan(ctx,"call-user-service")defertracer.FinishSpan(span,http.StatusOK)// Create requestreq,err:=http.NewRequestWithContext(ctx,"GET","http://localhost:8081/users",nil)iferr!=nil{return"",err}// Inject trace contexttracer.InjectTraceContext(ctx,req.Header)// Make requestresp,err:=http.DefaultClient.Do(req)iferr!=nil{return"",err}deferresp.Body.Close()body,_:=io.ReadAll(resp.Body)returnstring(body),nil}
Service B (User Service)
packagemainimport("context""log""net/http""rivaas.dev/tracing")funcmain(){tracer:=tracing.MustNew(tracing.WithServiceName("user-service"),tracing.WithServiceVersion("v1.0.0"),tracing.WithOTLP("localhost:4317"),)tracer.Start(context.Background())defertracer.Shutdown(context.Background())mux:=http.NewServeMux()mux.HandleFunc("/users",func(whttp.ResponseWriter,r*http.Request){ctx:=r.Context()// This span is part of the distributed tracectx,span:=tracer.StartSpan(ctx,"fetch-users")defertracer.FinishSpan(span,http.StatusOK)tracer.SetSpanAttribute(span,"db.system","postgresql")// Simulate database queryusers:=`{"users": [{"id": 1, "name": "Alice"}]}`w.Header().Set("Content-Type","application/json")w.Write([]byte(users))})// Middleware automatically extracts trace contexthandler:=tracing.MustMiddleware(tracer)(mux)log.Fatal(http.ListenAndServe(":8081",handler))}
Environment-Based Configuration
Configure tracing based on environment.
packagemainimport("context""log""log/slog""net/http""os""rivaas.dev/tracing")funcmain(){tracer:=createTracer(os.Getenv("ENVIRONMENT"))defertracer.Shutdown(context.Background())// If OTLP, start the traceriftracer.GetProvider()==tracing.OTLPProvider||tracer.GetProvider()==tracing.OTLPHTTPProvider{iferr:=tracer.Start(context.Background());err!=nil{log.Fatal(err)}}mux:=http.NewServeMux()mux.HandleFunc("/",func(whttp.ResponseWriter,r*http.Request){w.Write([]byte("Hello"))})handler:=tracing.MustMiddleware(tracer)(mux)log.Fatal(http.ListenAndServe(":8080",handler))}funccreateTracer(envstring)*tracing.Tracer{serviceName:=os.Getenv("SERVICE_NAME")ifserviceName==""{serviceName="my-api"}version:=os.Getenv("VERSION")ifversion==""{version="dev"}opts:=[]tracing.Option{tracing.WithServiceName(serviceName),tracing.WithServiceVersion(version),}switchenv{case"production":opts=append(opts,tracing.WithOTLP(os.Getenv("OTLP_ENDPOINT")),tracing.WithSampleRate(0.1),// 10% sampling)case"staging":opts=append(opts,tracing.WithOTLP(os.Getenv("OTLP_ENDPOINT")),tracing.WithSampleRate(0.5),// 50% sampling)default:// developmentlogger:=slog.New(slog.NewTextHandler(os.Stdout,nil))opts=append(opts,tracing.WithStdout(),tracing.WithSampleRate(1.0),// 100% samplingtracing.WithLogger(logger),)}returntracing.MustNew(opts...)}
Database Tracing Example
Trace database operations.
packagemainimport("context""database/sql""net/http""go.opentelemetry.io/otel/attribute""rivaas.dev/tracing")typeUserRepositorystruct{db*sql.DBtracer*tracing.Tracer}func(r*UserRepository)GetUser(ctxcontext.Context,userIDint)(*User,error){// Create span for database operationctx,span:=r.tracer.StartSpan(ctx,"db-get-user")deferr.tracer.FinishSpan(span,http.StatusOK)// Add database attributesr.tracer.SetSpanAttribute(span,"db.system","postgresql")r.tracer.SetSpanAttribute(span,"db.operation","SELECT")r.tracer.SetSpanAttribute(span,"db.table","users")r.tracer.SetSpanAttribute(span,"user.id",userID)// Execute queryquery:="SELECT id, name, email FROM users WHERE id = $1"r.tracer.SetSpanAttribute(span,"db.query",query)varuserUsererr:=r.db.QueryRowContext(ctx,query,userID).Scan(&user.ID,&user.Name,&user.Email)iferr!=nil{r.tracer.SetSpanAttribute(span,"error",true)r.tracer.SetSpanAttribute(span,"error.message",err.Error())returnnil,err}// Add event for successful queryr.tracer.AddSpanEvent(span,"user_found",attribute.Int("user.id",user.ID),)return&user,nil}typeUserstruct{IDintNamestringEmailstring}
Custom Span Events Example
Record significant events within spans.
funcprocessOrder(ctxcontext.Context,tracer*tracing.Tracer,order*Order)error{ctx,span:=tracer.StartSpan(ctx,"process-order")defertracer.FinishSpan(span,http.StatusOK)tracer.SetSpanAttribute(span,"order.id",order.ID)tracer.SetSpanAttribute(span,"order.total",order.Total)// Event: Order validation startedtracer.AddSpanEvent(span,"validation_started")iferr:=validateOrder(ctx,tracer,order);err!=nil{tracer.AddSpanEvent(span,"validation_failed",attribute.String("error",err.Error()),)returnerr}tracer.AddSpanEvent(span,"validation_passed")// Event: Payment processing startedtracer.AddSpanEvent(span,"payment_started",attribute.Float64("amount",order.Total),)iferr:=chargePayment(ctx,tracer,order);err!=nil{tracer.AddSpanEvent(span,"payment_failed",attribute.String("error",err.Error()),)returnerr}tracer.AddSpanEvent(span,"payment_succeeded",attribute.String("transaction_id","TXN123"),)// Event: Order completedtracer.AddSpanEvent(span,"order_completed")returnnil}
Performance Benchmarks
Actual performance measurements from the tracing package:
// Operation Time Memory Allocations// Request overhead (100% sampling) ~1.6 µs 2.3 KB 23// Start/Finish span ~160 ns 240 B 3// Set attribute ~3 ns 0 B 0// Path exclusion (100 paths) ~9 ns 0 B 0