๐Ÿ“š zax - Awesome Go Library for Logging

Go Gopher mascot for zax

Integrate Context with Zap logger, which leads to more flexibility in Go logging

๐Ÿท๏ธ Logging
๐Ÿ“‚ Logging
โญ 0 stars
View on GitHub ๐Ÿ”—

Detailed Description of zax

โšก Zax

Context-Aware Logging for Go with Uber's Zap

Go Version Go Reference codecov Go Report Card License: AGPL v3 GitHub release

CodeQL Check & Build


Zax seamlessly integrates Zap Logger with Go's context.Context, enabling you to carry structured logging fields across your entire request lifecycle without boilerplate.

Features โ€ข Installation โ€ข Tasks โ€ข Releases โ€ข Quick Start โ€ข API Reference โ€ข Benchmarks โ€ข Contributing


๐ŸŽฏ Why Zax?

In modern Go applications, especially microservices, you often need to:

  • ๐Ÿ” Trace requests across multiple functions and services
  • ๐Ÿ“Š Correlate logs with trace IDs, span IDs, and user context
  • ๐Ÿงน Avoid boilerplate by not passing loggers as function parameters
  • โšก Maintain performance without sacrificing structured logging

Zax solves these problems elegantly by storing Zap fields in context, making them available wherever you need to log.

โœจ Features

FeatureDescription
๐Ÿš€ Zero DependenciesOnly requires go.uber.org/zap
๐ŸŽฏ Context-NativeWorks seamlessly with Go's context.Context
โšก High PerformanceSmall overhead over direct Zap usage
๐Ÿ”ง Simple APIJust 5 functions to learn
๐Ÿฌ SugaredLogger SupportWorks with both *zap.Logger and *zap.SugaredLogger
๐Ÿงช Well TestedComprehensive test coverage

๐Ÿ“ฆ Installation

go get -u github.com/yuseferi/zax/v2

Requirements: Go 1.26.1 or higher

๐Ÿ›  Tasks

This project uses Task to keep local commands and CI in sync.

Install Task:

go run github.com/go-task/task/v3/cmd/task@latest --version

Common commands:

task build
task test
task test:race
task lint
task bench
task ci

task ci runs the same lint and test flow used by GitHub Actions.

๐Ÿš€ Releases

This project uses semantic-release to automate Git tags and GitHub releases.

Release automation is configured in .releaserc.json and runs from release.yml after the Quality check workflow succeeds on master.

Use Conventional Commits so semantic-release can determine the next version:

  • fix: for patch releases
  • feat: for minor releases
  • feat!: or a BREAKING CHANGE: footer for major releases

You can preview the next release locally with:

task release:dry-run

๐Ÿš€ Quick Start

package main

import (
    "context"
    
    "github.com/yuseferi/zax/v2"
    "go.uber.org/zap"
)

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    
    ctx := context.Background()
    
    // Add trace_id to context
    ctx = zax.Set(ctx, []zap.Field{
        zap.String("trace_id", "abc-123"),
        zap.String("user_id", "user-456"),
    })
    
    // Log with context fields - automatically includes trace_id and user_id
    logger.With(zax.Get(ctx)...).Info("request started")
    
    // Pass context to other functions
    processRequest(ctx, logger)
}

func processRequest(ctx context.Context, logger *zap.Logger) {
    // All logs automatically include trace_id and user_id!
    logger.With(zax.Get(ctx)...).Info("processing request")
    
    // Append additional fields without losing existing ones
    ctx = zax.Append(ctx, []zap.Field{
        zap.String("step", "validation"),
    })
    
    logger.With(zax.Get(ctx)...).Info("validation complete")
}

Output:

{"level":"info","msg":"request started","trace_id":"abc-123","user_id":"user-456"}
{"level":"info","msg":"processing request","trace_id":"abc-123","user_id":"user-456"}
{"level":"info","msg":"validation complete","trace_id":"abc-123","user_id":"user-456","step":"validation"}

๐Ÿ“– API Reference

Core Functions

Set(ctx, fields) context.Context

Stores zap fields in context. Replaces any existing fields.

ctx = zax.Set(ctx, []zap.Field{
    zap.String("trace_id", "my-trace-id"),
    zap.Int("request_num", 42),
})

Append(ctx, fields) context.Context

Appends fields to existing context fields. Preserves previously set fields.

// Existing: trace_id
ctx = zax.Append(ctx, []zap.Field{
    zap.String("span_id", "my-span-id"),
})
// Now has: trace_id + span_id

When the same key is added multiple times, later fields follow Zap's normal behavior and take precedence at log time.

Get(ctx) []zap.Field

Retrieves all stored fields from context.

fields := zax.Get(ctx)
logger.With(fields...).Info("message")

GetField(ctx, key) zap.Field

Retrieves a specific field by key.

traceField := zax.GetField(ctx, "trace_id")
fmt.Println(traceField.String) // "my-trace-id"

GetSugared(ctx) []interface{}

Returns fields as key-value pairs for SugaredLogger.

sugar := logger.Sugar()
sugar.With(zax.GetSugared(ctx)...).Info("sugared log")

GetSugared converts fields through Zap's encoder so common field types like strings, bools, numbers, errors, durations, and times are preserved.

๐Ÿ“Š Benchmarks

Run benchmarks with:

go test -bench . -run '^$' ./...

Example output on an Apple M2 Pro:

BenchmarkLoggingWithOnlyZap-10    28372430    44.73 ns/op
BenchmarkLoggingWithZax-10        10809844   118.1 ns/op

This keeps the package lightweight while adding a modest per-call cost for carrying context fields.

๐Ÿ”ฅ Real-World Example

HTTP Middleware with Distributed Tracing

package main

import (
    "context"
    "net/http"
    
    "github.com/yuseferi/zax/v2"
    "go.uber.org/zap"
)

type Server struct {
    logger *zap.Logger
}

// Middleware injects trace context into all requests
func (s *Server) TracingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        
        // Extract or generate trace ID
        traceID := r.Header.Get("X-Trace-ID")
        if traceID == "" {
            traceID = generateTraceID()
        }
        
        // Store in context
        ctx = zax.Set(ctx, []zap.Field{
            zap.String("trace_id", traceID),
            zap.String("method", r.Method),
            zap.String("path", r.URL.Path),
        })
        
        s.logger.With(zax.Get(ctx)...).Info("request received")
        
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Handler automatically has access to trace context
func (s *Server) HandleUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    // Add handler-specific context
    ctx = zax.Append(ctx, []zap.Field{
        zap.String("handler", "user"),
    })
    
    user, err := s.fetchUser(ctx)
    if err != nil {
        s.logger.With(zax.Get(ctx)...).Error("failed to fetch user", zap.Error(err))
        http.Error(w, "Internal Error", 500)
        return
    }
    
    s.logger.With(zax.Get(ctx)...).Info("user fetched successfully",
        zap.String("user_id", user.ID),
    )
}

func (s *Server) fetchUser(ctx context.Context) (*User, error) {
    // All logs here include trace_id, method, path, and handler!
    s.logger.With(zax.Get(ctx)...).Debug("querying database")
    // ... database logic
    return &User{}, nil
}

๐Ÿ“Š Benchmarks

Zax V2 is optimized for performance. Here's how it compares:

Benchmarkns/opB/opallocs/op
Pure Zap~351121
Zax V2~57722
Zax V1~651602

๐Ÿ’ก V2 uses 55% less memory than V1 by storing only fields instead of the entire logger object.

๐Ÿ“‹ Full Benchmark Results
pkg: github.com/yuseferi/zax/v2
BenchmarkLoggingWithOnlyZap-10          103801226               35.56 ns/op          112 B/op          1 allocs/op
BenchmarkLoggingWithOnlyZap-10          98576570                35.56 ns/op          112 B/op          1 allocs/op
BenchmarkLoggingWithOnlyZap-10          100000000               35.24 ns/op          112 B/op          1 allocs/op
BenchmarkLoggingWithOnlyZap-10          100000000               34.85 ns/op          112 B/op          1 allocs/op
BenchmarkLoggingWithOnlyZap-10          100000000               34.98 ns/op          112 B/op          1 allocs/op
BenchmarkLoggingWithZaxV2-10            64324434                56.02 ns/op           72 B/op          2 allocs/op
BenchmarkLoggingWithZaxV2-10            63939517                56.98 ns/op           72 B/op          2 allocs/op
BenchmarkLoggingWithZaxV2-10            63374052                57.60 ns/op           72 B/op          2 allocs/op
BenchmarkLoggingWithZaxV2-10            63417358                57.37 ns/op           72 B/op          2 allocs/op
BenchmarkLoggingWithZaxV2-10            57964246                57.97 ns/op           72 B/op          2 allocs/op
BenchmarkLoggingWithZaxV1-10            54062712                66.40 ns/op          160 B/op          2 allocs/op
BenchmarkLoggingWithZaxV1-10            53155524                65.61 ns/op          160 B/op          2 allocs/op
BenchmarkLoggingWithZaxV1-10            54428521                64.19 ns/op          160 B/op          2 allocs/op
BenchmarkLoggingWithZaxV1-10            55420744                64.28 ns/op          160 B/op          2 allocs/op
BenchmarkLoggingWithZaxV1-10            55199061                64.50 ns/op          160 B/op          2 allocs/op
PASS
ok      github.com/yuseferi/zax/v2      56.919s

๐Ÿค Contributing

We โค๏ธ contributions! Here's how you can help:

  1. ๐Ÿด Fork the repository
  2. ๐ŸŒฟ Create a feature branch (git checkout -b feature/amazing-feature)
  3. ๐Ÿ’ป Commit your changes (git commit -m 'Add amazing feature')
  4. ๐Ÿ“ค Push to the branch (git push origin feature/amazing-feature)
  5. ๐ŸŽ‰ Open a Pull Request

Development

# Clone the repository
git clone https://github.com/yuseferi/zax.git
cd zax

# Run tests
go test -v ./...

# Run benchmarks
go test -bench=. -benchmem

# Run linter
golangci-lint run

๐Ÿ“„ License

This project is licensed under the GNU Affero General Public License v3.0 - see the LICENSE file for details.


Made with โค๏ธ by Yusef Mohamadi and contributors

โญ Star this repo if you find it useful!

Report Bug โ€ข Request Feature