Documentation
¶
Overview ¶
Package hookwriter provides a logrus hook for writing log entries to custom io.Writer instances with configurable field filtering and formatting options.
Overview ¶
The hookwriter package implements a logrus.Hook that intercepts log entries and writes them to any io.Writer with fine-grained control over which fields are included, how they're formatted, and whether special modes like access logging are enabled. This is particularly useful for:
- Writing logs to multiple destinations (files, network, buffers)
- Filtering sensitive or verbose fields from output
- Creating specialized log formats for different outputs
- Implementing access log patterns separate from application logs
Design Philosophy ¶
1. Flexible Output: Write to any io.Writer (files, buffers, network sockets, custom writers) 2. Non-invasive Filtering: Filter fields without modifying the original entry 3. Format Agnostic: Support any logrus.Formatter or use default serialization 4. Simple Integration: Single-function creation with clear configuration options 5. Stateless Operation: No background goroutines or complex lifecycle management
Key Features ¶
- Custom io.Writer support for any output destination
- Selective field filtering (stack traces, timestamps, caller info)
- Access log mode for message-only output
- Multiple formatter support (JSON, Text, custom)
- Level-based filtering (handle only specific log levels)
- Optional color output via mattn/go-colorable
- Zero-allocation for disabled hooks (returns nil)
Architecture ¶
The package consists of a simple architecture with minimal components:
┌──────────────────────────────────────────────┐
│ logrus.Logger │
│ │
│ ┌────────────────────────────────────┐ │
│ │ logger.Info("message") │ │
│ └────────────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ logrus.Entry │ │
│ └──────────┬───────┘ │
│ │ │
└────────────────────┼─────────────────────────┘
│
▼
┌────────────────────────────┐
│ HookWriter.Fire() │
│ │
│ 1. Duplicate Entry │
│ 2. Filter Fields │
│ - Stack (opt) │
│ - Time (opt) │
│ - Caller/File (opt) │
│ 3. Format Entry │
│ - Formatter │
│ - Access Log Mode │
│ 4. Write to io.Writer │
└────────────┬───────────────┘
│
▼
┌──────────────┐
│ io.Writer │
│ (file, net) │
└──────────────┘
Data Flow ¶
1. Entry Creation: Application code creates log entry via logger.Info/Warn/Error/etc. 2. Hook Invocation: logrus calls Fire() on all registered hooks for matching log levels 3. Entry Duplication: Hook duplicates entry to avoid modifying original 4. Field Filtering: Removes configured fields (stack, time, caller, file, line) 5. Formatting: Applies formatter or access log mode to serialize entry 6. Write: Outputs formatted bytes to configured io.Writer
Basic Usage ¶
Create a hook and register it with a logrus logger:
import (
"os"
"github.com/sirupsen/logrus"
"github.com/nabbar/golib/logger/config"
"github.com/nabbar/golib/logger/hookwriter"
)
func main() {
// Create file writer
file, _ := os.Create("app.log")
defer file.Close()
// Configure hook options
opt := &config.OptionsStd{
DisableStandard: false,
DisableColor: true,
DisableStack: true,
DisableTimestamp: false,
EnableTrace: false,
}
// Create hook with JSON formatter
hook, err := hookwriter.New(file, opt, nil, &logrus.JSONFormatter{})
if err != nil {
log.Fatal(err)
}
// Register hook with logger
logger := logrus.New()
logger.AddHook(hook)
// Log entries will be written to file
logger.Info("Application started")
}
Configuration Options ¶
The OptionsStd struct controls hook behavior:
DisableStandard: If true, returns nil hook (completely disabled)
opt := &config.OptionsStd{DisableStandard: true}
hook, _ := hookwriter.New(writer, opt, nil, nil) // Returns (nil, nil)
DisableColor: If true, wraps writer with colorable.NewNonColorable() to disable color output
opt := &config.OptionsStd{DisableColor: true}
// Disables color escape sequences in output
DisableStack: Filters out stack trace fields from output
opt := &config.OptionsStd{DisableStack: true}
logger.WithField("stack", trace).Error("error") // "stack" field removed from output
DisableTimestamp: Filters out timestamp fields from output
opt := &config.OptionsStd{DisableTimestamp: true}
// "time" field removed from all entries
EnableTrace: Controls caller/file/line field inclusion
opt := &config.OptionsStd{EnableTrace: false}
// Removes "caller", "file", "line" fields from output
EnableAccessLog: Enables message-only mode (ignores fields and formatters)
opt := &config.OptionsStd{EnableAccessLog: true}
logger.WithField("status", 200).Info("GET /api/users")
// Output: "GET /api/users\n" (fields ignored)
Common Use Cases ¶
Multiple Output Destinations:
fileHook, _ := hookwriter.New(logFile, fileOpt, nil, &logrus.JSONFormatter{})
netHook, _ := hookwriter.New(networkConn, netOpt, nil, &logrus.TextFormatter{})
logger.AddHook(fileHook)
logger.AddHook(netHook)
// Logs written to both file and network
Level-Specific Hooks:
errorHook, _ := hookwriter.New(errorFile, opt, []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}, nil)
// Only errors written to error file
Access Log Pattern:
accessOpt := &config.OptionsStd{
DisableStandard: false,
EnableAccessLog: true,
}
accessHook, _ := hookwriter.New(accessLog, accessOpt, nil, nil)
logger.AddHook(accessHook)
logger.Info("GET /api/users - 200 OK") // Clean access log format
Filtered Debug Output:
debugOpt := &config.OptionsStd{
DisableStack: true,
DisableTimestamp: true,
EnableTrace: false,
}
debugHook, _ := hookwriter.New(os.Stdout, debugOpt, []logrus.Level{logrus.DebugLevel}, nil)
// Minimal debug output without clutter
Performance Considerations ¶
Memory Efficiency:
- Entry duplication uses entry.Dup() which shares data structures where possible
- Field filtering modifies the duplicated entry's Data map without allocating new maps
- Disabled hooks (DisableStandard=true) return nil with zero allocation
Write Performance:
- Write performance depends entirely on the underlying io.Writer
- Buffered writers (bufio.Writer) recommended for high-frequency logging
- Network writers should have reasonable timeouts to avoid blocking
- File writers benefit from OS-level buffering
Formatter Overhead:
- JSON formatters are faster but produce larger output
- Text formatters are slower but more human-readable
- Access log mode bypasses formatting entirely (fastest)
Scalability:
- Hooks are called synchronously by logrus for each entry
- Multiple hooks add cumulative overhead (each hook's Fire() is called)
- For high-throughput scenarios, use aggregation packages: github.com/nabbar/golib/ioutils/aggregator for async writes
Thread Safety ¶
The hook implementation is thread-safe when used correctly:
- Safe: Multiple goroutines logging to the same logger with this hook
- Safe: Multiple hooks registered on the same logger
- Unsafe: Concurrent calls to Fire() with the same entry instance (logrus prevents this)
- Unsafe: Modifying hook configuration after creation (immutable design)
The underlying io.Writer must be thread-safe for concurrent writes. Most standard writers (os.File, bufio.Writer, bytes.Buffer) are not inherently thread-safe for concurrent writes. Use synchronization or the aggregator package for concurrent scenarios.
Error Handling ¶
The hook can return errors in the following situations:
Construction Errors:
hook, err := hookwriter.New(nil, opt, nil, nil) // err: "hook writer is nil"
Runtime Errors:
// Formatter error during Fire() err := hook.Fire(entry) // Returns formatter.Format() error // Writer error during Fire() err := hook.Fire(entry) // Returns writer.Write() error
Silent Failures:
- Empty log data: Fire() returns nil without writing (normal behavior)
- Empty access log message: Fire() returns nil without writing (normal behavior)
- Disabled hook: New() returns (nil, nil) - not an error
Comparison with Standard Output ¶
Standard logrus output (logger.SetOutput):
- Single output destination
- No field filtering
- Applied to all log levels
- Direct write (no hook overhead)
HookWriter advantages:
- Multiple simultaneous outputs
- Per-hook field filtering
- Per-hook level filtering
- Per-hook formatting
- Doesn't replace SetOutput (additive)
Use standard output for simple cases, hooks for advanced routing.
Integration with golib Packages ¶
Logger Package:
import "github.com/nabbar/golib/logger" // Main logger package that uses this hook internally
Logger Config:
import "github.com/nabbar/golib/logger/config" // Provides OptionsStd configuration structure
Logger Types:
import "github.com/nabbar/golib/logger/types" // Defines Hook interface and field constants
IOUtils Aggregator:
import "github.com/nabbar/golib/ioutils/aggregator" // For async high-performance log aggregation
Limitations ¶
Synchronous Writes: Hook writes are synchronous with log calls. Slow writers block logging. Mitigation: Use aggregator package for async writes or buffered writers.
No Write Retries: Failed writes return errors but don't retry or queue. Mitigation: Use reliable writers or add retry logic in custom writers.
No Buffer Management: Hook doesn't buffer or flush data. Mitigation: Use bufio.Writer and call Flush() explicitly when needed.
No Compression: No built-in log compression or rotation. Mitigation: Use external log rotation tools (logrotate) or writer wrappers.
Writer Lifecycle: Hook doesn't manage writer Close(). Mitigation: Caller must close writers when done. Not an issue - proper design.
Best Practices ¶
DO:
- Use bufio.Writer for high-frequency logging to amortize I/O costs
- Set reasonable timeouts on network writers to prevent blocking
- Close writers explicitly when shutting down
- Use level filtering to send different levels to different destinations
- Enable access log mode for HTTP access logs or similar patterns
- Check for nil when DisableStandard is conditionally true
DON'T:
- Use unbuffered network writers in performance-critical paths
- Ignore errors from New() (check for nil writer error)
- Share non-thread-safe writers across multiple hooks without synchronization
- Modify opt struct after passing to New() (not effective, options are copied)
- Use this for extremely high-throughput logging (>100k/sec) without aggregation
Testing ¶
The package includes comprehensive tests covering:
- Hook creation with various configurations
- Field filtering (stack, time, caller, file, line)
- Access log mode with empty messages
- Formatter integration (JSON, Text)
- Integration with logrus.Logger
- Level filtering behavior
- Multiple hooks on single logger
- Error paths (nil writer, write failures)
Run tests:
go test -v github.com/nabbar/golib/logger/hookwriter
Check coverage:
go test -cover github.com/nabbar/golib/logger/hookwriter
Current coverage: 90.2% (exceeds 80% target)
Examples ¶
See example_test.go for runnable examples demonstrating:
- Basic hook creation and usage
- File writing with JSON formatter
- Access log mode for HTTP logs
- Multiple hooks for different outputs
- Level-specific filtering
- Field filtering configurations
Related Packages ¶
- github.com/sirupsen/logrus - Underlying logging framework
- github.com/mattn/go-colorable - Color support on Windows
- github.com/nabbar/golib/logger - Main logger package
- github.com/nabbar/golib/logger/config - Configuration types
- github.com/nabbar/golib/logger/types - Hook interface and constants
- github.com/nabbar/golib/ioutils/aggregator - Async write aggregation
License ¶
MIT License - See LICENSE file for details.
Copyright (c) 2025 Nicolas JUHEL
Example (AccessLog) ¶
Example_accessLog demonstrates using access log mode for HTTP request logging.
package main
import (
"bytes"
"fmt"
"os"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
var buf bytes.Buffer
// Enable access log mode
opt := &logcfg.OptionsStd{
DisableStandard: false,
EnableAccessLog: true, // Message-only mode
}
hook, err := loghkw.New(&buf, opt, nil, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Setup logger
logger := logrus.New()
logger.SetOutput(os.Stderr) // Avoid double write
logger.AddHook(hook)
// IMPORTANT: In AccessLog mode, behavior is REVERSED!
// The message "GET /api/users - 200 OK - 45ms" IS output.
// The fields (method, path, status_code) are IGNORED.
logger.WithFields(logrus.Fields{
"method": "GET",
"path": "/api/users",
"status_code": 200,
}).Info("GET /api/users - 200 OK - 45ms")
fmt.Print(buf.String())
}
Output: GET /api/users - 200 OK - 45ms
Example (Basic) ¶
Example_basic demonstrates the simplest use case: creating a hook that writes to a buffer.
package main
import (
"bytes"
"fmt"
"os"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
var buf bytes.Buffer
// Configure the hook with minimal settings
opt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true, // Disable color for predictable output
}
// Create the hook writing to buffer
hook, err := loghkw.New(&buf, opt, nil, &logrus.TextFormatter{
DisableTimestamp: true, // Disable timestamp for predictable output
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Create and configure logger (output to Discard to avoid double write)
logger := logrus.New()
logger.SetOutput(os.Stderr) // Use Stderr to separate from example output
logger.AddHook(hook)
// IMPORTANT: The message parameter "ignored" is NOT used by the hook.
// Only the fields (here "msg") are written to the file.
// Exception: In AccessLog mode, only the message is used and fields are ignored.
logger.WithField("msg", "Application started").Info("ignored")
// Print what was written by the hook
fmt.Print(buf.String())
}
Output: level=info fields.msg="Application started"
Example (DisabledHook) ¶
Example_disabledHook demonstrates how to conditionally disable the hook.
package main
import (
"fmt"
"os"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
opt := &logcfg.OptionsStd{
DisableStandard: true, // This disables the hook
}
hook, err := loghkw.New(os.Stdout, opt, nil, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if hook == nil {
fmt.Println("Hook is disabled")
} else {
fmt.Println("Hook is enabled")
}
}
Output: Hook is disabled
Example (FieldFiltering) ¶
Example_fieldFiltering demonstrates filtering specific fields from output.
package main
import (
"bytes"
"fmt"
"os"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
var buf bytes.Buffer
// Configure to filter out stack and timestamp
opt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true,
DisableStack: true, // Remove stack fields
DisableTimestamp: true, // Remove time fields
EnableTrace: false, // Remove caller/file/line fields
}
hook, err := loghkw.New(&buf, opt, nil, &logrus.TextFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
logger := logrus.New()
logger.SetOutput(os.Stderr) // Avoid double write
logger.AddHook(hook)
// Log with fields that will be filtered
logger.WithFields(logrus.Fields{
"msg": "Filtered log",
"stack": "trace info",
"caller": "main.go:123",
"user": "john",
}).Info("ignored")
// Only "user" field remains after filtering
fmt.Print(buf.String())
}
Output: level=info fields.msg="Filtered log" user=john
Example (FileWriter) ¶
Example_fileWriter demonstrates writing logs to a file with JSON formatting.
package main
import (
"bytes"
"fmt"
"os"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
// Create a buffer to simulate a file (for example purposes)
var buf bytes.Buffer
// Configure options
opt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true,
DisableStack: true,
DisableTimestamp: true,
}
// Create hook with JSON formatter
hook, err := loghkw.New(&buf, opt, nil, &logrus.JSONFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Setup logger
logger := logrus.New()
logger.SetOutput(os.Stderr) // Avoid double write
logger.AddHook(hook)
// Log with fields
logger.WithFields(logrus.Fields{
"user_id": 123,
"action": "login",
"msg": "User logged in",
}).Info("ignored")
fmt.Println("Log written to file")
}
Output: Log written to file
Example (LevelFiltering) ¶
Example_levelFiltering demonstrates filtering logs by level.
package main
import (
"bytes"
"fmt"
"os"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
var buf = bytes.NewBuffer(make([]byte, 0))
opt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true,
}
// Only handle error and fatal levels
levels := []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
}
hook, err := loghkw.New(buf, opt, levels, &logrus.TextFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
logger := logrus.New()
logger.SetOutput(os.Stderr) // Avoid writing to buf
logger.AddHook(hook)
// These won't be written by the hook (wrong level)
logger.WithField("type", "info").Info("ignored")
logger.WithField("type", "warn").Warn("ignored")
// This will be written by the hook (error level)
logger.WithField("type", "error").WithField("msg", "This is an error").Error("ignored")
fmt.Printf("Hook captured: %s", buf.String())
}
Output: Hook captured: level=error fields.msg="This is an error" type=error
Example (MultipleHooks) ¶
Example_multipleHooks demonstrates using multiple hooks for different outputs.
package main
import (
"bytes"
"fmt"
"os"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
var infoBuf, errorBuf bytes.Buffer
// Hook for info/debug logs
infoOpt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true,
}
infoHook, _ := loghkw.New(&infoBuf, infoOpt, []logrus.Level{
logrus.InfoLevel,
logrus.DebugLevel,
}, &logrus.TextFormatter{DisableTimestamp: true})
// Hook for error logs
errorOpt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true,
}
errorHook, _ := loghkw.New(&errorBuf, errorOpt, []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
}, &logrus.JSONFormatter{DisableTimestamp: true})
// Setup logger with both hooks
logger := logrus.New()
logger.SetOutput(os.Stderr) // Avoid writing to buffers
logger.AddHook(infoHook)
logger.AddHook(errorHook)
logger.WithField("msg", "This goes to info buffer").WithField("target", "info").Info("ignored")
logger.WithField("msg", "This goes to error buffer").WithField("target", "error").Error("ignored")
fmt.Printf("Info has: %s", infoBuf.String())
fmt.Printf("Error has: %s", errorBuf.String())
}
Output: Info has: level=info fields.msg="This goes to info buffer" target=info Error has: {"fields.msg":"This goes to error buffer","level":"error","msg":"","target":"error"}
Example (NilWriter) ¶
Example_nilWriter demonstrates error handling for nil writer.
package main
import (
"fmt"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
opt := &logcfg.OptionsStd{
DisableStandard: false,
}
hook, err := loghkw.New(nil, opt, nil, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
if hook == nil {
fmt.Println("Hook was not created")
}
}
Output: Error: hook writer is nil Hook was not created
Example (TraceEnabled) ¶
Example_traceEnabled demonstrates enabling trace information in logs.
package main
import (
"bytes"
"fmt"
"os"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
loghkw "github.com/nabbar/golib/logger/hookwriter"
)
func main() {
var buf bytes.Buffer
opt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true,
EnableTrace: true, // Include caller/file/line information
}
hook, err := loghkw.New(&buf, opt, nil, &logrus.TextFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
logger := logrus.New()
logger.SetOutput(os.Stderr) // Avoid double write
logger.AddHook(hook)
logger.WithFields(logrus.Fields{
"msg": "Log with trace info",
"caller": "example_test.go:line",
"file": "example_test.go",
"line": 123,
"user": "john",
}).Info("ignored")
// Trace fields are included because EnableTrace is true
fmt.Print(buf.String())
}
Output: level=info caller="example_test.go:line" fields.msg="Log with trace info" file=example_test.go line=123 user=john
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type HookWriter ¶
HookWriter is a logrus hook that writes log entries to an io.Writer with configurable filtering and formatting options.
This interface extends logtps.Hook and provides integration with logrus logger for customized log output handling. It supports field filtering (stack, timestamp, trace), custom formatters, and access log mode.
func New ¶
func New(w io.Writer, opt *logcfg.OptionsStd, lvls []logrus.Level, f logrus.Formatter) (HookWriter, error)
New creates a new HookWriter instance for writing logrus entries to a custom writer.
Parameters:
- w: The target io.Writer where log entries will be written. Must not be nil.
- opt: Configuration options controlling behavior. If nil or DisableStandard is true, returns (nil, nil) to indicate the hook should be disabled.
- lvls: Log levels to handle. If empty or nil, defaults to logrus.AllLevels.
- f: Optional logrus.Formatter for entry formatting. If nil, uses entry.Bytes().
Configuration options (via opt):
- DisableStandard: If true, returns nil hook (disabled).
- DisableColor: If true, wraps writer with colorable.NewNonColorable() to disable color output.
- DisableStack: If true, filters out stack trace fields from log data.
- DisableTimestamp: If true, filters out time fields from log data.
- EnableTrace: If false, filters out caller/file/line fields from log data.
- EnableAccessLog: If true, uses message-only mode (ignores fields and formatter).
Returns:
- HookWriter: The configured hook instance, or nil if disabled.
- error: "hook writer is nil" if w is nil, otherwise nil.
Example:
opt := &logcfg.OptionsStd{
DisableStandard: false,
DisableColor: true,
}
hook, err := hookwriter.New(os.Stdout, opt, nil, &logrus.JSONFormatter{})
if err != nil {
log.Fatal(err)
}
logger.AddHook(hook)