tracing Package
Distributed tracing with OpenTelemetry and OTLP for request tracking across microservices.
Features
- Distributed Tracing: Track requests across services
- OpenTelemetry: Industry-standard tracing
- OTLP Export: Compatible with Jaeger, Zipkin, etc.
- Automatic Context Propagation: Span context in requests
- Custom Attributes: Add business context to spans
- Sampling: Control trace volume
- Error Recording: Automatic error tracking
Installation
import "github.com/LaRestoOU/laresto-go-common/pkg/tracing"
Quick Start
Initialize Tracing
cfg := tracing.Config{
ServiceName: "auth-service",
ServiceVersion: "1.0.0",
Environment: "production",
OTLPEndpoint: "localhost:4318",
Enabled: true,
SampleRate: 1.0, // Trace all requests
Insecure: false, // Use TLS in production
}
log := logger.New(logger.Config{...})
provider, err := tracing.NewProvider(cfg, log)
if err != nil {
log.Fatal("Failed to initialize tracing", err)
}
defer provider.Shutdown(context.Background())
Create Spans
func ProcessOrder(ctx context.Context, orderID string) error {
// Start span
ctx, span := tracing.StartSpan(ctx, "auth-service", "process-order")
defer span.End()
// Add attributes
tracing.SetAttributes(ctx,
tracing.OrderID.String(orderID),
tracing.UserID.String("user-123"),
)
// Process order
if err := validateOrder(ctx, orderID); err != nil {
tracing.RecordError(ctx, err)
return err
}
// Add event
tracing.AddEvent(ctx, "order-validated",
attribute.String("status", "valid"),
)
return nil
}
Configuration
type Config struct {
// ServiceName is the service identifier
ServiceName string // "auth-service"
// ServiceVersion is the service version
ServiceVersion string // "1.0.0"
// Environment is deployment env
Environment string // "production"
// OTLPEndpoint is the collector endpoint
// Jaeger OTLP HTTP: "localhost:4318"
// Jaeger OTLP gRPC: "localhost:4317"
OTLPEndpoint string
// Enabled toggles tracing
Enabled bool
// SampleRate (0.0 to 1.0)
// 1.0 = trace all, 0.1 = trace 10%
SampleRate float64
// Insecure for development (no TLS)
Insecure bool
}
Jaeger Setup (Week 2 Infrastructure)
Your Docker Compose already has Jaeger with OTLP support:
# Week 2: laresto-docker-compose
jaeger:
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
Access Jaeger UI: http://localhost:16686
Usage Patterns
HTTP Request Tracing
func (h *Handler) GetUser(c *gin.Context) {
// Start span for HTTP request
ctx, span := tracing.StartSpan(c.Request.Context(), "auth-service", "GET /users/:id")
defer span.End()
// Add HTTP attributes
tracing.SetAttributes(ctx,
tracing.HTTPMethod.String(c.Request.Method),
tracing.HTTPRoute.String("/users/:id"),
tracing.HTTPURL.String(c.Request.URL.String()),
)
// Get user
userID := c.Param("id")
user, err := h.service.GetUser(ctx, userID)
if err != nil {
tracing.RecordError(ctx, err)
tracing.SetAttributes(ctx, tracing.HTTPStatusCode.Int(500))
c.JSON(500, gin.H{"error": "Failed to get user"})
return
}
tracing.SetAttributes(ctx, tracing.HTTPStatusCode.Int(200))
c.JSON(200, user)
}
Database Query Tracing
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
// Start database span
ctx, span := tracing.StartSpan(ctx, "auth-service", "db.FindUser")
defer span.End()
// Add database attributes
tracing.SetAttributes(ctx,
tracing.DBSystem.String("postgresql"),
tracing.DBName.String("customer_db"),
tracing.DBOperation.String("SELECT"),
tracing.DBStatement.String("SELECT * FROM users WHERE id = $1"),
)
var user User
err := r.db.GetContext(ctx, &user, "SELECT * FROM users WHERE id = $1", id)
if err != nil {
tracing.RecordError(ctx, err)
return nil, err
}
return &user, nil
}
Service-to-Service Calls
func (c *UserServiceClient) GetUser(ctx context.Context, userID string) (*User, error) {
// Start span for external call
ctx, span := tracing.StartSpan(ctx, "order-service", "http.GetUser")
defer span.End()
url := fmt.Sprintf("%s/users/%s", c.baseURL, userID)
tracing.SetAttributes(ctx,
tracing.HTTPMethod.String("GET"),
tracing.HTTPURL.String(url),
)
// HTTP client automatically propagates span context
resp, err := c.httpClient.Get(ctx, url, nil)
if err != nil {
tracing.RecordError(ctx, err)
return nil, err
}
tracing.SetAttributes(ctx, tracing.HTTPStatusCode.Int(resp.StatusCode))
var user User
resp.JSON(&user)
return &user, nil
}
Kafka Event Tracing
func (p *EventPublisher) PublishUserRegistered(ctx context.Context, event UserRegisteredEvent) error {
// Start span
ctx, span := tracing.StartSpan(ctx, "auth-service", "kafka.PublishUserRegistered")
defer span.End()
tracing.SetAttributes(ctx,
attribute.String("messaging.system", "kafka"),
attribute.String("messaging.destination", "user.registered"),
tracing.UserID.String(event.UserID),
)
err := p.producer.Publish(ctx, "user.registered", event.UserID, event)
if err != nil {
tracing.RecordError(ctx, err)
return err
}
return nil
}
Predefined Attributes
// HTTP
tracing.HTTPMethod.String("GET")
tracing.HTTPStatusCode.Int(200)
tracing.HTTPRoute.String("/users/:id")
tracing.HTTPURL.String("https://api.laresto.com/users/123")
// Database
tracing.DBSystem.String("postgresql")
tracing.DBName.String("customer_db")
tracing.DBStatement.String("SELECT * FROM users")
tracing.DBOperation.String("SELECT")
// User
tracing.UserID.String("user-123")
tracing.UserEmail.String("user@example.com")
tracing.UserRole.String("admin")
// Business
tracing.OrderID.String("order-456")
tracing.VenueID.String("venue-789")
tracing.PaymentID.String("payment-012")
// Error
tracing.ErrorType.String("ValidationError")
tracing.ErrorMessage.String("Invalid email format")
Span Events
Add events to mark important moments:
ctx, span := tracing.StartSpan(ctx, "payment-service", "process-payment")
defer span.End()
tracing.AddEvent(ctx, "payment-validated",
attribute.String("method", "credit_card"),
)
// Process payment...
tracing.AddEvent(ctx, "payment-authorized",
attribute.String("authorization_code", "ABC123"),
)
tracing.AddEvent(ctx, "payment-completed",
attribute.Float64("amount", 99.99),
)
Error Recording
ctx, span := tracing.StartSpan(ctx, "order-service", "create-order")
defer span.End()
order, err := service.CreateOrder(ctx, req)
if err != nil {
// Record error in span
tracing.RecordError(ctx, err)
// Add error details
tracing.SetAttributes(ctx,
tracing.ErrorType.String("ValidationError"),
tracing.ErrorMessage.String(err.Error()),
)
return nil, err
}
Sampling
Control trace volume:
// Production: Sample 10% of requests
cfg := tracing.Config{
SampleRate: 0.1, // 10% of requests
}
// Development: Trace everything
cfg := tracing.Config{
SampleRate: 1.0, // 100% of requests
}
// High-traffic: Sample 1%
cfg := tracing.Config{
SampleRate: 0.01, // 1% of requests
}
Distributed Trace Example
User Request → API Gateway → Auth Service → Database
↓
User Service → Database
↓
Kafka → Email Service
Trace spans:
Trace ID: abc-123-def-456
├─ API Gateway [50ms]
│ └─ Auth Service [40ms]
│ ├─ DB: Check credentials [10ms]
│ └─ User Service [25ms]
│ └─ DB: Get user profile [5ms]
│ └─ Kafka: user.registered [5ms]
└─ Email Service [100ms]
└─ Send welcome email [95ms]
View in Jaeger UI: http://localhost:16686
Best Practices
DO ✅
// Start spans for significant operations
ctx, span := tracing.StartSpan(ctx, "service-name", "operation-name")
defer span.End()
// Add meaningful attributes
tracing.SetAttributes(ctx,
tracing.UserID.String(userID),
tracing.OrderID.String(orderID),
)
// Record errors
if err != nil {
tracing.RecordError(ctx, err)
}
// Use consistent span names
"http.GET /users/:id"
"db.FindUser"
"kafka.PublishEvent"
// Sample in production
SampleRate: 0.1 // 10% is usually enough
DON'T ❌
// Don't forget to end spans
ctx, span := tracing.StartSpan(ctx, "service", "op")
// Missing: defer span.End()
// Don't add sensitive data
tracing.SetAttributes(ctx,
attribute.String("password", password), // BAD!
attribute.String("credit_card", card), // BAD!
)
// Don't create too many spans
for item := range items {
ctx, span := tracing.StartSpan(ctx, "service", "process-item")
// Creates thousands of spans!
}
// Don't trace everything in production
SampleRate: 1.0 // Too much data in prod!
Testing
func TestTracing(t *testing.T) {
cfg := tracing.Config{
ServiceName: "test-service",
Enabled: false, // Disable in tests
}
provider, _ := tracing.NewProvider(cfg, logger.NewDefault())
defer provider.Shutdown(context.Background())
// Create spans (noop when disabled)
ctx, span := tracing.StartSpan(context.Background(), "test", "operation")
defer span.End()
// Still safe to call
tracing.SetAttributes(ctx, attribute.String("test", "value"))
}
Jaeger UI
Access: http://localhost:16686
Search traces:
- Service:
auth-service
- Operation:
GET /users/:id
- Tags:
user.id=123
- Duration:
> 100ms
View details:
- Full trace timeline
- All spans with duration
- Attributes and events
- Error information
Overhead:
- Enabled, not sampled: < 1µs per span
- Enabled, sampled: ~10-100µs per span
- Disabled: ~0µs (noop)
Best practices:
- Sample 10-20% in production
- Create spans for significant operations only
- Batch export to collector (automatic)
License
MIT License - see LICENSE file for details