Skip to main content

Echo Request Tracing

Introduction

Echo request tracing is a powerful debugging and monitoring technique that allows developers to track the journey of HTTP requests through a web application. By implementing tracing, you can observe how requests propagate through different components of your system, measure performance, identify bottlenecks, and troubleshoot issues more effectively.

In this guide, we'll explore how to implement and use echo request tracing in your applications. Whether you're building a simple web service or a complex microservice architecture, understanding request tracing will help you gain visibility into your application's behavior.

What is Echo Request Tracing?

Echo request tracing is the process of following a request's path as it travels through your application, recording information about each step along the way. This creates a "trace" - a collection of spans that represent operations performed during the request's lifecycle.

Key concepts in request tracing include:

  • Trace: A complete record of a request's journey through your system
  • Span: A single operation within a trace (e.g., database query, API call)
  • Context: Information that's propagated across service boundaries
  • Sampling: The process of selecting which requests to trace

Basic Echo Request Tracing

Let's start with a simple example of how to implement basic request tracing in an Echo application:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)

func main() {
// Create a new Echo instance
e := echo.New()

// Add request ID middleware
e.Use(middleware.RequestID())

// Add logger middleware with request ID
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "time=${time_rfc3339}, id=${id}, method=${method}, uri=${uri}, status=${status}\n",
}))

// Route handlers
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

// Start server
e.Logger.Fatal(e.Start(":1323"))
}

When you run this application and send a request to http://localhost:1323/, you'll see log output like:

time=2023-11-05T14:32:18Z, id=1fpw78veqyd3c5fsa9zz, method=GET, uri=/, status=200

The id in this log is the request ID, which is automatically generated for each request and can be used to trace that request through your application logs.

Advanced Request Tracing with OpenTelemetry

For more comprehensive tracing, you can integrate Echo with OpenTelemetry, an industry-standard observability framework:

go
package main

import (
"context"
"github.com/labstack/echo/v4"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/attribute"
"log"
"net/http"
)

func initTracer() (*trace.TracerProvider, error) {
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
if err != nil {
return nil, err
}

tp := trace.NewTracerProvider(
trace.WithSampler(trace.AlwaysSample()),
trace.WithBatcher(exporter),
)
otel.SetTracerProvider(tp)
return tp, nil
}

func main() {
tp, err := initTracer()
if err != nil {
log.Fatal(err)
}
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Printf("Error shutting down tracer provider: %v", err)
}
}()

tracer := tp.Tracer("echo-example")

// Create a new Echo instance
e := echo.New()

// Add middleware to handle tracing
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
req := c.Request()
ctx, span := tracer.Start(req.Context(), "http "+req.Method+" "+c.Path())
defer span.End()

// Add attributes to the span
span.SetAttributes(
attribute.String("http.method", req.Method),
attribute.String("http.path", c.Path()),
)

// Update request with traced context
c.SetRequest(req.WithContext(ctx))

// Call the next handler
return next(c)
}
})

// Route handlers with child spans
e.GET("/", func(c echo.Context) error {
ctx := c.Request().Context()

// Create a child span for database operation
_, dbSpan := tracer.Start(ctx, "database-query")
// Simulate database query
// time.Sleep(100 * time.Millisecond)
dbSpan.End()

return c.String(http.StatusOK, "Hello, World with tracing!")
})

// Start server
e.Logger.Fatal(e.Start(":1323"))
}

This example sets up OpenTelemetry tracing with the following components:

  1. A tracer provider with a console exporter
  2. Middleware that creates a span for each HTTP request
  3. Child spans for operations within handlers

When you run this application and make requests, you'll see detailed trace information printed to the console, including the request span and any child spans.

Distributed Tracing

In microservice architectures, requests often span multiple services. Distributed tracing allows you to track these requests as they flow between services. Here's how to implement it:

go
package main

import (
"context"
"github.com/labstack/echo/v4"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"log"
"net/http"
)

func main() {
// Setup tracer (similar to previous example)
// ...

// Create a new Echo instance
e := echo.New()

// Configure propagator
otel.SetTextMapPropagator(propagation.TraceContext{})

// Add middleware to extract trace context from incoming requests
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
req := c.Request()
ctx := req.Context()

// Extract trace context from headers
ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(req.Header))

// Start a new span
tracer := otel.Tracer("service-a")
ctx, span := tracer.Start(ctx, "service-a-handler")
defer span.End()

// Update request context
c.SetRequest(req.WithContext(ctx))

return next(c)
}
})

// Route that calls another service
e.GET("/call-service-b", func(c echo.Context) error {
ctx := c.Request().Context()

// Create HTTP client with tracing
client := http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}

// Create request to Service B
req, err := http.NewRequestWithContext(ctx, "GET", "http://service-b:8080/endpoint", nil)
if err != nil {
return err
}

// Make the request
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

return c.String(http.StatusOK, "Called Service B successfully")
})

// Start server
e.Logger.Fatal(e.Start(":1323"))
}

This example shows how to:

  1. Extract trace context from incoming requests
  2. Create spans within the current service
  3. Propagate trace context to downstream services via HTTP headers

Real-World Application: Tracing a User Authentication Flow

Let's see how request tracing can be applied to a real-world scenario: tracing a user authentication flow.

go
package main

import (
"context"
"github.com/labstack/echo/v4"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"net/http"
"time"
)

// Simplified user authentication service
type AuthService struct {
tracer trace.Tracer
}

func NewAuthService(tracer trace.Tracer) *AuthService {
return &AuthService{tracer: tracer}
}

func (s *AuthService) Authenticate(ctx context.Context, username, password string) (bool, error) {
ctx, span := s.tracer.Start(ctx, "authenticate-user")
defer span.End()

// Add attributes to help with debugging
span.SetAttributes(
attribute.String("user", username),
attribute.Int("password.length", len(password)),
)

// Child span for database lookup
_, dbSpan := s.tracer.Start(ctx, "db.find-user")
// Simulate database query
time.Sleep(50 * time.Millisecond)
dbSpan.End()

// Child span for password verification
ctx, verifySpan := s.tracer.Start(ctx, "verify-password")
// Simulate password hashing and comparison
time.Sleep(100 * time.Millisecond)

// Record the outcome
success := username == "admin" && password == "password"
verifySpan.SetAttributes(attribute.Bool("auth.success", success))
verifySpan.End()

return success, nil
}

func main() {
// Setup tracer
tracer := otel.Tracer("auth-service")

// Create authentication service
authService := NewAuthService(tracer)

// Create Echo instance
e := echo.New()

// Login endpoint
e.POST("/login", func(c echo.Context) error {
ctx, span := tracer.Start(c.Request().Context(), "login-handler")
defer span.End()

// Extract credentials
username := c.FormValue("username")
password := c.FormValue("password")

// Authenticate user
success, err := authService.Authenticate(ctx, username, password)
if err != nil {
span.RecordError(err)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Authentication service error",
})
}

if !success {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}

return c.JSON(http.StatusOK, map[string]string{
"message": "Login successful",
"token": "sample-jwt-token",
})
})

// Start server
e.Logger.Fatal(e.Start(":1323"))
}

In this example:

  1. We create a dedicated AuthService with tracing capabilities
  2. Each step of the authentication process creates its own span
  3. Important details are recorded as span attributes
  4. Errors are properly recorded in the spans

With this implementation, you can visualize the complete authentication flow, see how long each step takes, and quickly identify issues when authentication fails.

Visualizing Traces

While console output is useful for development, in production environments you'll want to send traces to a proper tracing backend. Popular options include:

  • Jaeger
  • Zipkin
  • Datadog
  • New Relic

Here's a quick example of how to configure Echo to send traces to Jaeger:

go
package main

import (
"context"
"github.com/labstack/echo/v4"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
"log"
)

func initJaegerTracer() (*trace.TracerProvider, error) {
// Configure Jaeger exporter
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://jaeger:14268/api/traces")))
if err != nil {
return nil, err
}

// Create a trace provider with the exporter
tp := trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("my-service"),
semconv.ServiceVersionKey.String("1.0.0"),
)),
)

// Set the global tracer provider
otel.SetTracerProvider(tp)
return tp, nil
}

func main() {
tp, err := initJaegerTracer()
if err != nil {
log.Fatal(err)
}
defer tp.Shutdown(context.Background())

// Create Echo application with tracing middleware
// ...
}

With this setup, you can view your traces in the Jaeger UI, which provides a visual representation of your request flows, making it easier to identify performance issues and bottlenecks.

Best Practices for Echo Request Tracing

To get the most out of request tracing in your Echo applications:

  1. Name spans meaningfully: Use descriptive names that identify the operation being performed.

  2. Add relevant attributes: Include information that will help you troubleshoot issues:

    go
    span.SetAttributes(
    attribute.String("user.id", userID),
    attribute.Int64("items.count", itemsCount),
    )
  3. Record errors properly: When errors occur, record them in the span:

    go
    if err != nil {
    span.RecordError(err)
    span.SetStatus(codes.Error, err.Error())
    }
  4. Set appropriate sampling rates: In high-traffic production environments, trace only a percentage of requests:

    go
    trace.WithSampler(trace.TraceIDRatioBased(0.1)) // 10% sampling
  5. Propagate context: Always pass the trace context down the call chain.

  6. Monitor trace data size: Large traces can impact performance, so be selective about what you record.

Summary

Echo request tracing provides invaluable insights into how requests flow through your application. By implementing proper tracing, you can:

  • Visualize request flows across services
  • Measure performance at each step
  • Identify bottlenecks and performance issues
  • Debug complex interactions between components
  • Monitor system health in production

In this guide, we covered the fundamentals of request tracing, from basic request ID logging to advanced distributed tracing with OpenTelemetry. We also explored practical applications and best practices to help you implement effective tracing in your Echo applications.

Additional Resources

Exercises

  1. Implement basic request ID tracing in a simple Echo application and observe how requests are logged.
  2. Set up OpenTelemetry tracing with console output and create spans for different operations in your application.
  3. Create a multi-service application and implement distributed tracing between services.
  4. Set up Jaeger in a Docker container and configure your application to send traces to it.
  5. Implement tracing for a real-world workflow in your application, such as a checkout process or user registration flow.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)