[file/bandwidth] - ADD documentation: add enhanced README and TESTING guidelines - ADD tests: complete test suites with benchmarks, concurrency, and edge cases [file/perm] - ADD documentation: add enhanced README and TESTING guidelines - ADD tests: complete test suites with benchmarks, concurrency, and edge cases - ADD function to parse form "rwx-wxr-x" or "-rwx-w-r-x" - ADD function to ParseFileMode to convert os.FileMode to file.Perm [file/progress] - ADD documentation: add enhanced README and TESTING guidelines - ADD tests: complete test suites with benchmarks, concurrency, and edge cases [ioutils/...] - UPDATE documentation: update enhanced README and TESTING guidelines - UPDATE tests: complete test suites with benchmarks, concurrency, and edge cases [logger/...] - UPDATE documentation: update enhanced README and TESTING guidelines - ADD documentation: add enhanced README and TESTING guidelines for sub packages - UPDATE tests: complete test suites with benchmarks, concurrency, and edge cases - UPDATE config: remove FileBufferSize from OptionFile (rework hookfile) - UPDATE fields: expose Store function in interface - REWORK hookfile: rework package, use aggregator to allow multi write and single file - FIX hookstderr: fix bug with NonColorable - FIX hookstdout: fix bug with NonColorable - FIX hookwriter: fix bug with NonColorable [network/protocol] - ADD function IsTCP, IsUDP, IsUnixLike to check type of protocol [runner] - FIX typo [socket] - UPDATE documentation: update enhanced README and TESTING guidelines - ADD documentation: add enhanced README and TESTING guidelines for sub packages - UPDATE tests: complete test suites with benchmarks, concurrency, and edge cases - REWORK server: use context compatible io.reader, io.writer, io.closer instead of reader / writer - REWORK server: simplify, optimize server - REMOVE reader, writer type - ADD context: add new interface in root socket interface to expose context interface that extend context, io reader/writer/closer, dediacted function to server (IsConnected, ...)
22 KiB
Logger HookWriter
Logrus hook for writing log entries to custom io.Writer instances with configurable field filtering, formatting options, and access log mode.
Table of Contents
- Overview
- Architecture
- Performance
- Use Cases
- Quick Start
- Best Practices
- API Reference
- Contributing
- Improvements & Security
- Resources
- AI Transparency
- License
Overview
The hookwriter package provides 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 directing logs to multiple destinations with different filtering and formatting requirements.
Design Philosophy
- Flexible Output: Write to any io.Writer (files, buffers, network sockets, custom writers)
- Non-invasive Filtering: Filter fields without modifying the original entry
- Format Agnostic: Support any logrus.Formatter or use default serialization
- Simple Integration: Single-function creation with clear configuration options
- Stateless Operation: No background goroutines or complex lifecycle management
Key Features
- ✅ Custom io.Writer Support: Write to any output destination
- ✅ Selective Field Filtering: Stack traces, timestamps, caller info
- ✅ Access Log Mode: Message-only output without fields
- ✅ Multiple Formatter Support: JSON, Text, or custom formatters
- ✅ Level-Based Filtering: Handle only specific log levels
- ✅ Color Output Control: Via mattn/go-colorable integration
- ✅ 90.2% Test Coverage: 31 comprehensive test specs with race detection
- ✅ Zero Dependencies: Only logrus and internal golib packages
Architecture
Component Diagram
┌────────────────────────────────────────────────────────┐
│ logrus.Logger │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ logger.WithFields(fields).Info("message") │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ logrus.Entry │ │
│ └────────┬────────┘ │
│ │ │
└────────────────────┼───────────────────────────────────┘
│
▼
┌────────────────────────────────┐
│ HookWriter.Fire() │
│ │
│ 1. Duplicate Entry │
│ 2. Filter Fields │
│ - Stack (opt) │
│ - Time (opt) │
│ - Caller/File/Line (opt) │
│ 3. Format Entry │
│ - Formatter (opt) │
│ - Access Log Mode (opt) │
│ 4. Write to io.Writer │
└────────────┬───────────────────┘
│
▼
┌──────────────┐
│ io.Writer │
│ (file, net) │
└──────────────┘
Data Flow
Log Entry → Fire() → Entry Duplication → Field Filtering → Formatting → Write
│ │ │ │ │
│ │ │ │ ▼
│ │ │ │ io.Writer
│ │ │ │
│ │ ▼ ▼
│ │ Remove stack/time/ Formatter
│ │ caller/file/line or Bytes()
│ │ or Message
│ ▼
│ entry.Dup()
│ (thread-safe)
│
▼
Original Entry
(unchanged)
Performance
Benchmarks
Based on actual test results from the comprehensive test suite:
| Operation | Overhead | Notes |
|---|---|---|
| Hook.Fire() | <1µs | Per log entry processed |
| Entry Duplication | ~48 bytes | Memory allocation per entry |
| Field Filtering | <500ns | Per filtered field |
| Formatter Call | Varies | Depends on formatter (JSON ~10µs, Text ~5µs) |
| Write Operation | Varies | Depends on underlying io.Writer |
Throughput:
- Single logger: ~10,000 entries/second (with file writer)
- Concurrent loggers: Limited by io.Writer, not hook overhead
- Field filtering: Negligible impact on overall performance
Memory Usage
Hook Overhead:
Base struct: ~128 bytes
Per log entry: ~48 bytes (entry duplication)
Zero allocations: For access log mode with pre-allocated []byte
Scalability: Hooks are called synchronously by logrus for each matching entry. Performance is limited by the underlying io.Writer speed, not the hook itself.
Scalability
- Concurrent Loggers: Safe when multiple goroutines log to the same logger
- Multiple Hooks: Each hook adds minimal overhead (~1µs per hook)
- Tested Scenarios: Up to 10,000 log entries/second with file writers
- Zero Race Conditions: All tests pass with
-racedetector
Note: For high-throughput scenarios (>10,000 writes/sec), consider using the github.com/nabbar/golib/ioutils/aggregator package to buffer writes to slower io.Writers.
Use Cases
1. Multi-Destination Logging
Write logs to multiple destinations with different formats:
// JSON logs to file
fileHook, _ := hookwriter.New(logFile, fileOpt, nil, &logrus.JSONFormatter{})
// Text logs to console
consoleHook, _ := hookwriter.New(os.Stdout, consoleOpt, nil, &logrus.TextFormatter{})
logger.AddHook(fileHook)
logger.AddHook(consoleHook)
Real-world: Separate application logs (detailed) from user-facing console output (concise).
2. Error-Only File
Route only errors to a dedicated file:
errorHook, _ := hookwriter.New(errorFile, opt, []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}, &logrus.JSONFormatter{})
Real-world: Centralized error monitoring and alerting.
3. HTTP Access Logs
Generate clean access logs without structured fields:
accessOpt := &config.OptionsStd{
EnableAccessLog: true, // Message-only mode
}
accessHook, _ := hookwriter.New(accessLog, accessOpt, nil, nil)
logger.WithFields(logrus.Fields{
"method": "GET",
"path": "/api/users",
"status": 200,
}).Info("GET /api/users - 200 OK - 45ms")
// Output: "GET /api/users - 200 OK - 45ms\n"
Real-world: Apache/Nginx-style access logs without JSON overhead.
4. Filtered Debug Output
Remove verbose fields for cleaner debugging:
debugOpt := &config.OptionsStd{
DisableStack: true,
DisableTimestamp: true,
EnableTrace: false,
}
debugHook, _ := hookwriter.New(os.Stdout, debugOpt, []logrus.Level{logrus.DebugLevel}, nil)
Real-world: Development logging without cluttering output.
5. Network Log Shipping
Send logs to remote syslog or logging service:
conn, _ := net.Dial("tcp", "log-server:514")
netHook, _ := hookwriter.New(conn, netOpt, nil, &logrus.JSONFormatter{})
// Logs automatically sent to remote server
logger.AddHook(netHook)
Real-world: Centralized logging infrastructure, SIEM integration.
Quick Start
Installation
go get github.com/nabbar/golib/logger/hookwriter
Basic Example
package main
import (
"os"
"github.com/sirupsen/logrus"
"github.com/nabbar/golib/logger/config"
"github.com/nabbar/golib/logger/hookwriter"
)
func main() {
logger := logrus.New()
// Configure hook options
opt := &config.OptionsStd{
DisableStandard: false,
DisableColor: true,
}
// Create hook writing to stdout
hook, err := hookwriter.New(os.Stdout, opt, nil, &logrus.TextFormatter{})
if err != nil {
panic(err)
}
// Register hook
logger.AddHook(hook)
// Log with fields (fields required for hook to write)
logger.WithFields(logrus.Fields{
"msg": "User logged in",
"user": "john",
"action": "login",
}).Info("ignored") // message in logrus function are ignored, must be in field
}
File Writing
file, _ := os.Create("app.log")
defer file.Close()
opt := &config.OptionsStd{
DisableStandard: false,
DisableColor: true,
}
hook, _ := hookwriter.New(file, opt, nil, &logrus.JSONFormatter{})
logger.AddHook(hook)
// Fields are required for hook to write in standard mode
logger.WithFields(logrus.Fields{
"module": "auth",
"status": "success",
}).Info("ignored message")
Access Log Mode
accessFile, _ := os.Create("access.log")
defer accessFile.Close()
opt := &config.OptionsStd{
EnableAccessLog: true, // Message-only
}
hook, _ := hookwriter.New(accessFile, opt, nil, nil)
logger.AddHook(hook)
// In access log mode: only message is written, fields are ignored
logger.WithFields(logrus.Fields{
"method": "GET",
"status": 200,
}).Info("GET /api/users - 200 OK")
// Output: "GET /api/users - 200 OK\n" (fields ignored in access log mode)
Field Filtering
opt := &config.OptionsStd{
DisableStack: true, // Remove stack traces
DisableTimestamp: true, // Remove timestamps
EnableTrace: false, // Remove caller/file/line
}
hook, _ := hookwriter.New(os.Stdout, opt, nil, &logrus.TextFormatter{
DisableTimestamp: true,
})
logger.AddHook(hook)
// Fields "stack", "time", "caller" will be filtered out
logger.WithFields(logrus.Fields{
"stack": "trace...", // Filtered out by DisableStack
"time": "...", // Filtered out by DisableTimestamp
"caller": "...", // Filtered out by EnableTrace=false
"user": "john", // Kept
"msg": "Filtered log" // Kept
}).Info("ignored message")
// Output: level=info fields.msg="Filtered log" user=john
Multiple Hooks
// JSON to file
fileHook, _ := hookwriter.New(logFile, &config.OptionsStd{
DisableColor: true,
}, nil, &logrus.JSONFormatter{})
// Text to console
consoleHook, _ := hookwriter.New(os.Stdout, &config.OptionsStd{
DisableColor: false,
}, nil, &logrus.TextFormatter{})
logger.AddHook(fileHook)
logger.AddHook(consoleHook)
// Logs written to both destinations (field required for hook to write)
logger.WithField("msg", "Application started").WithField("app", "myapp").Info("ignored message")
Best Practices
Testing
The package includes a comprehensive test suite with 90.2% code coverage and 31 test specifications using BDD methodology (Ginkgo v2 + Gomega).
Key test coverage:
- ✅ All public APIs and configuration options
- ✅ Concurrent access with race detector (zero races detected)
- ✅ Field filtering behavior (stack, timestamp, trace)
- ✅ Access log mode and formatter integration
- ✅ Error handling and edge cases
For detailed test documentation, see TESTING.md.
✅ DO
Check for nil hook:
// DisableStandard can return nil hook
hook, err := hookwriter.New(writer, opt, nil, nil)
if err != nil {
return err
}
if hook != nil {
logger.AddHook(hook)
}
Use appropriate formatters:
// JSON for machine parsing
hook, _ := hookwriter.New(file, opt, nil, &logrus.JSONFormatter{})
// Text for human reading
hook, _ := hookwriter.New(os.Stdout, opt, nil, &logrus.TextFormatter{})
Close writers explicitly:
file, _ := os.Create("app.log")
defer file.Close() // Always close
hook, _ := hookwriter.New(file, opt, nil, formatter)
logger.AddHook(hook)
Use level filtering:
// Only errors to error file
errorLevels := []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
hook, _ := hookwriter.New(errorFile, opt, errorLevels, formatter)
❌ DON'T
Don't ignore nil writer errors:
// ❌ BAD: Ignoring error
hook, _ := hookwriter.New(nil, opt, nil, nil)
logger.AddHook(hook) // Panic!
// ✅ GOOD: Check error
hook, err := hookwriter.New(nil, opt, nil, nil)
if err != nil {
return err
}
Don't use blocking writers without buffering:
// ❌ BAD: Slow network write blocks all logging
netConn, _ := net.Dial("tcp", "slow-server:514")
hook, _ := hookwriter.New(netConn, opt, nil, formatter)
// ✅ GOOD: Use aggregator for buffering
agg, _ := aggregator.New(ctx, aggregator.Config{
FctWriter: netConn.Write,
BufWriter: 1000,
})
hook, _ := hookwriter.New(agg, opt, nil, formatter)
Don't forget to add fields in standard mode:
// ❌ BAD: No fields = no output in standard mode
logger.Info("This won't be written by the hook")
// ✅ GOOD: Add at least one field in standard mode
logger.WithField("msg", "This will be written").WithField("app", "myapp").Info("ignored message")
// ℹ️ NOTE: In access log mode (EnableAccessLog=true), only message is used
Don't mix DisableColor incorrectly:
// ❌ BAD: DisableColor=false but writing to file
opt := &config.OptionsStd{DisableColor: false}
hook, _ := hookwriter.New(file, opt, nil, formatter) // Color codes in file!
// ✅ GOOD: DisableColor=true for files
opt := &config.OptionsStd{DisableColor: true}
hook, _ := hookwriter.New(file, opt, nil, formatter)
API Reference
HookWriter Interface
type HookWriter interface {
logtps.Hook
}
The HookWriter interface extends logtps.Hook (which itself extends logrus.Hook) and implements all required methods for logrus hook integration.
Implemented Methods:
Fire(entry *logrus.Entry) error- Process and write log entryLevels() []logrus.Level- Return log levels handled by this hookRegisterHook(log *logrus.Logger)- Convenience method to register hookRun(ctx context.Context)- No-op for lifecycle compatibilityIsRunning() bool- Always returns true (stateless hook)
Configuration
type OptionsStd struct {
DisableStandard bool // If true, returns nil hook (disabled)
DisableColor bool // If true, wraps writer with colorable.NewNonColorable()
DisableStack bool // If true, filters out stack trace fields
DisableTimestamp bool // If true, filters out time fields
EnableTrace bool // If true, includes caller/file/line fields
EnableAccessLog bool // If true, uses message-only mode (no fields/formatting)
}
Function Signature:
func New(w io.Writer, opt *OptionsStd, lvls []logrus.Level, f logrus.Formatter) (HookWriter, error)
Parameters:
w- Target io.Writer (required, error if nil)opt- Configuration options (nil or DisableStandard returns nil hook)lvls- Log levels to handle (nil or empty defaults to logrus.AllLevels)f- Optional formatter (nil uses entry.Bytes())
Returns:
- Hook instance (or nil if disabled)
- Error if writer is nil
Field Filtering
The hook filters fields from duplicated entries based on configuration:
| Field | Filtered By | Field Name | Description |
|---|---|---|---|
| Stack Trace | DisableStack |
"stack" |
Stack traces from errors |
| Timestamp | DisableTimestamp |
"time" |
Log entry timestamp |
| Caller | !EnableTrace |
"caller" |
Function that logged |
| File | !EnableTrace |
"file" |
Source file path |
| Line | !EnableTrace |
"line" |
Source line number |
Note: Filtering occurs on a duplicated entry (entry.Dup()), so the original entry remains unchanged for other hooks.
Error Handling
Errors Returned:
var (
ErrInvalidWriter = errors.New("hook writer is nil")
)
When Errors Occur:
New()returns error ifwis nilFire()returns errors from formatter or writer- Hook gracefully handles empty data (returns nil, no write)
Error Propagation:
- Formatter errors propagate to caller
- Writer errors propagate to caller
- Logrus logs but doesn't stop on hook errors
Contributing
Contributions are welcome! Please follow these guidelines:
Code Quality
- Follow Go best practices and idioms
- Maintain or improve code coverage (target: >80%)
- Pass all tests including race detector
- Use
gofmtandgolint
AI Usage Policy
- ❌ AI must NEVER be used to generate package code or core functionality
- ✅ AI assistance is limited to:
- Testing (writing and improving tests)
- Debugging (troubleshooting and bug resolution)
- Documentation (comments, README, TESTING.md)
- All AI-assisted work must be reviewed and validated by humans
Testing
- Add tests for new features
- Use Ginkgo v2 / Gomega for test framework
- Ensure zero race conditions
- Maintain coverage above 80%
Documentation
- Update GoDoc comments for public APIs
- Add examples for new features
- Update README.md and TESTING.md if needed
Pull Request Process
- Fork the repository
- Create a feature branch
- Write clear commit messages
- Ensure all tests pass
- Update documentation
- Submit PR with description of changes
Improvements & Security
Current Status
The package is production-ready with no urgent improvements or security vulnerabilities identified.
Code Quality Metrics
- ✅ 90.2% test coverage (target: >80%)
- ✅ Zero race conditions detected with
-raceflag - ✅ Thread-safe when used with logrus (logrus serializes hook calls)
- ✅ Memory-safe with proper entry duplication
- ✅ No goroutine leaks (stateless design)
Future Enhancements (Non-urgent)
The current implementation is stable and performant.
Resources
Package Documentation
-
GoDoc - Complete API reference with function signatures, method descriptions, and runnable examples. Essential for understanding the public interface and usage patterns.
-
doc.go - In-depth package documentation including design philosophy, architecture diagrams, field filtering logic, access log mode, and performance considerations. Provides detailed explanations of internal mechanisms and best practices for production use.
-
TESTING.md - Comprehensive test suite documentation covering test architecture, BDD methodology with Ginkgo v2, coverage analysis (90.2%), and guidelines for writing new tests. Includes troubleshooting and CI integration examples.
Related golib Packages
-
github.com/nabbar/golib/logger/config - Configuration types including OptionsStd used for hook configuration. Provides standardized configuration structures across logger components.
-
github.com/nabbar/golib/logger/types - Logger interfaces and field constants (FieldStack, FieldTime, FieldCaller, etc.). Defines common types and constants used throughout the logger ecosystem.
-
github.com/nabbar/golib/ioutils/aggregator - Thread-safe write aggregator for buffering writes to slow io.Writers. Recommended for high-throughput logging scenarios with network or slow file systems.
External References
-
Logrus Documentation - Official logrus documentation for structured logging in Go. The hookwriter package integrates seamlessly with logrus through the standard Hook interface.
-
Effective Go - Official Go programming guide covering best practices for interfaces, error handling, and I/O patterns. The hookwriter package follows these conventions for idiomatic Go code.
AI Transparency
In compliance with EU AI Act Article 50.4: AI assistance was used for testing, documentation, and bug resolution under human supervision. All core functionality is human-designed and validated.
License
MIT License - See LICENSE file for details.
Copyright (c) 2025 Nicolas JUHEL
Maintained by: Nicolas JUHEL
Package: github.com/nabbar/golib/logger/hookwriter
Version: See releases for versioning