Documentation
ΒΆ
Overview ΒΆ
Package logger provides a system-agnostic, reusable, modular logging library built on top of Go's standard library log/slog.
go-logger does not wrap or replace slog β it extends slog through composable slog.Handler middleware. The public API produces and consumes standard *slog.Logger instances, ensuring full ecosystem compatibility.
Core Features ΒΆ
- Async logging with configurable backpressure (drop, block, sync-fallback)
- Sensitive data redaction (type-level, key-based, pattern-based, nested groups)
- Probabilistic and per-level log sampling
- Per-module/component log level filtering with runtime hot-reload
- Multi-output fan-out to multiple handlers simultaneously
- Builder pattern for composing handler middleware chains
- Graceful shutdown lifecycle (Flush/Close) with cascade support
Design Principles ΒΆ
- Standard library first: zero third-party dependencies in the core module
- System-agnostic: no domain-specific types (blockchain, HTTP, IoT, etc.)
- All domain metadata is represented as generic slog.Attr key-value pairs
- Composable middleware: all features are slog.Handler implementations
- Immutable handlers: slog.Handler.WithAttrs and slog.Handler.WithGroup always return new instances; receivers are never mutated
Quick Start ΒΆ
log := logger.NewJSON(os.Stdout, logger.WithLevel(slog.LevelInfo))
log.Info("server started", "port", 8080)
Builder Pattern ΒΆ
log := logger.NewBuilder(slog.NewJSONHandler(os.Stdout, nil)).
WithRedaction(handler.WithRedactKeys("password", "token")).
WithAsync(handler.WithBufferSize(4096)).
BuildLogger()
defer logger.Close(log.Handler())
Index ΒΆ
- func Close(h slog.Handler) error
- func CloseContext(ctx context.Context, h slog.Handler) error
- func Component(name string) slog.Attr
- func Default() *slog.Logger
- func Err(err error) slog.Attr
- func Exit(h slog.Handler, code int)
- func Fatal(log *slog.Logger, msg string, args ...any)
- func Flush(h slog.Handler) error
- func FlushContext(ctx context.Context, h slog.Handler) error
- func FromContext(ctx context.Context) *slog.Logger
- func New(h slog.Handler) *slog.Logger
- func NewContext(ctx context.Context, log *slog.Logger) context.Context
- func NewJSON(w io.Writer, opts ...Option) *slog.Logger
- func NewText(w io.Writer, opts ...Option) *slog.Logger
- func SetDefault(l *slog.Logger)
- func SpanID(id string) slog.Attr
- func TraceID(id string) slog.Attr
- type Builder
- func (b *Builder) Build() slog.Handler
- func (b *Builder) BuildLogger() *slog.Logger
- func (b *Builder) WithAsync(opts ...handler.AsyncOption) *Builder
- func (b *Builder) WithMiddleware(mw func(slog.Handler) slog.Handler) *Builder
- func (b *Builder) WithModuleFilter(cfg *handler.ModuleConfig) *Builder
- func (b *Builder) WithRedaction(opts ...handler.RedactOption) *Builder
- func (b *Builder) WithSampling(opts ...handler.SampleOption) *Builder
- type Closer
- type ContextCloser
- type ContextFlusher
- type Flusher
- type Option
- type Redacted
- type SensitiveBytes
- type Unwrapper
Examples ΒΆ
Constants ΒΆ
This section is empty.
Variables ΒΆ
This section is empty.
Functions ΒΆ
func Close ΒΆ
Close attempts to close the given handler by checking if it implements Closer. If it does not, Close recursively unwraps the handler chain (via the Unwrap() method) to find a Closer in the middleware stack.
Usage in application shutdown:
h := log.Handler()
if err := logger.Close(h); err != nil {
fmt.Fprintf(os.Stderr, "logger close: %v\n", err)
}
func CloseContext ΒΆ
CloseContext is like Close but accepts a context for deadline and cancellation support. It prefers ContextCloser if implemented, falling back to Closer.
CloseContext propagates through the entire middleware chain: after calling a handler's lifecycle method, it continues unwrapping to close inner handlers as well. This ensures that all handlers in the chain (including MultiHandler children) receive the close signal.
Errors from multiple handlers are aggregated using errors.Join.
func Component ΒΆ
Component creates a slog.Attr with the key "component" for module or subsystem identification.
This attribute is used by [ModuleHandler] to apply per-component log level filtering. The component name should identify the subsystem, not the application domain.
log := slog.New(h).With(logger.Component("networking"))
log.Info("listening", "port", 9000)
Example ΒΆ
package main
import (
"log/slog"
"os"
logger "github.com/amhrmsn/go-logger"
"github.com/amhrmsn/go-logger/handler"
)
// removeTime strips the time attribute so example output is deterministic.
func removeTime(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
}
func main() {
config := handler.NewModuleConfig(slog.LevelInfo)
config.SetLevel("database", slog.LevelDebug)
base := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
ReplaceAttr: removeTime,
})
log := slog.New(handler.NewModuleHandler(base, config))
dbLog := log.With(logger.Component("database"))
dbLog.Debug("query executed") // logged: database is set to Debug
apiLog := log.With(logger.Component("api"))
apiLog.Debug("parsing request") // filtered: api uses the Info default
apiLog.Info("request handled")
}
Output: {"level":"DEBUG","msg":"query executed","component":"database"} {"level":"INFO","msg":"request handled","component":"api"}
func Default ΒΆ
Default returns the current default *slog.Logger.
This is a convenience wrapper around slog.Default.
func Err ΒΆ
Err creates a slog.Attr for an error value with the key "error".
This is a convenience helper that ensures consistent key naming for error attributes across an application.
if err != nil {
log.Error("operation failed", logger.Err(err))
}
func Exit ΒΆ
Exit flushes and closes the handler chain, then terminates the process with the given status code.
Calling os.Exit directly after logging loses any records still queued in an handler.AsyncHandler buffer: the process dies before the background worker drains them. Exit closes that gap by running FlushContext and CloseContext over the entire middleware chain first.
The flush and close are best-effort: they share a 5-second timeout and their errors are discarded, because the process is terminating either way.
log.Error("unrecoverable", logger.Err(err))
logger.Exit(log.Handler(), 1)
func Fatal ΒΆ
Fatal logs the message at slog.LevelError with the given arguments, then calls Exit with status code 1.
This is the async-safe replacement for the common pattern of logging an error followed by os.Exit: buffered records β including the fatal message itself β are flushed before the process terminates.
logger.Fatal(log, "cannot bind listener", "addr", addr, logger.Err(err))
func Flush ΒΆ
Flush attempts to flush the given handler by checking if it implements Flusher. If it does not, Flush recursively unwraps the handler chain (via the Unwrap() method) to find a Flusher in the middleware stack.
After Flush returns, all records submitted before the Flush call are guaranteed to have been written to the underlying output.
func FlushContext ΒΆ
FlushContext is like Flush but accepts a context for deadline and cancellation support. It prefers ContextFlusher if implemented, falling back to Flusher.
FlushContext propagates through the entire middleware chain: after calling a handler's lifecycle method, it continues unwrapping to flush inner handlers as well.
Errors from multiple handlers are aggregated using errors.Join.
func FromContext ΒΆ
FromContext returns the *slog.Logger stored in ctx by NewContext.
If ctx is nil or carries no logger, slog.Default is returned, so the result is always safe to use.
func New ΒΆ
New creates a *slog.Logger with the given handler.
This is a thin convenience wrapper around slog.New. The handler is typically built using the Builder or composed manually from handler middleware.
func NewContext ΒΆ
NewContext returns a copy of ctx that carries log.
This enables the common request-scoped logger pattern: attach a logger enriched with request attributes once, then retrieve it anywhere below with FromContext.
log := logger.FromContext(ctx).With("request_id", id)
ctx = logger.NewContext(ctx, log)
func NewJSON ΒΆ
NewJSON creates a *slog.Logger with a slog.JSONHandler writing to w.
Options are applied to the underlying slog.HandlerOptions. If no options are provided, the handler uses default settings (Info level, no source).
Example ΒΆ
package main
import (
"log/slog"
"os"
logger "github.com/amhrmsn/go-logger"
)
// removeTime strips the time attribute so example output is deterministic.
func removeTime(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
}
func main() {
log := logger.NewJSON(os.Stdout, logger.WithReplaceAttr(removeTime))
log.Info("server started", "port", 8080)
}
Output: {"level":"INFO","msg":"server started","port":8080}
func NewText ΒΆ
NewText creates a *slog.Logger with a slog.TextHandler writing to w.
Options are applied to the underlying slog.HandlerOptions. If no options are provided, the handler uses default settings (Info level, no source).
func SetDefault ΒΆ
SetDefault sets the default *slog.Logger used by the top-level functions in log/slog.
This is a convenience wrapper around slog.SetDefault.
func SpanID ΒΆ
SpanID creates a slog.Attr with the key "span_id" for distributed trace span correlation.
slog.InfoContext(ctx, "handling request",
logger.TraceID(tid),
logger.SpanID(sid),
)
func TraceID ΒΆ
TraceID creates a slog.Attr with the key "trace_id" for distributed trace correlation.
In applications using OpenTelemetry or similar tracing systems, include the trace ID in log records to enable log-trace correlation in your observability backend.
slog.InfoContext(ctx, "processing",
logger.TraceID(extractTraceID(ctx)),
)
Types ΒΆ
type Builder ΒΆ
type Builder struct {
// contains filtered or unexported fields
}
Builder provides a fluent API for composing slog.Handler middleware chains.
The builder records configurations for each middleware layer and composes them in a fixed order when [Build] or [BuildLogger] is called.
Composition order (innermost β outermost):
base β AsyncHandler β RedactionHandler β SamplingHandler β ModuleHandler β custom middleware
This means ModuleHandler is checked first at call time (outermost), followed by SamplingHandler, RedactionHandler, and finally AsyncHandler wraps the base handler directly.
Example:
log := logger.NewBuilder(slog.NewJSONHandler(os.Stdout, nil)).
WithRedaction(handler.WithRedactKeys("password", "token")).
WithAsync(handler.WithBufferSize(4096)).
BuildLogger()
defer logger.Close(log.Handler())
func NewBuilder ΒΆ
NewBuilder creates a Builder with the given base handler.
The base handler is the innermost handler in the chain β typically a slog.JSONHandler or slog.TextHandler.
Example ΒΆ
package main
import (
"log/slog"
"os"
logger "github.com/amhrmsn/go-logger"
"github.com/amhrmsn/go-logger/handler"
)
// removeTime strips the time attribute so example output is deterministic.
func removeTime(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
}
func main() {
base := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: removeTime})
log := logger.NewBuilder(base).
WithRedaction(handler.WithRedactKeys("password")).
BuildLogger()
log.Info("user login", "user", "alice", "password", "s3cret")
}
Output: {"level":"INFO","msg":"user login","user":"alice","password":"[REDACTED]"}
func (*Builder) Build ΒΆ
Build composes the handler chain and returns the outermost handler.
Composition order (innermost β outermost):
base β AsyncHandler β RedactionHandler β SamplingHandler β ModuleHandler β custom middleware
Each middleware is only included if it was configured via the corresponding With*() method.
Call Build (or Builder.BuildLogger) at most once per Builder. Every call composes a fresh chain around the same base handler; with Builder.WithAsync configured, each call starts its own background worker goroutine that must be closed independently.
func (*Builder) BuildLogger ΒΆ
BuildLogger composes the handler chain and returns a *slog.Logger.
This is equivalent to calling slog.New(b.Build()).
func (*Builder) WithAsync ΒΆ
func (b *Builder) WithAsync(opts ...handler.AsyncOption) *Builder
WithAsync enables the handler.AsyncHandler middleware with the given options.
The async handler wraps the base handler directly (innermost middleware), buffering records in a channel for background processing.
func (*Builder) WithMiddleware ΒΆ
WithMiddleware adds a custom middleware function to the chain.
Custom middleware is applied after all built-in middleware (outermost layer). Multiple calls to WithMiddleware append to the chain in registration order: the first registered middleware wraps the built-in chain, the second wraps the first, and so on. Therefore, the LAST registered middleware becomes the outermost handler and is executed FIRST at log time.
Example with two custom middlewares:
builder.WithMiddleware(mwA).WithMiddleware(mwB).Build() // Composition: base β ... β ModuleHandler β mwA β mwB // Execution: mwB β mwA β ModuleHandler β ... β base
func (*Builder) WithModuleFilter ΒΆ
func (b *Builder) WithModuleFilter(cfg *handler.ModuleConfig) *Builder
WithModuleFilter enables the handler.ModuleHandler middleware with the given configuration.
The module handler applies per-component log level filtering. It is the outermost built-in middleware, so it is checked first.
func (*Builder) WithRedaction ΒΆ
func (b *Builder) WithRedaction(opts ...handler.RedactOption) *Builder
WithRedaction enables the handler.RedactionHandler middleware with the given options.
The redaction handler inspects and redacts sensitive attributes before they reach the base handler (or async handler).
func (*Builder) WithSampling ΒΆ
func (b *Builder) WithSampling(opts ...handler.SampleOption) *Builder
WithSampling enables the handler.SamplingHandler middleware with the given options.
The sampling handler applies probabilistic filtering to reduce log volume.
type Closer ΒΆ
type Closer interface {
Close() error
}
Closer represents a slog.Handler that holds resources requiring cleanup.
Handlers that open files, network connections, or background goroutines should implement this interface to support graceful shutdown.
type ContextCloser ΒΆ
ContextCloser is like Closer but accepts a context for deadline and cancellation support. Handlers should prefer implementing this interface to allow callers to bound shutdown time.
type ContextFlusher ΒΆ
ContextFlusher is like Flusher but accepts a context for deadline and cancellation support.
type Flusher ΒΆ
type Flusher interface {
Flush() error
}
Flusher represents a slog.Handler that buffers data internally.
Handlers that buffer log records (such as [AsyncHandler]) should implement this interface to allow callers to ensure all buffered records are written before inspecting output or shutting down.
type Option ΒΆ
type Option func(*options)
Option configures the base slog.Handler created by NewJSON and NewText.
func WithLevel ΒΆ
WithLevel sets the minimum log level for the handler.
Any slog.Leveler can be used, including a *slog.LevelVar for dynamic runtime level changes.
func WithLevelVar ΒΆ
WithLevelVar sets a dynamic log level that can be changed at runtime without restarting the application.
This is equivalent to calling WithLevel with the *slog.LevelVar, but communicates the intent more clearly.
The slog.LevelVar is safe for concurrent use by multiple goroutines.
func WithReplaceAttr ΒΆ
WithReplaceAttr sets a function that is called for each non-group slog.Attr before it is logged. The function can modify, replace, or remove attributes.
This is useful for:
- Redacting sensitive fields by key name
- Customizing timestamp or source location formatting
- Removing unwanted built-in attributes
See slog.HandlerOptions.ReplaceAttr for details on the function signature.
func WithSource ΒΆ
WithSource enables or disables source code location (file, line, function) in log output.
Enabling source location has a performance cost due to runtime.Caller stack introspection. Consider enabling it only in development or for specific debugging scenarios.
type Redacted ΒΆ
type Redacted string
Redacted is a string type that always logs as "[REDACTED]".
Implementing slog.LogValuer, any value of this type will automatically have its contents hidden when logged, regardless of the handler or redaction middleware in use.
This provides compile-time safety: sensitive types redact themselves, and callers cannot accidentally bypass the redaction.
type Config struct {
APIKey logger.Redacted
Host string
}
// When logged: {"APIKey":"[REDACTED]","Host":"example.com"}
Example ΒΆ
package main
import (
"log/slog"
"os"
logger "github.com/amhrmsn/go-logger"
)
// removeTime strips the time attribute so example output is deterministic.
func removeTime(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
}
func main() {
log := logger.NewJSON(os.Stdout, logger.WithReplaceAttr(removeTime))
log.Info("config loaded",
"api_key", logger.Redacted("sk-1234-secret"),
"host", "example.com",
)
}
Output: {"level":"INFO","msg":"config loaded","api_key":"[REDACTED]","host":"example.com"}
type SensitiveBytes ΒΆ
type SensitiveBytes []byte
SensitiveBytes is a []byte type that logs its length but not its content.
This is useful for binary secrets (encryption keys, raw tokens, etc.) where knowing the size is helpful for debugging but the content must never appear in logs.
slog.Info("key loaded", "key", logger.SensitiveBytes(privateKeyBytes))
// Output: {"msg":"key loaded","key":"[REDACTED:32 bytes]"}
Example ΒΆ
package main
import (
"log/slog"
"os"
logger "github.com/amhrmsn/go-logger"
)
// removeTime strips the time attribute so example output is deterministic.
func removeTime(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
}
func main() {
log := logger.NewJSON(os.Stdout, logger.WithReplaceAttr(removeTime))
log.Info("key loaded", "key", logger.SensitiveBytes([]byte{0xDE, 0xAD, 0xBE, 0xEF}))
}
Output: {"level":"INFO","msg":"key loaded","key":"[REDACTED:4 bytes]"}
func (SensitiveBytes) LogValue ΒΆ
func (s SensitiveBytes) LogValue() slog.Value
LogValue implements slog.LogValuer. It returns a string indicating the byte length without exposing the content.
type Unwrapper ΒΆ
Unwrapper is implemented by middleware handlers that wrap an inner handler.
Implementing Unwrapper lets Close, CloseContext, Flush, and FlushContext traverse past a handler to reach lifecycle-aware handlers (such as handler.AsyncHandler) deeper in the chain. All middleware in this library implements it; third-party middleware that wraps another handler should too, otherwise the chain traversal stops at that handler and inner resources are never flushed or closed.
Source Files
ΒΆ
Directories
ΒΆ
| Path | Synopsis |
|---|---|
|
examples
|
|
|
async
command
Package main demonstrates the AsyncHandler with graceful shutdown.
|
Package main demonstrates the AsyncHandler with graceful shutdown. |
|
basic
command
Package main demonstrates the basic usage of go-logger.
|
Package main demonstrates the basic usage of go-logger. |
|
full-chain
command
Package main demonstrates the full go-logger middleware chain using the Builder.
|
Package main demonstrates the full go-logger middleware chain using the Builder. |
|
module-filter
command
Package main demonstrates the ModuleHandler for per-component log level filtering.
|
Package main demonstrates the ModuleHandler for per-component log level filtering. |
|
multi-output
command
Package main demonstrates the MultiHandler for fan-out to multiple outputs.
|
Package main demonstrates the MultiHandler for fan-out to multiple outputs. |
|
redaction
command
Package main demonstrates the RedactionHandler for protecting sensitive data.
|
Package main demonstrates the RedactionHandler for protecting sensitive data. |
|
Package handler provides composable slog.Handler middleware for the go-logger library.
|
Package handler provides composable slog.Handler middleware for the go-logger library. |
|
internal
|
|
|
record
Package record provides internal utilities for safe manipulation of slog.Record values.
|
Package record provides internal utilities for safe manipulation of slog.Record values. |