Skip to main content
โšก Calmops

Distributed Tracing with Jaeger in Go

Distributed Tracing with Jaeger in Go

Introduction

Distributed tracing tracks requests across microservices, providing visibility into system behavior. This guide covers implementing distributed tracing with Jaeger in Go.

Jaeger helps you understand request flows, identify bottlenecks, and debug issues in distributed systems by tracking spans across services.

Jaeger Fundamentals

Trace and Span Concepts

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
)

// InitJaeger initializes Jaeger tracer
func InitJaeger(serviceName string) (opentracing.Tracer, error) {
	cfg := &config.Configuration{
		ServiceName: serviceName,
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans: true,
		},
	}

	tracer, _, err := cfg.NewTracer()
	if err != nil {
		return nil, fmt.Errorf("failed to initialize tracer: %w", err)
	}

	return tracer, nil
}

// TraceExample demonstrates basic tracing
func TraceExample(tracer opentracing.Tracer) {
	// Create root span
	span := tracer.StartSpan("operation")
	defer span.Finish()

	// Create child span
	childSpan := tracer.StartSpan(
		"child-operation",
		opentracing.ChildOf(span.Context()),
	)
	defer childSpan.Finish()

	// Add tags
	span.SetTag("component", "my-service")
	span.SetTag("http.method", "GET")
	span.SetTag("http.url", "/api/users")

	// Add logs
	span.LogKV(
		"event", "request_received",
		"timestamp", time.Now(),
	)

	childSpan.LogKV(
		"event", "processing",
		"duration_ms", 100,
	)
}

Good: Proper Distributed Tracing Implementation

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"

	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/ext"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
)

// TracedHTTPClient wraps HTTP client with tracing
type TracedHTTPClient struct {
	client *http.Client
	tracer opentracing.Tracer
}

// NewTracedHTTPClient creates a new traced HTTP client
func NewTracedHTTPClient(tracer opentracing.Tracer) *TracedHTTPClient {
	return &TracedHTTPClient{
		client: &http.Client{
			Timeout: 5 * time.Second,
		},
		tracer: tracer,
	}
}

// Do performs an HTTP request with tracing
func (c *TracedHTTPClient) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
	// Get span from context or create new one
	span, ctx := opentracing.StartSpanFromContext(ctx, "http-request")
	defer span.Finish()

	// Set span tags
	ext.HTTPMethod.Set(span, req.Method)
	ext.HTTPUrl.Set(span, req.URL.String())
	ext.Component.Set(span, "http-client")

	// Inject trace context into request headers
	if err := c.tracer.Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header)); err != nil {
		log.Printf("Failed to inject trace context: %v", err)
	}

	// Perform request
	resp, err := c.client.Do(req)

	// Set response status
	if resp != nil {
		ext.HTTPStatusCode.Set(span, uint16(resp.StatusCode))
	}

	// Set error if occurred
	if err != nil {
		ext.Error.Set(span, true)
		span.LogKV("error", err.Error())
	}

	return resp, err
}

// UserService demonstrates service with tracing
type UserService struct {
	tracer opentracing.Tracer
	client *TracedHTTPClient
}

// NewUserService creates a new user service
func NewUserService(tracer opentracing.Tracer) *UserService {
	return &UserService{
		tracer: tracer,
		client: NewTracedHTTPClient(tracer),
	}
}

// GetUser retrieves a user with tracing
func (s *UserService) GetUser(ctx context.Context, userID string) (map[string]interface{}, error) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "GetUser")
	defer span.Finish()

	span.SetTag("user.id", userID)

	// Simulate database call
	span.LogKV("event", "database_query_start")
	time.Sleep(100 * time.Millisecond)
	span.LogKV("event", "database_query_end")

	user := map[string]interface{}{
		"id":    userID,
		"name":  "John Doe",
		"email": "[email protected]",
	}

	span.SetTag("user.found", true)
	return user, nil
}

// CreateUser creates a user with tracing
func (s *UserService) CreateUser(ctx context.Context, name, email string) (map[string]interface{}, error) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "CreateUser")
	defer span.Finish()

	span.SetTag("user.name", name)
	span.SetTag("user.email", email)

	// Validate input
	validateSpan, _ := opentracing.StartSpanFromContext(ctx, "ValidateInput")
	if name == "" || email == "" {
		ext.Error.Set(validateSpan, true)
		validateSpan.LogKV("error", "invalid input")
		validateSpan.Finish()
		return nil, fmt.Errorf("invalid input")
	}
	validateSpan.Finish()

	// Save to database
	saveSpan, _ := opentracing.StartSpanFromContext(ctx, "SaveToDatabase")
	time.Sleep(50 * time.Millisecond)
	saveSpan.Finish()

	user := map[string]interface{}{
		"id":    "user-123",
		"name":  name,
		"email": email,
	}

	return user, nil
}

// HTTP Handler with tracing
func (s *UserService) HandleGetUser(w http.ResponseWriter, r *http.Request) {
	// Extract trace context from request
	spanCtx, _ := s.tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
	span := s.tracer.StartSpan("HandleGetUser", ext.RPCServerOption(spanCtx))
	defer span.Finish()

	ctx := opentracing.ContextWithSpan(r.Context(), span)

	userID := r.URL.Query().Get("id")
	user, err := s.GetUser(ctx, userID)

	if err != nil {
		ext.Error.Set(span, true)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, "%v", user)
}

Bad: Improper Tracing Implementation

package main

import (
	"context"
	"net/http"
)

// BAD: No tracing
type BadUserService struct{}

// BAD: No span creation
func (s *BadUserService) GetUser(ctx context.Context, userID string) (map[string]interface{}, error) {
	// No tracing
	// No span tags
	// No error logging
	return map[string]interface{}{}, nil
}

// BAD: No trace context propagation
func (s *BadUserService) HandleGetUser(w http.ResponseWriter, r *http.Request) {
	// No trace extraction
	// No span creation
	// No context propagation
	w.WriteHeader(http.StatusOK)
}

Problems:

  • No tracing implementation
  • No span creation
  • No trace context propagation
  • No error tracking

Advanced Tracing Patterns

Baggage and Context Propagation

package main

import (
	"context"

	"github.com/opentracing/opentracing-go"
)

// SetBaggage sets baggage in span
func SetBaggage(span opentracing.Span, key, value string) {
	span.SetBaggageItem(key, value)
}

// GetBaggage gets baggage from span
func GetBaggage(span opentracing.Span, key string) string {
	return span.BaggageItem(key)
}

// PropagateContext propagates context across services
func PropagateContext(ctx context.Context, tracer opentracing.Tracer) context.Context {
	span := opentracing.SpanFromContext(ctx)
	if span == nil {
		return ctx
	}

	// Set baggage items
	span.SetBaggageItem("user_id", "user-123")
	span.SetBaggageItem("request_id", "req-456")

	return ctx
}

Sampling Strategies

package main

import (
	"github.com/uber/jaeger-client-go/config"
)

// ConstSampler samples all traces
func ConstSampler() *config.SamplerConfig {
	return &config.SamplerConfig{
		Type:  "const",
		Param: 1, // Sample all
	}
}

// ProbabilisticSampler samples a percentage of traces
func ProbabilisticSampler(rate float64) *config.SamplerConfig {
	return &config.SamplerConfig{
		Type:  "probabilistic",
		Param: rate, // 0.1 = 10%
	}
}

// RateLimitingSampler limits traces per second
func RateLimitingSampler(tracesPerSecond float64) *config.SamplerConfig {
	return &config.SamplerConfig{
		Type:  "rate_limiting",
		Param: tracesPerSecond,
	}
}

Metrics from Traces

package main

import (
	"context"
	"time"

	"github.com/opentracing/opentracing-go"
)

// TraceMetrics extracts metrics from traces
type TraceMetrics struct {
	OperationName string
	Duration      time.Duration
	Success       bool
	ErrorMessage  string
}

// ExtractMetrics extracts metrics from span
func ExtractMetrics(span opentracing.Span) TraceMetrics {
	return TraceMetrics{
		OperationName: span.OperationName(),
		Success:       true,
	}
}

// RecordMetrics records metrics
func RecordMetrics(ctx context.Context, tracer opentracing.Tracer, operation string, duration time.Duration, err error) {
	span, _ := opentracing.StartSpanFromContext(ctx, operation)
	defer span.Finish()

	span.SetTag("duration_ms", duration.Milliseconds())
	if err != nil {
		span.SetTag("error", true)
		span.LogKV("error", err.Error())
	}
}

Best Practices

1. Consistent Naming

// Use consistent operation names
span := tracer.StartSpan("service.operation")

2. Meaningful Tags

// Add meaningful tags for filtering
span.SetTag("user.id", userID)
span.SetTag("http.status_code", 200)
span.SetTag("db.query_time_ms", 50)

3. Error Tracking

// Always track errors
if err != nil {
	ext.Error.Set(span, true)
	span.LogKV("error", err.Error())
}

4. Sampling Strategy

// Choose appropriate sampling strategy
// Use const for development, probabilistic for production

Common Pitfalls

1. No Context Propagation

Always propagate trace context across service boundaries.

2. Missing Error Tags

Always tag errors for visibility.

3. Excessive Spans

Don’t create too many spans; it impacts performance.

4. No Sampling

Use sampling in production to manage overhead.

Resources

Summary

Distributed tracing provides visibility into microservices. Key takeaways:

  • Initialize Jaeger tracer for your service
  • Create spans for operations
  • Propagate trace context across services
  • Add meaningful tags and logs
  • Track errors in spans
  • Use appropriate sampling strategies
  • Monitor trace metrics

By implementing distributed tracing, you can effectively debug and optimize your microservices.

Comments