Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

72 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

lecho πŸ…

A high-performance Zerolog wrapper for Echo web framework that provides structured logging with minimal overhead.

Features

  • πŸš€ High Performance - Built on top of zerolog, one of the fastest structured loggers for Go
  • πŸ“Š Structured Logging - JSON formatted logs with rich contextual information
  • πŸ”— Request Correlation - Automatic request ID tracking and context propagation
  • ⚑ Low Latency - Minimal overhead request/response logging
  • πŸ›  Highly Configurable - Extensive middleware configuration options
  • πŸ” Request Enrichment - Add custom fields based on request context
  • πŸ“ˆ Performance Monitoring - Built-in slow request detection and alerting

Table of Contents

Installation

go get github.com/ziflex/lecho/v4

Previous Versions

Version Branch
v3 v3
v2 v2
v1 v1

Quick Start

Basic Usage

Replace Echo's default logger with lecho for structured logging:

package main

import (
	"os"
	"github.com/labstack/echo/v5"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	e.Logger = lecho.New(os.Stdout).Slog()
	
	// Your routes and middleware here
	e.Start(":8080")
}

Using Existing Zerolog Instance

If you already have a zerolog logger configured, you can wrap it with lecho:

package main

import (
	"os"
	"github.com/labstack/echo/v5"
	"github.com/rs/zerolog"
	"github.com/ziflex/lecho/v4"
)

func main() {
	// Configure your zerolog instance
	log := zerolog.New(os.Stdout).With().Timestamp().Logger()
	
	e := echo.New()
	e.Logger = lecho.From(log).Slog()
	
	e.Start(":8080")
}

Slog returns an Echo v5-compatible *slog.Logger backed by the current Zerolog configuration. Configure the lecho logger before calling Slog.

Options

Lecho provides several configuration options to customize logging behavior:

package main

import (
	"os"
	"github.com/labstack/echo/v5"
	"github.com/rs/zerolog"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	logger := lecho.New(
		os.Stdout,
		lecho.WithLevel(zerolog.DebugLevel),                           // Set log level
		lecho.WithFields(map[string]any{"service": "api"}),            // Add default fields
		lecho.WithTimestamp(),                                         // Add timestamp to logs
		lecho.WithCaller(),                                           // Add caller information
		lecho.WithPrefix("MyApp"),                                    // Add a prefix to logs
		// lecho.WithHook(myHook),                                    // Add custom hooks
		// lecho.WithHookFunc(myHookFunc),                            // Add hook functions
	)
	e.Logger = logger.Slog()

	log := logger.Unwrap()
	log.Info().Msg("Application logger configured")
	
	e.Start(":8080")
}

Available Options

  • WithLevel(level zerolog.Level) - Set the minimum Zerolog level
  • WithFields(fields map[string]any) - Add default fields to all log entries
  • WithField(key string, value any) - Add a single default field
  • WithTimestamp() - Include timestamp in log entries
  • WithCaller() - Include caller file and line information
  • WithCallerWithSkipFrameCount(count int) - Include caller info with custom skip frame count
  • WithPrefix(prefix string) - Add a prefix field to all log entries
  • WithHook(hook zerolog.Hook) - Add a custom zerolog hook
  • WithHookFunc(hookFunc zerolog.HookFunc) - Add a custom hook function

Runtime Configuration

The logger exposes Zerolog-native level and output helpers:

logger := lecho.New(os.Stdout)

logger.SetLevel(zerolog.WarnLevel)
level := logger.Level()

logger.SetOutput(os.Stderr)
writer := logger.Output()

Output returns the wrapped Zerolog logger as an io.Writer, preserving its configured fields, hooks, and level. SetLevel and SetOutput replace the logger configuration used by future calls. Values already returned by Unwrap, Output, Slog, or WithContext remain snapshots of the earlier configuration.

Configure the logger before using it concurrently or assigning logger.Slog() to Echo.

Middleware

The lecho middleware provides automatic request logging with rich contextual information. It integrates seamlessly with Echo's request lifecycle and supports various customization options.

Basic Request Logging

package main

import (
	"net/http"
	"os"
	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
	"github.com/rs/zerolog"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	
	// Create and configure logger
	logger := lecho.New(
		os.Stdout,
		lecho.WithLevel(zerolog.DebugLevel),
		lecho.WithTimestamp(),
		lecho.WithCaller(),
	)
	e.Logger = logger.Slog()
	
	// Add request ID middleware (optional but recommended)
	e.Use(middleware.RequestID())
	
	// Add lecho middleware for request logging
	e.Use(lecho.Middleware(lecho.Config{
		Logger: logger,
	}))
	
	// Example route
	e.GET("/", func(c *echo.Context) error {
		// Log using Echo's slog logger
		c.Logger().Info("Processing request")
		
		// Or use zerolog directly from context
		lecho.Ctx(c.Request().Context()).Info().Msg("Using zerolog interface")
		
		return c.String(http.StatusOK, "Hello, World!")
	})
	
	e.Start(":8080")
}

Sample output:

{"level":"info","id":"123e4567-e89b-12d3-a456-426614174000","remote_ip":"127.0.0.1","host":"localhost:8080","method":"GET","uri":"/","user_agent":"curl/7.68.0","status":200,"referer":"","latency":1.234,"latency_human":"1.234ms","bytes_in":"0","bytes_out":"13","time":"2023-10-15T10:30:00Z"}

Escalate Log Level for Slow Requests

Monitor and highlight slow requests by logging them at a higher level when they exceed a specified duration:

package main

import (
	"os"
	"time"
	"github.com/labstack/echo/v5"
	"github.com/rs/zerolog"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	logger := lecho.New(os.Stdout, lecho.WithTimestamp())
	
	e.Use(lecho.Middleware(lecho.Config{
		Logger:              logger,
		RequestLatencyLevel: zerolog.WarnLevel,        // Log level for slow requests
		RequestLatencyLimit: 500 * time.Millisecond,  // Threshold for slow requests
	}))
	
	// Requests taking longer than 500ms will be logged at WARN level
	// instead of the default INFO level
}

Output for slow request:

{"level":"warn","remote_ip":"127.0.0.1","method":"GET","uri":"/slow","status":200,"latency":750.123,"latency_human":"750.123ms","time":"2023-10-15T10:30:00Z"}

Nesting Under a Sub Dictionary

Organize request information under a nested key for better log structure:

package main

import (
	"os"
	"github.com/labstack/echo/v5"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	logger := lecho.New(os.Stdout, lecho.WithTimestamp())
	
	e.Use(lecho.Middleware(lecho.Config{
		Logger:  logger,
		NestKey: "request", // Nest all request info under this key
	}))
	
	// All request-related fields will be nested under "request"
}

Sample output:

{"level":"info","request":{"remote_ip":"127.0.0.1","method":"GET","uri":"/api/users","status":200,"latency":15.234,"latency_human":"15.234ms"},"time":"2023-10-15T10:30:00Z"}

Enricher

The Enricher function allows you to add custom fields to log entries based on request context. This is useful for adding user IDs, trace IDs, or other contextual information:

package main

import (
	"os"
	"github.com/labstack/echo/v5"
	"github.com/rs/zerolog"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	logger := lecho.New(os.Stdout, lecho.WithTimestamp())
	
	e.Use(lecho.Middleware(lecho.Config{
		Logger: logger,
		Enricher: func(c *echo.Context, logger zerolog.Context) zerolog.Context {
			// Add user ID if available in context
			if userID := c.Get("user_id"); userID != nil {
				logger = logger.Str("user_id", userID.(string))
			}
			
			// Add trace ID from header
			if traceID := c.Request().Header.Get("X-Trace-ID"); traceID != "" {
				logger = logger.Str("trace_id", traceID)
			}
			
			return logger
		},
	}))
	
	// Set up routes that use user context
	e.GET("/api/profile", func(c *echo.Context) error {
		c.Set("user_id", "user123") // This will be logged
		return c.JSON(200, map[string]string{"status": "ok"})
	})
}

AfterNextEnricher

The AfterNextEnricher function allows you to add custom fields to log entries after the next handler has executed. This is useful for adding fields that depend on the outcome of the request processing, such as response status or values set during request handling.

package main

import (
    "os"
    "github.com/labstack/echo/v5"
    "github.com/rs/zerolog"
    "github.com/ziflex/lecho/v4"
)

func main() {
    e := echo.New()
    logger := lecho.New(os.Stdout, lecho.WithTimestamp())
    
    e.Use(lecho.Middleware(lecho.Config{
        Logger: logger,
        AfterNextEnricher: func(c *echo.Context, logger zerolog.Context) zerolog.Context {
            // Add response status code after the handler has executed
            if response, err := echo.UnwrapResponse(c.Response()); err == nil {
                logger = logger.Int("status", response.Status)
            }
            return logger
        },
    }))
    
    e.GET("/api/profile", func(c *echo.Context) error {
        c.Set("user_id", "user123") // This will be logged
        return c.JSON(200, map[string]string{"status": "ok"})
    })
}

Sample output:

{"level":"info","user_id":"user123","trace_id":"abc-def-123","remote_ip":"127.0.0.1","method":"GET","uri":"/api/profile","status":200,"time":"2023-10-15T10:30:00Z"}

Error Handling

Control how errors are handled in the middleware chain. By default, lecho logs errors but doesn't propagate them to Echo's error handler:

package main

import (
	"errors"
	"net/http"
	"os"
	"github.com/labstack/echo/v5"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	logger := lecho.New(os.Stdout, lecho.WithTimestamp())
	
	// Configure error handling
	e.Use(lecho.Middleware(lecho.Config{
		Logger:      logger,
		HandleError: true, // Propagate errors to Echo's error handler
	}))
	
	// Custom error handler
	e.HTTPErrorHandler = func(c *echo.Context, err error) {
		code := http.StatusInternalServerError
		if he, ok := err.(*echo.HTTPError); ok {
			code = he.Code
		}
		c.JSON(code, map[string]string{"error": err.Error()})
	}
	
	// Route that may return an error
	e.GET("/error", func(c *echo.Context) error {
		return errors.New("something went wrong")
	})
}

With HandleError: false (default): Errors are logged but not propagated to Echo's error handler.
With HandleError: true: Errors are both logged and passed to Echo's error handler for proper HTTP response handling.

Skipping Built-in Request Fields

Use SkipDefaultFields when you want full control over which request attributes are emitted, for example when you need to log OpenTelemetry-style field names instead of lecho's built-in keys.

e.Use(lecho.Middleware(lecho.Config{
	Logger:            logger,
	SkipDefaultFields: true,
	Enricher: func(c *echo.Context, logger zerolog.Context) zerolog.Context {
		return logger.
			Str("http.request.method", c.Request().Method).
			Str("url.path", c.Request().URL.Path)
	},
}))

When SkipDefaultFields is enabled:

  • built-in request fields such as method, status, latency, and bytes_out are not added automatically
  • RequestIDHeader / RequestIDKey are ignored
  • NestKey is ignored because only built-in fields can be nested
  • at least one of Enricher or AfterNextEnricher must be configured

Middleware Configuration Options

The lecho.Config struct provides extensive customization options:

Option Type Description Default
Logger *lecho.Logger Custom logger instance lecho.New(os.Stdout, lecho.WithTimestamp())
Skipper middleware.Skipper Function to skip middleware middleware.DefaultSkipper
AfterNextSkipper middleware.Skipper Skip logging after handler execution middleware.DefaultSkipper
BeforeNext middleware.BeforeFunc Function executed before next handler nil
Enricher lecho.Enricher Function to add custom fields nil
AfterNextEnricher lecho.Enricher Function to add custom fields after the next handler runs; invoked only if AfterNextSkipper returns false nil
RequestIDHeader string Header name for request ID; used only if SkipDefaultFields is set to false "X-Request-ID"
RequestIDKey string JSON key for request ID in logs; used only if SkipDefaultFields is set to false "id"
NestKey string Key for nesting request fields. Will only affect built-in fields (SkipDefaultFields is set to false) "" (no nesting)
HandleError bool Propagate errors to error handler false
RequestLatencyLimit time.Duration Threshold for slow request detection 0 (disabled)
RequestLatencyLevel zerolog.Level Log level for slow requests zerolog.InfoLevel
SkipDefaultFields bool Disable built-in request fields (remote_ip, host, method, uri, user_agent, status, referer, latency, bytes_in, bytes_out); only fields added through Enricher or AfterNextEnricher are logged, and at least one of them must be configured false

Helpers

Context Logger Access

The middleware installs the request-scoped logger in both Echo's native context and the request's standard library context; no custom Echo context wrapper is required:

e.GET("/api/users", func(c *echo.Context) error {
	// Method 1: Using Echo's slog logger
	c.Logger().Info("Fetching users")
	
	// Method 2: Using zerolog directly from request context
	lecho.Ctx(c.Request().Context()).Info().Str("action", "fetch_users").Msg("Processing request")
	
	return c.JSON(200, []string{"user1", "user2"})
})

Advanced Configuration

Complete Configuration Example

package main

import (
	"net/http"
	"os"
	"time"
	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
	"github.com/rs/zerolog"
	"github.com/ziflex/lecho/v4"
)

func main() {
	e := echo.New()
	
	// Configure logger with all options
	logger := lecho.New(
		os.Stdout,
		lecho.WithLevel(zerolog.InfoLevel),
		lecho.WithTimestamp(),
		lecho.WithCaller(),
		lecho.WithFields(map[string]any{
			"service": "api",
			"version": "1.0.0",
		}),
	)
	e.Logger = logger.Slog()
	
	// Add middleware stack
	e.Use(middleware.RequestID())
	e.Use(lecho.Middleware(lecho.Config{
		Logger:              logger,
		HandleError:         true,
		RequestLatencyLevel: zerolog.WarnLevel,
		RequestLatencyLimit: 200 * time.Millisecond,
		NestKey:            "http",
		Enricher: func(c *echo.Context, logger zerolog.Context) zerolog.Context {
			if userID := c.Get("user_id"); userID != nil {
				logger = logger.Str("user_id", userID.(string))
			}
			return logger
		},
		AfterNextEnricher: func(c *echo.Context, logger zerolog.Context) zerolog.Context {
			// Example of adding a field after the next handler runs, based on context value
			logger = logger.Interface("some_key", c.Get("some_key"))
			return logger
		},
		Skipper: func(c *echo.Context) bool {
			// Skip logging for health check endpoints
			return c.Request().URL.Path == "/health"
		},
	}))
	e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c *echo.Context) error {
			c.Set("some_key", "some_value") // Example of setting context value for after next enricher
			return next(c)
		}
	})
	
	e.GET("/health", func(c *echo.Context) error {
		return c.String(http.StatusOK, "OK")
	})
	
	e.GET("/api/slow", func(c *echo.Context) error {
		time.Sleep(300 * time.Millisecond) // Simulates slow operation
		return c.JSON(200, map[string]string{"result": "completed"})
	})
	
	e.Start(":8080")
}

About

Zerolog wrapper for Echo framework πŸ…

Topics

Resources

Stars

112 stars

Watchers

3 watching

Forks

Releases

Used by

Contributors

Languages