goctxid
A lightweight Go middleware for managing and propagating request/correlation IDs through context.Context.
goctxid provides a simple way to ensure every request has a unique identifier, making your services observable and traceable. It's built on the standard context.Context package, making it compatible with any Go HTTP framework (with adapters included for popular frameworks like Fiber).
π Features
- Framework Agnostic: Core logic is built on standard
context.Context.
- Multiple Framework Support:
- β
Fiber - Two adapters available:
adapters/fiber - Context-based (standard approach)
adapters/fibernative - Fiber-native using c.Locals() (better performance)
- β
Standard
net/http (no adapter needed - use core package directly)
- β
Echo (adapter in
adapters/echo)
- β
Gin (adapter in
adapters/gin)
- π§ Easy to create adapters for other frameworks (Chi, Gorilla, etc.)
- Extract or Generate: Automatically extracts an existing ID from request headers (e.g.,
X-Correlation-ID) or generates a new one if not found.
- Propagation:
- Injects the ID into the
context.Context (via c.UserContext() in Fiber) for use in your application logic (logging, downstream API calls).
- Adds the ID to the response headers so clients (like web frontends or mobile apps) can also use it for debugging.
- Customizable:
- Easily change the default header key (e.g., use
X-Request-ID, X-Trace-ID).
- Provide your own custom ID generator function (e.g., UUID, nanoid).
π¦ Installation
go get github.com/hiiamtin/goctxid
π― Quick Start
Basic Usage with Fiber
package main
import (
"log"
"github.com/gofiber/fiber/v2"
goctxid_fiber "github.com/hiiamtin/goctxid/adapters/fiber"
)
func main() {
app := fiber.New()
// Add goctxid middleware
app.Use(goctxid_fiber.New())
app.Get("/", func(c *fiber.Ctx) error {
// β¨ No need to import goctxid package!
// Use re-exported function directly from adapter
correlationID := goctxid_fiber.MustFromContext(c.UserContext())
return c.JSON(fiber.Map{
"message": "Hello, World!",
"correlation_id": correlationID,
})
})
log.Fatal(app.Listen(":3000"))
}
π‘ Pro Tip: All adapters re-export common functions (FromContext, MustFromContext, NewContext) and constants (DefaultHeaderKey), so you don't need to import the base goctxid package separately!
Custom Configuration
app.Use(goctxid_fiber.New(goctxid_fiber.Config{
Config: goctxid.Config{
HeaderKey: "X-Request-ID", // Custom header name
Generator: func() string { // Custom ID generator
return "REQ-" + uuid.NewString()
},
},
}))
β‘ Advanced Features
Skip Middleware for Specific Requests (Next Function)
Save ~400-500 ns per request by skipping middleware for health checks, metrics, or static files:
app.Use(goctxid_fiber.New(goctxid_fiber.Config{
Next: func(c *fiber.Ctx) bool {
// Skip middleware for these paths
path := c.Path()
return path == "/health" || path == "/metrics"
},
}))
Available for all adapters:
- Fiber:
Next func(c *fiber.Ctx) bool
- Echo:
Next func(c echo.Context) bool
- Gin:
Next func(c *gin.Context) bool
For high-throughput systems, use FastGenerator for ~33% faster ID generation:
app.Use(goctxid_fiber.New(goctxid_fiber.Config{
Config: goctxid.Config{
Generator: goctxid.FastGenerator, // Fast but exposes request count
},
}))
β οΈ Privacy Warning: FastGenerator uses an atomic counter and exposes your request count. Use only when:
- Performance is critical (high-throughput systems)
- Request count exposure is acceptable
- IDs are used only for internal tracing (not exposed to clients)
Performance Comparison:
FastGenerator: 234 ns/op (single-threaded), 149 ns/op (parallel)
DefaultGenerator: 349 ns/op (single-threaded), 731 ns/op (parallel)
For most applications, use DefaultGenerator (UUID v4) for better privacy/security.
Custom LocalsKey (Fiber Native Only)
Prevent collisions when using fibernative adapter:
app.Use(fibernative.New(fibernative.Config{
LocalsKey: "my_correlation_id", // Custom key to avoid collisions
}))
// Retrieve with custom key
id := fibernative.MustFromLocalsWithKey(c, "my_correlation_id")
See examples/advanced-features for complete examples.
π Framework Support
Using with Different Frameworks
Fiber (Context-Based)
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/hiiamtin/goctxid"
goctxid_fiber "github.com/hiiamtin/goctxid/adapters/fiber"
)
func main() {
app := fiber.New()
// Add middleware (context-based)
app.Use(goctxid_fiber.New())
app.Get("/", func(c *fiber.Ctx) error {
correlationID := goctxid.MustFromContext(c.UserContext())
return c.SendString("Correlation ID: " + correlationID)
})
app.Listen(":3000")
}
package main
import (
"github.com/gofiber/fiber/v2"
goctxid_fibernative "github.com/hiiamtin/goctxid/adapters/fibernative"
)
func main() {
app := fiber.New()
// Add middleware (Fiber-native using c.Locals())
app.Use(goctxid_fibernative.New())
app.Get("/", func(c *fiber.Ctx) error {
// Access ID directly from Locals (more performant!)
correlationID := goctxid_fibernative.MustFromLocals(c)
return c.SendString("Correlation ID: " + correlationID)
})
app.Listen(":3000")
}
Which Fiber adapter should I use?
| Adapter |
Storage |
Use Case |
Performance |
Goroutine Safety |
adapters/fiber |
context.Context |
Standard patterns, compatibility with other middleware |
Good |
β
Safe (context is immutable) |
adapters/fibernative |
c.Locals() |
Fiber-native, maximum performance |
Better (17% faster) |
β οΈ Must copy values before goroutines |
See complete example: examples/fiber-native
β οΈ Important: Goroutine Safety with fibernative
The fibernative adapter uses c.Locals() which is NOT safe to use directly in goroutines because Fiber recycles the context after the handler completes.
// β WRONG - Don't do this:
app.Get("/", func(c *fiber.Ctx) error {
go func() {
// β οΈ DANGER: c may be recycled!
id := goctxid_fibernative.MustFromLocals(c)
log.Println(id)
}()
return c.SendString("OK")
})
// β
CORRECT - Copy the value first:
app.Get("/", func(c *fiber.Ctx) error {
correlationID := goctxid_fibernative.MustFromLocals(c)
go func() {
// Safe to use the copied value
log.Println(correlationID)
}()
return c.SendString("OK")
})
If you need to use correlation IDs in goroutines frequently, consider using the context-based adapter (adapters/fiber) instead.
Standard net/http
package main
import (
"net/http"
"github.com/hiiamtin/goctxid"
)
func correlationIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get or generate correlation ID
correlationID := r.Header.Get(goctxid.DefaultHeaderKey)
if correlationID == "" {
correlationID = goctxid.DefaultGenerator()
}
// Set response header
w.Header().Set(goctxid.DefaultHeaderKey, correlationID)
// Add to context
ctx := goctxid.NewContext(r.Context(), correlationID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
correlationID := goctxid.MustFromContext(r.Context())
w.Write([]byte("Correlation ID: " + correlationID))
})
handler := correlationIDMiddleware(mux)
http.ListenAndServe(":3000", handler)
}
Echo
See complete example: examples/echo-basic
import goctxid_echo "github.com/hiiamtin/goctxid/adapters/echo"
e.Use(goctxid_echo.New())
Gin
See complete example: examples/gin-basic
import goctxid_gin "github.com/hiiamtin/goctxid/adapters/gin"
r.Use(goctxid_gin.New())
Other frameworks? See adapters/README.md for a guide on creating your own adapter.
π Examples
Check out the examples/ directory for complete, runnable examples:
| Example |
Framework |
Description |
| basic |
Fiber |
Simple usage with default configuration (context-based) |
| re-exported-api |
Fiber |
Using re-exported functions (no need to import goctxid) |
| advanced-features |
Fiber |
Next function, FastGenerator, and custom LocalsKey |
| fiber-native |
Fiber |
Fiber-native approach using c.Locals() (better performance) |
| standard-http |
net/http |
Framework-agnostic usage with standard library |
| custom-generator |
Fiber |
Custom ID generation strategies (sequential, prefixed) |
| logging |
Fiber |
Integration with logging systems and service layers |
Running Examples
# Run any example
cd examples/basic
go run main.go
# Test it
curl http://localhost:3000/
curl -H "X-Correlation-ID: my-custom-id" http://localhost:3000/
π§ API Reference
Middleware
Each framework adapter provides a New() function that creates middleware for that framework:
Fiber (Context-Based): goctxid_fiber.New(config ...fiber.Config)
import goctxid_fiber "github.com/hiiamtin/goctxid/adapters/fiber"
// Default configuration
app.Use(goctxid_fiber.New())
// Custom configuration
app.Use(goctxid_fiber.New(goctxid_fiber.Config{
Config: goctxid.Config{
HeaderKey: "X-Request-ID",
Generator: goctxid.FastGenerator,
},
Next: func(c *fiber.Ctx) bool {
return c.Path() == "/health"
},
}))
Fiber Native (c.Locals()): goctxid_fibernative.New(config ...fibernative.Config)
import goctxid_fibernative "github.com/hiiamtin/goctxid/adapters/fibernative"
// Default configuration
app.Use(goctxid_fibernative.New())
// Custom configuration
app.Use(goctxid_fibernative.New(goctxid_fibernative.Config{
Config: goctxid.Config{
HeaderKey: "X-Request-ID",
},
LocalsKey: "my_correlation_id",
Next: func(c *fiber.Ctx) bool {
return c.Path() == "/health"
},
}))
// Access ID from Locals
correlationID := goctxid_fibernative.MustFromLocals(c)
// Or with custom key
correlationID := goctxid_fibernative.MustFromLocalsWithKey(c, "my_correlation_id")
Echo: goctxid_echo.New(config ...echo.Config)
import goctxid_echo "github.com/hiiamtin/goctxid/adapters/echo"
// Default configuration
e.Use(goctxid_echo.New())
// Custom configuration
e.Use(goctxid_echo.New(goctxid_echo.Config{
Config: goctxid.Config{
HeaderKey: "X-Request-ID",
},
Next: func(c echo.Context) bool {
return c.Path() == "/health"
},
}))
Gin: goctxid_gin.New(config ...gin.Config)
import goctxid_gin "github.com/hiiamtin/goctxid/adapters/gin"
// Default configuration
r.Use(goctxid_gin.New())
// Custom configuration
r.Use(goctxid_gin.New(goctxid_gin.Config{
Config: goctxid.Config{
HeaderKey: "X-Request-ID",
},
Next: func(c *gin.Context) bool {
return c.Request.URL.Path == "/health"
},
}))
Configuration
Base goctxid.Config
All adapters embed this base configuration:
type Config struct {
// HeaderKey is the HTTP header key used to store the correlation ID
// Default: "X-Correlation-ID"
HeaderKey string
// Generator is the function used to generate a new correlation ID
// Must be thread-safe as it will be called concurrently by multiple requests
// Default: UUID v4 (goctxid.DefaultGenerator)
// Alternative: goctxid.FastGenerator (faster but exposes request count)
Generator func() string
}
β οΈ Important: Custom generators MUST be thread-safe. See Thread-Safety Requirements for details and examples.
Adapter-Specific Configs
Each adapter extends the base config with framework-specific options:
Fiber (Context-Based):
type Config struct {
goctxid.Config
// Next defines a function to skip middleware
Next func(c *fiber.Ctx) bool
}
Fiber Native (c.Locals()):
type Config struct {
goctxid.Config
// LocalsKey is the key used to store the correlation ID in c.Locals()
// Default: "goctxid"
LocalsKey string
// Next defines a function to skip middleware
Next func(c *fiber.Ctx) bool
}
Echo:
type Config struct {
goctxid.Config
// Next defines a function to skip middleware
Next func(c echo.Context) bool
}
Gin:
type Config struct {
goctxid.Config
// Next defines a function to skip middleware
Next func(c *gin.Context) bool
}
Custom Configuration Example:
import (
"github.com/hiiamtin/goctxid"
goctxid_fiber "github.com/hiiamtin/goctxid/adapters/fiber"
)
// Custom configuration
app.Use(goctxid_fiber.New(goctxid_fiber.Config{
Config: goctxid.Config{
HeaderKey: "X-Request-ID", // Use different header
Generator: func() string { // Custom ID generator
return "REQ-" + uuid.NewString()
},
},
Next: func(c *fiber.Ctx) bool {
// Skip middleware for health checks
return c.Path() == "/health"
},
}))
Retrieving Correlation IDs
Convenience Functions (Recommended)
Each context-based adapter provides a GetCorrelationID convenience function:
Fiber:
import goctxid_fiber "github.com/hiiamtin/goctxid/adapters/fiber"
app.Get("/", func(c *fiber.Ctx) error {
correlationID := goctxid_fiber.GetCorrelationID(c)
return c.SendString(correlationID)
})
Echo:
import goctxid_echo "github.com/hiiamtin/goctxid/adapters/echo"
e.GET("/", func(c echo.Context) error {
correlationID := goctxid_echo.GetCorrelationID(c)
return c.String(http.StatusOK, correlationID)
})
Gin:
import goctxid_gin "github.com/hiiamtin/goctxid/adapters/gin"
r.GET("/", func(c *gin.Context) {
correlationID := goctxid_gin.GetCorrelationID(c)
c.JSON(http.StatusOK, gin.H{"id": correlationID})
})
Fibernative (uses c.Locals()):
import goctxid_fibernative "github.com/hiiamtin/goctxid/adapters/fibernative"
app.Get("/", func(c *fiber.Ctx) error {
correlationID := goctxid_fibernative.MustFromLocals(c)
return c.SendString(correlationID)
})
Context Operations
FromContext(ctx context.Context) (string, bool)
Retrieves the correlation ID from the context.
Returns:
string: The correlation ID
bool: true if the ID exists, false otherwise
Example:
correlationID, exists := goctxid.FromContext(c.UserContext())
if !exists {
// Handle missing ID
}
MustFromContext(ctx context.Context) string
Retrieves the correlation ID from the context, returning an empty string if not found.
Returns:
string: The correlation ID or empty string
Example:
correlationID := goctxid.MustFromContext(c.UserContext())
log.Printf("[%s] Processing request", correlationID)
NewContext(ctx context.Context, id string) context.Context
Creates a new context with the correlation ID.
β οΈ Note: This function is primarily intended for middleware adapters and custom middleware implementations. Most users should use the provided framework adapters instead of calling this directly.
When to use:
- β
Creating custom middleware for unsupported frameworks
- β
Implementing custom middleware patterns with
net/http
- β
Testing scenarios where you need to manually inject a correlation ID
- β Regular application code (use
FromContext or MustFromContext instead)
Parameters:
ctx: The parent context
id: The correlation ID to store
Returns:
context.Context: New context with the correlation ID
Example (custom middleware):
func customMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := goctxid.DefaultGenerator()
ctx := goctxid.NewContext(r.Context(), id)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
π¨ Common Patterns
Pattern 1: Logging with Correlation ID
func logWithCorrelation(ctx context.Context, message string) {
correlationID := goctxid.MustFromContext(ctx)
log.Printf("[%s] %s", correlationID, message)
}
app.Get("/user/:id", func(c *fiber.Ctx) error {
ctx := c.UserContext()
logWithCorrelation(ctx, "Fetching user")
// ... your logic
})
Pattern 2: Propagating to Downstream Services
func callExternalAPI(ctx context.Context, url string) error {
correlationID := goctxid.MustFromContext(ctx)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("X-Correlation-ID", correlationID)
// Make request...
}
Pattern 3: Service Layer Integration
type UserService struct {
logger *Logger
}
func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
correlationID := goctxid.MustFromContext(ctx)
s.logger.Info(correlationID, "Fetching user", userID)
// ... your logic
}
The middleware has minimal overhead:
- Time overhead: ~1.3 microseconds per request (~25-30% increase)
- Memory overhead: ~250-300 bytes per request
- Throughput: 200,000+ requests/second
See BENCHMARKS.md for detailed performance analysis.
π§ͺ Testing
# Run all tests
go test ./... -v
# Run tests with coverage
go test ./... -cover
# Run benchmarks
go test ./... -bench=. -benchmem
# Or use Makefile
make test
make test-coverage
make bench
Test Coverage: 100% (core package), 81-92% (adapters)
π οΈ Development
Code Generation
This project uses code generation to eliminate duplication in adapter re-exports:
# Generate re-export code for all adapters
make generate-reexports
# Or manually for a specific adapter
go run tools/generate_reexports.go fiber > adapters/fiber/reexports_generated.go
Why code generation?
- β
Single source of truth for re-export code
- β
Ensures consistency across all adapters
- β
Easy to maintain and update
- β
Zero runtime overhead
See tools/README.md for more details.
Available Make Targets
make help # Show all available targets
make generate-reexports # Generate re-export code
make test # Run all tests
make test-coverage # Run tests with coverage
make bench # Run benchmarks
make fmt # Format code
make vet # Run go vet
make check # Run fmt, vet, and tests
π Documentation
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
π License
MIT License - see LICENSE file for details
π Acknowledgments
Built with support for:
- Fiber - Express-inspired web framework
- Echo - High performance, minimalist Go web framework
- Gin - HTTP web framework written in Go
- google/uuid - UUID generation