Documentation
¶
Overview ¶
Package hookfile provides file-based logging hooks for logrus. This file handles log file aggregation and rotation functionality. It manages multiple writers to the same log file efficiently.
Package hookfile provides a logrus hook for writing log entries to files with automatic rotation detection, efficient multi-writer aggregation, and configurable formatting.
Overview ¶
The hookfile package implements a production-ready logrus.Hook that writes log entries to files with sophisticated features not found in standard file logging:
- Automatic log rotation detection using inode comparison
- Efficient write aggregation when multiple loggers share the same file
- Thread-safe concurrent writes with reference counting
- Configurable field filtering (stack, time, caller info)
- Access log mode for HTTP request logging
- Automatic directory creation and permission management
This package is particularly useful for:
- Production applications requiring robust log rotation
- Multi-tenant systems where multiple loggers write to shared files
- Systems using external log rotation tools (logrotate, etc.)
- Applications needing separation of access logs and application logs
Design Philosophy ¶
1. Rotation-Aware: Automatically detect and handle external log rotation 2. Resource Efficient: Share file handles and aggregators across multiple hooks 3. Production-Ready: Handle edge cases like file deletion, permission errors, disk full 4. Zero-Copy Writes: Use aggregator pattern to minimize memory allocations 5. Fail-Safe Operation: Continue logging even when rotation fails
Key Features ¶
- **Automatic Rotation Detection**: Detects when log files are moved/renamed (inode tracking)
- **File Handle Sharing**: Multiple hooks to same file share single aggregator and file handle
- **Buffered Aggregation**: Uses ioutils/aggregator for efficient async writes
- **Reference Counting**: Automatically closes files when last hook is removed
- **Permission Management**: Configurable file and directory permissions
- **Field Filtering**: Remove stack traces, timestamps, caller info as needed
- **Access Log Mode**: Message-only output for HTTP access logs
- **Error Recovery**: Automatic file reopening on errors
Architecture ¶
The package uses a multi-layered architecture with reference-counted file aggregators:
┌─────────────────────────────────────────────┐
│ Multiple logrus.Logger │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Logger 1 │ │Logger 2 │ │Logger 3 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
└───────┼────────────┼────────────┼───────────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────┐
│ HookFile Instances │
│ (3 hooks, same filepath) │
└────────────┬───────────────────┘
│
▼
┌───────────────────┐
│ File Aggregator │
│ (RefCount: 3) │
│ │
│ • Shared File │
│ • Sync Timer │
│ • Rotation Check │
└────────┬──────────┘
│
▼
┌──────────────┐
│ Aggregator │
│ (buffered) │
└──────┬───────┘
│
▼
┌──────────┐
│ app.log │
└──────────┘
Component Interaction ¶
1. Hook Creation: New(opts, formatter) → creates or reuses file aggregator 2. Write Aggregation: Multiple hooks → single aggregator → single file 3. Rotation Detection: Sync timer (1s) → inode comparison → file reopen if rotated 4. Reference Counting: Close hook → decrement refcount → close file at zero 5. Error Handling: Write error → log to stderr → continue operation
Log Rotation Detection ¶
The package automatically detects external log rotation (e.g., by logrotate) using inode tracking:
Time T0: app.log (inode: 12345)
↓
Hook writes → file descriptor points to inode 12345
Time T1: logrotate renames app.log to app.log.1
Creates new app.log (inode: 67890)
↓
Hook still writes → FD points to OLD inode 12345 (app.log.1)
Time T2: Sync timer runs (every 1 second)
Compare: FD inode (12345) ≠ Disk inode (67890)
↓
Rotation detected!
Close old FD → Open new file → Resume logging to NEW inode
Time T3: Hook writes → file descriptor points to NEW inode 67890
The rotation detection uses os.SameFile() to compare inodes, which works reliably across Unix systems and Windows (using file IDs). The sync timer runs every second to balance between detection latency and system overhead.
Logrus Hook Behavior ¶
**⚠️ CRITICAL**: Understanding how logrus hooks process log data:
Standard Mode (Default):
- Fields (logrus.Fields) ARE written to output
- Message parameter in Info/Error/etc. is IGNORED by formatter
- To log a message: use logger.WithField("msg", "text").Info("")
Access Log Mode (EnableAccessLog=true):
- Message parameter IS written to output
- Fields (logrus.Fields) are IGNORED
- To log a message: use logger.Info("GET /api/users - 200 OK")
Example of Standard Mode:
// ❌ WRONG: Message will NOT appear in logs
logger.Info("User logged in") // Output: (empty)
// ✅ CORRECT: Use fields
logger.WithField("msg", "User logged in").Info("")
// Output: level=info fields.msg="User logged in"
Example of Access Log Mode:
// ✅ CORRECT in AccessLog mode
logger.Info("GET /api/users - 200 OK - 45ms")
// Output: GET /api/users - 200 OK - 45ms
// ❌ WRONG in AccessLog mode: Fields are ignored
logger.WithField("status", 200).Info("") // Output: (empty)
Basic Usage ¶
Create a file hook with automatic rotation detection:
import (
"github.com/sirupsen/logrus"
"github.com/nabbar/golib/logger/config"
"github.com/nabbar/golib/logger/hookfile"
)
func main() {
// Configure file hook options
opts := config.OptionsFile{
Filepath: "/var/log/myapp/app.log",
FileMode: 0644,
PathMode: 0755,
CreatePath: true, // Create directories if needed
LogLevel: []string{"info", "warning", "error"},
DisableStack: true,
DisableTimestamp: false,
EnableTrace: false,
}
// Create hook with JSON formatter
hook, err := hookfile.New(opts, &logrus.JSONFormatter{})
if err != nil {
panic(err)
}
defer hook.Close()
// Register hook with logger
logger := logrus.New()
logger.AddHook(hook)
// IMPORTANT: Use fields, not message parameter
logger.WithFields(logrus.Fields{
"msg": "Application started",
"user": "system",
"action": "startup",
}).Info("")
// Writes to /var/log/myapp/app.log with rotation detection
}
Configuration Options ¶
The OptionsFile struct controls hook behavior:
Filepath (required): Path to the log file
opts := config.OptionsFile{
Filepath: "/var/log/app.log",
}
FileMode: File permissions (default: 0644)
opts.FileMode = 0600 // Owner read/write only
PathMode: Directory permissions when creating paths (default: 0755)
opts.PathMode = 0700 // Owner full access only
CreatePath: Create parent directories if they don't exist
opts.CreatePath = true // Required for rotation detection
Create: Create file if it doesn't exist
opts.Create = true // Required for rotation detection and file recreation
LogLevel: Log levels this hook should handle
opts.LogLevel = []string{"error", "warning"} // Only errors and warnings
DisableStack: Filter out stack trace fields
opts.DisableStack = true // Removes "stack" field from output
DisableTimestamp: Filter out timestamp fields
opts.DisableTimestamp = true // Removes "time" field from output
EnableTrace: Include caller/file/line information
opts.EnableTrace = true // Adds "caller", "file", "line" fields
EnableAccessLog: Use message-only mode (for HTTP access logs)
opts.EnableAccessLog = true // Message param is used, fields ignored
Common Use Cases ¶
## Production Application Logging
opts := config.OptionsFile{
Filepath: "/var/log/myapp/app.log",
FileMode: 0644,
PathMode: 0755,
CreatePath: true,
LogLevel: []string{"info", "warning", "error"},
}
hook, _ := hookfile.New(opts, &logrus.JSONFormatter{})
logger.AddHook(hook)
// Configure logrotate:
// /etc/logrotate.d/myapp:
// /var/log/myapp/app.log {
// daily
// rotate 7
// compress
// delaycompress
// missingok
// notifempty
// }
//
// Hook automatically detects rotation and reopens new file
## Separate Access Logs
// Application logs (standard mode)
appOpts := config.OptionsFile{
Filepath: "/var/log/myapp/app.log",
CreatePath: true,
}
appHook, _ := hookfile.New(appOpts, &logrus.JSONFormatter{})
appLogger := logrus.New()
appLogger.AddHook(appHook)
// Access logs (access log mode)
accessOpts := config.OptionsFile{
Filepath: "/var/log/myapp/access.log",
CreatePath: true,
EnableAccessLog: true, // Message-only mode
}
accessHook, _ := hookfile.New(accessOpts, nil)
accessLogger := logrus.New()
accessLogger.AddHook(accessHook)
// Application logging (uses fields)
appLogger.WithField("msg", "Request processed").Info("")
// Access logging (uses message)
accessLogger.Info("GET /api/users - 200 OK - 45ms")
## Multiple Loggers, Single File
// Multiple hooks writing to same file (efficient aggregation)
opts := config.OptionsFile{
Filepath: "/var/log/shared.log",
CreatePath: true,
}
hook1, _ := hookfile.New(opts, &logrus.TextFormatter{})
hook2, _ := hookfile.New(opts, &logrus.TextFormatter{})
hook3, _ := hookfile.New(opts, &logrus.TextFormatter{})
logger1 := logrus.New()
logger1.AddHook(hook1)
logger2 := logrus.New()
logger2.AddHook(hook2)
logger3 := logrus.New()
logger3.AddHook(hook3)
// All three loggers share the same file aggregator
// Only one file descriptor is open
// Reference count is 3
hook1.Close() // RefCount: 3 → 2
hook2.Close() // RefCount: 2 → 1
hook3.Close() // RefCount: 1 → 0 (file closed)
## Level-Specific Files
// Error log file
errorOpts := config.OptionsFile{
Filepath: "/var/log/myapp/error.log",
CreatePath: true,
LogLevel: []string{"error", "fatal", "panic"},
}
errorHook, _ := hookfile.New(errorOpts, &logrus.JSONFormatter{})
// Debug log file
debugOpts := config.OptionsFile{
Filepath: "/var/log/myapp/debug.log",
CreatePath: true,
LogLevel: []string{"debug"},
DisableStack: true,
DisableTimestamp: true,
}
debugHook, _ := hookfile.New(debugOpts, &logrus.TextFormatter{})
logger := logrus.New()
logger.AddHook(errorHook)
logger.AddHook(debugHook)
logger.WithField("msg", "Debug info").Debug("") // → debug.log
logger.WithField("msg", "Error occurred").Error("") // → error.log
Performance Considerations ¶
Write Performance:
- Buffered aggregation reduces syscall overhead (250 byte buffer)
- Multiple hooks to same file share single aggregator (no duplication)
- Async writes available via aggregator AsyncFct callback
- File sync runs every 1 second (balances durability and performance)
Memory Efficiency:
- Reference counting prevents duplicate file handles
- Entry duplication shares data structures where possible
- Field filtering modifies duplicated entry without new allocations
- Aggregator reuses buffers to minimize GC pressure
Rotation Detection Overhead:
- Sync timer runs every 1 second (configurable in aggregator)
- Stat syscalls: 2 per second (current FD + disk file)
- Negligible CPU impact (<0.1% on modern systems)
- Rotation reopening: ~1-5ms downtime during file switch
Scalability:
- Thread-safe for concurrent writes from multiple goroutines
- File aggregator uses channels for serialized writes
- Supports hundreds of concurrent loggers writing to same file
- Reference counting prevents resource leaks
Benchmarks (typical workload):
- Single write: ~100-150µs (includes formatting + buffer)
- Throughput: ~5000-10000 entries/sec (depends on formatter)
- Memory: ~320KB per file aggregator (includes buffers)
- Rotation detection: <1µs per sync cycle
Thread Safety ¶
The package is designed for thread-safe operation:
Safe Operations:
- Multiple goroutines logging via same logger
- Multiple loggers with hooks to the same file
- Concurrent hook creation for the same filepath
- Concurrent Close() calls on different hooks
Unsafe Operations:
- Modifying OptionsFile after hook creation (immutable design)
- Manually deleting log files while hook is active (rotation detection handles this)
Synchronization Mechanisms:
- Atomic reference counting for file aggregators
- Channel-based writes in aggregator package
- Mutex-protected file operations in aggregator
- Atomic bool for hook running state
Error Handling ¶
Construction Errors:
hook, err := hookfile.New(config.OptionsFile{}, formatter)
// err: "missing file path"
hook, err := hookfile.New(config.OptionsFile{
Filepath: "/root/noperm.log",
CreatePath: false,
}, formatter)
// err: permission denied (if /root/noperm.log doesn't exist)
Runtime Errors:
// Formatter error during Fire() err := hook.Fire(entry) // Returns formatter.Format() error // Disk full error during Fire() err := hook.Fire(entry) // Returns write error, logged to stderr
Rotation Errors:
- File deleted externally: Automatically recreated on next sync
- Permission changed: Error logged to stderr, continues with old FD
- Disk full during rotation: Error logged to stderr, retries next sync
Silent Behaviors:
- Empty log data: Fire() returns nil without writing
- Empty access log message: Fire() returns nil without writing
- Entry level not in LogLevel filter: Fire() returns nil (normal filtering)
Integration with Other golib Packages ¶
This package integrates with several other golib packages:
github.com/nabbar/golib/ioutils/aggregator:
- Provides buffered, thread-safe write aggregation
- Handles sync timer for rotation detection
- Manages async callbacks if configured
github.com/nabbar/golib/logger/config:
- Defines OptionsFile configuration structure
- Provides FileMode and PathMode types
- Used by all logger packages for consistency
github.com/nabbar/golib/logger/types:
- Defines Hook interface (extended by HookFile)
- Provides field name constants (FieldStack, FieldTime, etc.)
- Ensures compatibility across logger packages
github.com/nabbar/golib/logger/level:
- Parses log level strings ("debug", "info", etc.)
- Converts to logrus.Level
- Validates level names
github.com/nabbar/golib/ioutils:
- PathCheckCreate for directory creation
- Permission handling utilities
Comparison with Standard File Logging ¶
Standard logrus file logging (logger.SetOutput):
- Single file per logger
- No rotation detection
- No buffering (direct writes)
- No reference counting
- No field filtering
HookFile advantages:
- Automatic rotation detection
- Multiple loggers sharing single file
- Buffered aggregation
- Reference-counted file handles
- Per-hook field filtering
- Per-hook level filtering
- Per-hook formatting
HookFile disadvantages:
- More complex architecture
- Slight overhead from rotation detection
- Requires understanding of hook behavior (fields vs message)
Limitations ¶
Known limitations of the package:
1. Rotation Detection Latency: Up to 1 second delay (sync timer interval)
- Mitigation: Acceptable for most applications
- Alternative: Decrease SyncTimer in aggregator config (not recommended <100ms)
2. Windows Limitations: File rotation detection less reliable on Windows
- Reason: Windows file locking can prevent rotation
- Mitigation: Use CreatePath=true, avoid manual file operations
3. Network File Systems: Rotation detection may not work on NFS/CIFS
- Reason: Inode semantics vary across network filesystems
- Mitigation: Test thoroughly, use local filesystems for logs
4. Reference Counting Leak: If Close() is never called, file handles leak
- Mitigation: Always defer hook.Close() or use finalizers
- Mitigation: Package init() sets finalizer on aggregator map
5. No Built-in Compression: Package doesn't compress rotated files
- Mitigation: Use external tools (logrotate with compress option)
6. No Size-Based Rotation: Only detects external rotation
- Mitigation: Use logrotate or similar tools for size-based rotation
Best Practices ¶
DO:
- Always use CreatePath=true for production (enables rotation detection)
- Configure external log rotation (logrotate, etc.)
- Use defer hook.Close() to ensure cleanup
- Use fields for log data, not message parameter (unless AccessLog mode)
- Set appropriate FileMode/PathMode for security
DON'T:
- Don't manually rotate files from within application (use external tools)
- Don't modify log files while hook is active (use rotation instead)
- Don't create hundreds of hooks to the same file (one per logger is enough)
- Don't use EnableAccessLog for structured logging (use standard mode)
- Don't log sensitive data without appropriate file permissions
TESTING:
- Use ResetOpenFiles() in test cleanup (BeforeEach/AfterEach)
- Create temporary directories for test files
- Add delays before cleanup to allow goroutines to stop
- Test with race detector enabled (CGO_ENABLED=1 go test -race)
Testing ¶
The package includes comprehensive tests with BDD methodology (Ginkgo v2 + Gomega):
go test -v # Run all tests go test -race -v # Run with race detector go test -cover # Check code coverage go test -bench=. # Run benchmarks
Test organization:
- hookfile_test.go: Basic functionality tests
- hookfile_concurrency_test.go: Thread safety tests
- hookfile_integration_test.go: Rotation detection, multiple hooks
- hookfile_benchmark_test.go: Performance benchmarks
Coverage target: >80% (current: 84.0%, target exceeded)
Requirements ¶
- Go 1.24 or higher (requires os.OpenRoot introduced in Go 1.24) - atomic.Int64 (Go 1.19+) for thread-safe reference counting
Examples ¶
See example_test.go for runnable examples demonstrating:
- Basic file logging
- Log rotation handling
- Multiple loggers to single file
- Access log mode
- Level-specific filtering
- Field filtering
- Production-ready configuration
Related Packages ¶
- github.com/nabbar/golib/logger/hookstdout: Hook for stdout output
- github.com/nabbar/golib/logger/hookstderr: Hook for stderr output
- github.com/nabbar/golib/logger/hookwriter: Base hook for custom io.Writer
- github.com/nabbar/golib/logger: High-level logger abstraction
License ¶
MIT License - See LICENSE file for details.
Copyright (c) 2025 Nicolas JUHEL ¶
Package hookfile provides a logrus hook implementation for file-based logging with configurable formatting and log levels.
This file contains error definitions used throughout the package.
Package hookfile provides a logrus hook implementation for writing logs to files with various formatting options. It supports log rotation detection, custom formatters, and different log levels. The hook can be configured to enable/disable stack traces, timestamps, and access log formatting.
Important Usage Notes ¶
When using this hook in normal mode (not access log mode), all log data MUST be passed via the logrus.Entry.Data field. The Message parameter is ignored by the formatter. For example:
logger.WithField("msg", "User logged in").WithField("user", "john").Info("")
NOT:
logger.Info("User logged in") // This message will be ignored!
Log Rotation ¶
The hook automatically detects external log rotation (e.g., by logrotate) when CreatePath and Create are enabled. It uses inode comparison to detect when the log file has been moved/renamed and automatically reopens the file at the configured path. The sync timer runs every second to check for rotation.
Thread Safety ¶
The hook is thread-safe and can be used concurrently from multiple goroutines. It uses an aggregator pattern to manage writes to the same file from multiple hooks efficiently.
Related Packages ¶
This package integrates with:
- github.com/sirupsen/logrus - The logging framework
- github.com/nabbar/golib/logger/config - Configuration structures
- github.com/nabbar/golib/ioutils/aggregator - Buffered file writing with rotation support
- github.com/nabbar/golib/logger/types - Common logger types and interfaces
Package hookfile provides file-based logging hooks for logrus. This file implements the io.Writer interface and related methods for the file hook.
Package hookfile provides a logrus hook implementation for file-based logging with configurable formatting and log levels. It's part of the golib logger package.
Package hookfile provides file-based logging hooks for logrus. This file contains getter methods for accessing the hook's configuration options.
Example (AccessLog) ¶
Example_accessLog demonstrates using access log mode for HTTP request logging.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-access-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
accessLog := filepath.Join(tmpDir, "access.log")
// Enable access log mode
opts := logcfg.OptionsFile{
Filepath: accessLog,
FileMode: 0644,
CreatePath: true,
EnableAccessLog: true, // Message-only mode
}
hook, err := logfil.New(opts, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer hook.Close()
logger := logrus.New()
logger.SetOutput(os.Stderr)
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")
time.Sleep(100 * time.Millisecond)
content, _ := os.ReadFile(accessLog)
fmt.Print(string(content))
}
Output: GET /api/users - 200 OK - 45ms
Example (Basic) ¶
Example_basic demonstrates the simplest use case: creating a hook that writes to a file.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
// Create temporary directory for example
tmpDir, _ := os.MkdirTemp("", "hookfile-example-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
logFile := filepath.Join(tmpDir, "app.log")
// Configure the hook with minimal settings
opts := logcfg.OptionsFile{
Filepath: logFile,
FileMode: 0600,
PathMode: 0700,
CreatePath: true,
}
// Create the hook
hook, err := logfil.New(opts, &logrus.TextFormatter{
DisableTimestamp: true, // Disable timestamp for predictable output
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer hook.Close()
// Create and configure logger
logger := logrus.New()
logger.SetOutput(os.Stderr) // Use Stderr to separate from file 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")
// Give aggregator time to flush
time.Sleep(100 * time.Millisecond)
// Read and print what was written by the hook
content, _ := os.ReadFile(logFile)
fmt.Print(string(content))
}
Output: level=info fields.msg="Application started"
Example (ErrorHandling) ¶
Example_errorHandling demonstrates error handling for invalid file paths.
package main
import (
"fmt"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
// Try to create hook with missing file path
opts := logcfg.OptionsFile{
Filepath: "", // Missing!
}
hook, err := logfil.New(opts, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
if hook == nil {
fmt.Println("Hook was not created")
}
}
Output: Error: missing file path Hook was not created
Example (FieldFiltering) ¶
Example_fieldFiltering demonstrates filtering specific fields from output.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-filter-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
logFile := filepath.Join(tmpDir, "app.log")
// Configure to filter out stack and timestamp
opts := logcfg.OptionsFile{
Filepath: logFile,
CreatePath: true,
DisableStack: true, // Remove stack fields
DisableTimestamp: true, // Remove time fields
EnableTrace: false, // Remove caller/file/line fields
}
hook, err := logfil.New(opts, &logrus.TextFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer hook.Close()
logger := logrus.New()
logger.SetOutput(os.Stderr)
logger.AddHook(hook)
// IMPORTANT: message "ignored" is NOT used, only fields
logger.WithFields(logrus.Fields{
"msg": "Filtered log",
"stack": "will be filtered",
"caller": "will be filtered",
"user": "john",
}).Info("ignored")
time.Sleep(100 * time.Millisecond)
content, _ := os.ReadFile(logFile)
fmt.Print(string(content))
}
Output: level=info fields.msg="Filtered log" user=john
Example (LevelFiltering) ¶
Example_levelFiltering demonstrates filtering logs by level to different files.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-levels-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
errorLog := filepath.Join(tmpDir, "error.log")
opts := logcfg.OptionsFile{
Filepath: errorLog,
CreatePath: true,
LogLevel: []string{"error", "fatal"}, // Only errors
}
hook, err := logfil.New(opts, &logrus.TextFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer hook.Close()
logger := logrus.New()
logger.SetOutput(os.Stderr)
logger.AddHook(hook)
// This will be written (error level)
// Note: message "ignored" is NOT used, only the field "msg"
logger.WithField("msg", "Error occurred").Error("ignored")
// This won't be written (wrong level)
logger.WithField("msg", "Info message").Info("ignored")
time.Sleep(100 * time.Millisecond)
content, _ := os.ReadFile(errorLog)
fmt.Printf("Error log: %s", string(content))
}
Output: Error log: level=error fields.msg="Error occurred"
Example (MultipleLoggers) ¶
Example_multipleLoggers demonstrates multiple loggers writing to the same file efficiently.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-multi-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
sharedLog := filepath.Join(tmpDir, "shared.log")
opts := logcfg.OptionsFile{
Filepath: sharedLog,
CreatePath: true,
}
// Create multiple hooks to the same file (they share the file aggregator)
hook1, _ := logfil.New(opts, &logrus.TextFormatter{DisableTimestamp: true})
hook2, _ := logfil.New(opts, &logrus.TextFormatter{DisableTimestamp: true})
defer hook1.Close()
defer hook2.Close()
logger1 := logrus.New()
logger1.SetOutput(os.Stderr)
logger1.AddHook(hook1)
logger2 := logrus.New()
logger2.SetOutput(os.Stderr)
logger2.AddHook(hook2)
// Both loggers write to the same file efficiently
// IMPORTANT: message parameter is NOT used, only fields
logger1.WithField("msg", "From logger 1").Info("ignored")
logger2.WithField("msg", "From logger 2").Info("ignored")
time.Sleep(100 * time.Millisecond)
fmt.Println("Multiple loggers wrote to same file")
}
Output: Multiple loggers wrote to same file
Example (ProductionSetup) ¶
Example_productionSetup demonstrates a production-ready configuration with rotation support.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-prod-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
logFile := filepath.Join(tmpDir, "app.log")
opts := logcfg.OptionsFile{
Filepath: logFile,
FileMode: 0644, // Readable by others
PathMode: 0755, // Standard directory permissions
CreatePath: true, // Create directories if needed (enables rotation detection)
LogLevel: []string{"info", "warning", "error"},
DisableStack: true, // Don't log stack traces
DisableTimestamp: false, // Include timestamps
}
hook, err := logfil.New(opts, &logrus.JSONFormatter{})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer hook.Close()
logger := logrus.New()
logger.SetOutput(os.Stderr)
logger.AddHook(hook)
// IMPORTANT: Use fields, not message parameter
logger.WithFields(logrus.Fields{
"msg": "Application started",
"action": "startup",
"user": "system",
}).Info("ignored")
time.Sleep(100 * time.Millisecond)
fmt.Println("Logs written to file with rotation detection enabled")
}
Output: Logs written to file with rotation detection enabled
Example (RotationDetection) ¶
Example_rotationDetection demonstrates automatic log rotation detection.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-rotate-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
logFile := filepath.Join(tmpDir, "app.log")
opts := logcfg.OptionsFile{
Filepath: logFile,
CreatePath: true, // Required for rotation detection
Create: true, // Required for automatic file creation after rotation
}
hook, err := logfil.New(opts, &logrus.TextFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer hook.Close()
logger := logrus.New()
logger.SetOutput(os.Stderr)
logger.AddHook(hook)
// Write first log entry
logger.WithField("msg", "Before rotation").Info("ignored")
time.Sleep(100 * time.Millisecond)
// Simulate external log rotation (like logrotate)
os.Rename(logFile, logFile+".1")
// Wait for rotation detection (sync timer runs every 1 second)
time.Sleep(2500 * time.Millisecond)
// Write after rotation - should go to new file
logger.WithField("msg", "After rotation").Info("ignored")
time.Sleep(500 * time.Millisecond)
// Close hook to flush
_ = hook.Close()
time.Sleep(100 * time.Millisecond)
// Check that new file was created
if _, err := os.Stat(logFile); err == nil {
fmt.Println("New log file created after rotation")
}
}
Output: New log file created after rotation
Example (SeparateAccessLogs) ¶
Example_separateAccessLogs demonstrates separating access logs from application logs.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-sep-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
appLog := filepath.Join(tmpDir, "app.log")
accessLog := filepath.Join(tmpDir, "access.log")
// Application logs (standard mode with fields)
appOpts := logcfg.OptionsFile{
Filepath: appLog,
CreatePath: true,
}
appHook, _ := logfil.New(appOpts, &logrus.JSONFormatter{})
defer appHook.Close()
appLogger := logrus.New()
appLogger.SetOutput(os.Stderr)
appLogger.AddHook(appHook)
// Access logs (access log mode with message)
accessOpts := logcfg.OptionsFile{
Filepath: accessLog,
CreatePath: true,
EnableAccessLog: true,
}
accessHook, _ := logfil.New(accessOpts, nil)
defer accessHook.Close()
accessLogger := logrus.New()
accessLogger.SetOutput(os.Stderr)
accessLogger.AddHook(accessHook)
// Application log (uses fields)
appLogger.WithField("msg", "Request processed").Info("ignored")
// Access log (uses message)
accessLogger.Info("GET /api/users - 200 OK - 45ms")
time.Sleep(100 * time.Millisecond)
fmt.Println("Application and access logs separated successfully")
}
Output: Application and access logs separated successfully
Example (TraceEnabled) ¶
Example_traceEnabled demonstrates enabling trace information in logs.
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
logcfg "github.com/nabbar/golib/logger/config"
logfil "github.com/nabbar/golib/logger/hookfile"
)
func main() {
tmpDir, _ := os.MkdirTemp("", "hookfile-trace-*")
defer os.RemoveAll(tmpDir)
defer logfil.ResetOpenFiles()
logFile := filepath.Join(tmpDir, "app.log")
opts := logcfg.OptionsFile{
Filepath: logFile,
CreatePath: true,
EnableTrace: true, // Include caller/file/line information
}
hook, err := logfil.New(opts, &logrus.TextFormatter{
DisableTimestamp: true,
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer hook.Close()
logger := logrus.New()
logger.SetOutput(os.Stderr)
logger.AddHook(hook)
// IMPORTANT: message "ignored" is NOT used, only fields
logger.WithFields(logrus.Fields{
"msg": "Log with trace",
"caller": "example_test.go:line",
"file": "example_test.go",
"line": 123,
}).Info("ignored")
time.Sleep(100 * time.Millisecond)
content, _ := os.ReadFile(logFile)
fmt.Print(string(content))
}
Output: level=info caller="example_test.go:line" fields.msg="Log with trace" file=example_test.go line=123
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ResetOpenFiles ¶ added in v1.19.0
func ResetOpenFiles()
ResetOpenFiles closes all open file aggregators and clears the aggregator map. This function is primarily used for testing and cleanup purposes.
It iterates through all registered file aggregators and:
- Closes the aggregator writer
- Closes the underlying file descriptor
- Closes the root file handle
- Removes the aggregator from the global map
This function is thread-safe but should be used with caution in production as it will close all active log file handles.
Types ¶
type HookFile ¶
HookFile defines the interface for a logrus hook that writes logs to files. It embeds the base Hook interface from golib/logger/types.
func New ¶
New creates and initializes a new file hook with the specified options and formatter.
Parameters:
- opt: Configuration options for the file hook including file path, permissions, and log levels
- format: The logrus.Formatter to use for formatting log entries
Returns:
- HookFile: The initialized file hook instance
- error: An error if the hook could not be created (e.g., invalid file path)
The function will create necessary directories if CreatePath is enabled in options. For automatic log rotation support, both CreatePath and Create must be enabled. If no log levels are specified, it will log all levels by default.
Example usage:
opts := logcfg.OptionsFile{
Filepath: "/var/log/myapp.log",
CreatePath: true,
Create: true,
FileMode: 0644,
PathMode: 0755,
LogLevel: []string{"info", "warning", "error"},
}
hook, err := New(opts, &logrus.TextFormatter{})
if err != nil {
log.Fatal(err)
}
logger := logrus.New()
logger.AddHook(hook)
// Remember to use Data field for messages:
logger.WithField("msg", "Application started").Info("")