⚡ Zax
August 31, 2026 · View on GitHub
⚡ Zax
Context-Aware Logging for Go with Uber's Zap
Zax seamlessly integrates Zap Logger with Go's context.Context, enabling you to carry structured logging fields across your entire request lifecycle without boilerplate.
📚 Table of Contents
- Why Zax?
- Features
- Installation
- Tasks
- Releases
- Quick Start
- API Reference
- Real-World Example
- Benchmarks
- Contributing
- License
🎯 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
| Feature | Description |
|---|---|
| 🚀 Zero Dependencies | Only requires go.uber.org/zap |
| 🎯 Context-Native | Works seamlessly with Go's context.Context |
| ⚡ High Performance | Minimal, predictable overhead (see Benchmarks) |
| 🔧 Simple API | Just 7 functions to learn |
| 🍬 SugaredLogger Support | Works with both *zap.Logger and *zap.SugaredLogger |
| 🧪 Well Tested | Comprehensive 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 .github/workflows/release.yml after the Quality check workflow succeeds on master.
Use Conventional Commits so semantic-release can determine the next version:
fix:for patch releasesfeat:for minor releasesfeat!:or aBREAKING 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. Returns the zero-value zap.Field when the key is not present.
traceField := zax.GetField(ctx, "trace_id")
fmt.Println(traceField.String) // "my-trace-id"
LookupField(ctx, key) (zap.Field, bool)
Like GetField, but also reports whether the key was found — useful when a
stored field may legitimately hold a zero value.
if field, ok := zax.LookupField(ctx, "attempt"); ok {
fmt.Println(field.Integer)
}
Remove(ctx, keys...) context.Context
Returns a context with the given keys removed from the stored fields. Handy for scrubbing sensitive data (e.g. PII) before passing a context on.
ctx = zax.Remove(ctx, "user_email", "api_token")
GetSugared(ctx) []any
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.
🔥 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:
Run benchmarks yourself with:
task bench
# or
go test -bench . -run '^$' -benchmem ./...
| Benchmark | ns/op | B/op | allocs/op |
|---|---|---|---|
| Pure Zap | ~43 | 128 | 1 |
| Zax V2 | ~226 | 584 | 5 |
💡 The extra allocations come from defensively cloning fields on
Set/Append/Getand from storing them in the context, so callers can never mutate fields after they are stored. If you need the absolute minimum overhead, pass zap fields directly.
📋 Full Benchmark Results
Measured on an Apple M2 Pro with Go 1.26 and zap v1.28.0:
pkg: github.com/yuseferi/zax/v2
BenchmarkLoggingWithOnlyZap-10 84292609 43.15 ns/op 128 B/op 1 allocs/op
BenchmarkLoggingWithZax-10 15688470 226.2 ns/op 584 B/op 5 allocs/op
PASS
🤝 Contributing
We ❤️ contributions! Here's how you can help:
- 🍴 Fork the repository
- 🌿 Create a feature branch (
git checkout -b feature/amazing-feature) - 💻 Commit your changes (
git commit -m 'Add amazing feature') - 📤 Push to the branch (
git push origin feature/amazing-feature) - 🎉 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!