hookfile

package
v1.19.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2026 License: MIT Imports: 18 Imported by: 0

README

Logger HookFile

Go Version License Coverage

Logrus hook for writing log entries to files with automatic rotation detection, efficient multi-writer aggregation, and configurable field filtering.


Table of Contents


Overview

The hookfile package provides a production-ready logrus.Hook that writes log entries to files with sophisticated features not found in standard file logging. It automatically detects log rotation, efficiently aggregates writes from multiple loggers, and provides fine-grained control over log formatting.

Design Philosophy
  1. Rotation-Aware: Automatically detect and handle external log rotation using inode tracking
  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
  • 84.0% Test Coverage: 28 specs + 10 examples, zero race conditions

Architecture

Component Diagram
┌─────────────────────────────────────────────┐
│           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  │
           └──────────┘
Data Flow

File Aggregation:

  1. Multiple hooks created for same file path
  2. First hook creates file aggregator with refcount=1
  3. Subsequent hooks increment refcount (reuse aggregator)
  4. Each hook.Close() decrements refcount
  5. When refcount reaches 0, file and aggregator are closed

Rotation Detection:

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

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

Time T3: Hook writes → file descriptor points to NEW inode 67890
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):

  • Fields (logrus.Fields) are IGNORED
  • Message parameter IS written to output
  • To log a message: use logger.Info("GET /api - 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("ignored")
// 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)

Performance

Benchmarks

Based on test results with gmeasure:

Metric Value Notes
Write Latency (Median) 106ms Includes formatting + buffer
Write Latency (Mean) 119ms Average under normal load
Write Latency (P99) 169ms 99th percentile
Memory Usage ~280KB Per file aggregator
Throughput ~5000-10000 entries/sec Depends on formatter
Rotation Detection 1s Sync timer interval
File Reopen 1-5ms During rotation
Memory Usage
Hook struct:        ~120 bytes (minimal footprint)
File aggregator:    ~280 KB (includes buffers)
Per operation:      0 allocations (zero-copy delegation)
Reference counting: ~16 bytes per hook
Total per file:     ~280 KB (shared across hooks)

Memory characteristics:

  • File handles shared across multiple hooks (reference counted)
  • Aggregator reuses buffers to minimize GC pressure
  • No heap allocations during normal operation
  • Suitable for high-volume applications (thousands of concurrent hooks)
Scalability
  • Concurrent Writers: Multiple goroutines can log safely
  • File Sharing: Multiple hooks efficiently share single file
  • Reference Counting: Automatic resource cleanup
  • Thread-Safe: All operations safe for concurrent use
  • Zero Race Conditions: Tested with -race detector
  • Rotation Resilience: Continues logging during rotation

Tested Scalability:

  • 100+ concurrent goroutines writing to same file
  • 10+ loggers sharing single file
  • Millions of log entries without memory leaks
  • Sub-second rotation detection and recovery

Use Cases

1. Production Application with Log Rotation

Problem: Application needs persistent file logging with external log rotation (logrotate).

Solution: Use HookFile with CreatePath=true for automatic rotation detection.

Advantages:

  • Automatic rotation detection via inode comparison
  • No application restart required after rotation
  • Continues logging to new file after rotation
  • Compatible with all rotation tools (logrotate, etc.)

Suited for: Production servers, long-running daemons, system services, containerized apps with volume mounts.

2. Multi-Logger Applications

Problem: Multiple loggers in same application need to write to shared log file.

Solution: Create multiple HookFile instances for same file path.

Advantages:

  • Single file descriptor shared across all hooks
  • Reference counting prevents premature file closure
  • Thread-safe concurrent writes via aggregator
  • No file handle exhaustion

Suited for: Microservices, multi-tenant applications, plugin architectures, distributed logging.

3. Separate Access and Application Logs

Problem: HTTP access logs need different format and file from application logs.

Solution: Use two hooks - one in AccessLog mode, one in standard mode.

Advantages:

  • Clean access log format (message-only)
  • Structured application logs (JSON/fields)
  • Independent rotation policies
  • Easy parsing with standard tools

Suited for: Web servers, API gateways, reverse proxies, HTTP middleware.

4. Level-Specific Log Files

Problem: Different log levels need separate files (errors separate from info).

Solution: Create multiple hooks with different LogLevel configurations.

Advantages:

  • Errors written to separate file for easy monitoring
  • Debug logs isolated from production logs
  • Independent retention policies per level
  • Efficient filtering at hook level

Suited for: Debugging environments, error monitoring, compliance logging, audit trails.

5. Structured Logging for Log Aggregation

Problem: Logs need structured format for ELK, Splunk, CloudWatch, etc.

Solution: Use HookFile with JSON formatter for machine-readable logs.

Advantages:

  • Structured JSON for easy parsing
  • Compatible with log aggregation tools
  • Field filtering for sensitive data
  • Automatic rotation for log shippers

Suited for: Cloud-native apps, microservices, observability platforms, SIEM integration.


Quick Start

Installation
go get github.com/nabbar/golib/logger/hookfile

Requirements:

  • Go 1.24 or higher (requires os.OpenRoot)
  • Compatible with Linux, macOS, Windows
Basic Example

Write logs to a file with automatic rotation detection:

package main

import (
    "github.com/sirupsen/logrus"
    "github.com/nabbar/golib/logger/config"
    "github.com/nabbar/golib/logger/hookfile"
)

func main() {
    // Configure file hook
    opts := config.OptionsFile{
        Filepath:   "/var/log/myapp/app.log",
        FileMode:   0644,
        PathMode:   0755,
        CreatePath: true,  // Enable rotation detection
        Create:     true,  // Enable file creation after rotation
    }

    // Create hook
    hook, err := hookfile.New(opts, &logrus.TextFormatter{})
    if err != nil {
        panic(err)
    }
    defer hook.Close()

    // Configure logger
    logger := logrus.New()
    logger.AddHook(hook)

    // IMPORTANT: Message parameter "ignored" is NOT used.
    // Only fields are written to the file.
    logger.WithField("msg", "Application started").Info("ignored")
    // Output to file: level=info fields.msg="Application started"
}
Production Setup

Production-ready configuration with JSON formatter:

opts := config.OptionsFile{
    Filepath:         "/var/log/myapp/app.log",
    FileMode:         0644,  // Readable by others
    PathMode:         0755,  // Standard directory permissions
    CreatePath:       true,  // Create dirs + rotation detection
    LogLevel:         []string{"info", "warning", "error"},
    DisableStack:     true,  // Don't log stack traces
    DisableTimestamp: false, // Include timestamps
}

hook, _ := hookfile.New(opts, &logrus.JSONFormatter{})
defer hook.Close()

logger := logrus.New()
logger.AddHook(hook)

// IMPORTANT: Use fields, not message parameter
logger.WithFields(logrus.Fields{
    "msg":    "Request processed",
    "method": "GET",
    "status": 200,
}).Info("ignored")
// Output: {"fields.msg":"Request processed","level":"info","method":"GET","status":200}
Access Log Mode

Use message-only mode for HTTP access logs:

accessOpts := config.OptionsFile{
    Filepath:        "/var/log/myapp/access.log",
    CreatePath:      true,
    EnableAccessLog: true,  // Message-only mode
}

accessHook, _ := hookfile.New(accessOpts, nil)
defer accessHook.Close()

accessLogger := logrus.New()
accessLogger.AddHook(accessHook)

// IMPORTANT: In AccessLog mode, MESSAGE is output, fields ignored
accessLogger.WithFields(logrus.Fields{
    "method": "GET",  // This field is IGNORED
    "path":   "/api", // This field is IGNORED
}).Info("GET /api/users - 200 OK - 45ms")
// Output: GET /api/users - 200 OK - 45ms
Level Filtering

Route different log levels to different files:

// Error log file
errorOpts := config.OptionsFile{
    Filepath: "/var/log/myapp/error.log",
    CreatePath: true,
    LogLevel: []string{"error", "fatal"},
}
errorHook, _ := hookfile.New(errorOpts, &logrus.JSONFormatter{})
defer errorHook.Close()

// Info log file
infoOpts := config.OptionsFile{
    Filepath: "/var/log/myapp/info.log",
    CreatePath: true,
    LogLevel: []string{"info", "debug"},
}
infoHook, _ := hookfile.New(infoOpts, &logrus.TextFormatter{})
defer infoHook.Close()

logger := logrus.New()
logger.AddHook(errorHook)
logger.AddHook(infoHook)

// IMPORTANT: Use fields, message parameter is ignored
logger.WithField("msg", "Normal operation").Info("ignored")    // → info.log
logger.WithField("msg", "Error occurred").Error("ignored")     // → error.log
Field Filtering

Filter out verbose fields for cleaner output:

opts := config.OptionsFile{
    Filepath:         "/var/log/myapp/app.log",
    CreatePath:       true,
    DisableStack:     true,  // Remove stack traces
    DisableTimestamp: true,  // Remove timestamps
    EnableTrace:      false, // Remove caller info
}

hook, _ := hookfile.New(opts, &logrus.TextFormatter{
    DisableTimestamp: true,
})
defer hook.Close()

logger := logrus.New()
logger.AddHook(hook)

// IMPORTANT: Fields are used, message parameter is ignored
logger.WithFields(logrus.Fields{
    "msg":    "Clean log",
    "stack":  "will be filtered",  // Removed by DisableStack
    "caller": "will be filtered",  // Removed by EnableTrace=false
    "user":   "john",               // Kept
}).Info("ignored")
// Output: level=info fields.msg="Clean log" user=john

Best Practices

Testing

The package includes comprehensive tests with 84.0% code coverage and 28 test specifications using BDD methodology (Ginkgo v2 + Gomega).

Key test coverage:

  • ✅ Hook creation and configuration
  • ✅ File writing and rotation detection
  • ✅ Multiple loggers sharing same file
  • ✅ Access log mode and field filtering
  • ✅ Concurrency and race conditions
  • ✅ Integration with logrus formatters

For detailed test documentation, see TESTING.md.

✅ DO

Configure log rotation externally:

# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0644 myapp myapp
}

Use fields for structured logging:

// ✅ GOOD: Fields are output
logger.WithFields(logrus.Fields{
    "user_id": 123,
    "action":  "login",
    "msg":     "User logged in",
}).Info("ignored")

Enable rotation detection:

opts := config.OptionsFile{
    Filepath:   "/var/log/app.log",
    CreatePath: true,  // Required for rotation detection
}

Separate stdout and file logs:

logger := logrus.New()
logger.SetOutput(os.Stdout)  // Console output
logger.AddHook(fileHook)     // File output

Close hooks on shutdown:

hook, _ := hookfile.New(opts, formatter)
defer hook.Close()  // Ensures proper cleanup
❌ DON'T

Don't rely on message parameter in standard mode:

// ❌ BAD: Message "important" is NOT output
logger.Info("important")

// ✅ GOOD: Put text in field
logger.WithField("msg", "important").Info("ignored")

Don't manually rotate files from application:

// ❌ BAD: Manual rotation
os.Rename("/var/log/app.log", "/var/log/app.log.1")
// Hook will detect and handle rotation automatically

// ✅ GOOD: Use external tools
// Configure logrotate or similar

Don't create hundreds of hooks to same file:

// ❌ BAD: Excessive hooks
for i := 0; i < 1000; i++ {
    hook, _ := hookfile.New(sameOpts, formatter)
    logger.AddHook(hook)
}

// ✅ GOOD: One hook per logger, multiple loggers OK
hook, _ := hookfile.New(opts, formatter)
logger1.AddHook(hook)  // Reuse same hook

Don't mix AccessLog and standard logging:

// ❌ BAD: Single logger with AccessLog mode
hook, _ := hookfile.New(&config.OptionsFile{
    EnableAccessLog: true,
}, nil)
logger.AddHook(hook)
logger.Info("app message")  // Confusing behavior

// ✅ GOOD: Separate loggers
appLogger.AddHook(appHook)
accessLogger.AddHook(accessHook)

Don't ignore errors:

// ❌ BAD: Ignore errors
hook, _ := hookfile.New(opts, formatter)

// ✅ GOOD: Handle errors
hook, err := hookfile.New(opts, formatter)
if err != nil {
    log.Fatalf("Failed to create hook: %v", err)
}

API Reference

HookFile Interface

HookFile

Extends logtps.Hook interface with file-specific functionality:

type HookFile interface {
    logtps.Hook
    // Inherits: Fire, Levels, RegisterHook, Run, IsRunning, Close, Write
}
Configuration

New(opt config.OptionsFile, format logrus.Formatter) (HookFile, error)

Creates a new file hook with specified options and formatter.

  • Parameters:

    • opt: File configuration including path, permissions, log levels
    • format: Logrus formatter (JSON, Text, or custom). If nil, uses entry.Bytes()
  • Returns:

    • HookFile: Configured hook instance
    • error: Error if file cannot be created or accessed

config.OptionsFile struct:

type OptionsFile struct {
    Filepath         string      // Required: Path to log file
    FileMode         FileMode    // File permissions (default: 0644)
    PathMode         FileMode    // Directory permissions (default: 0755)
    CreatePath       bool        // Create parent directories (required for rotation)
    Create           bool        // Create file if missing (required for rotation)
    LogLevel         []string    // Log levels to handle (default: all)
    DisableStack     bool        // Filter "stack" field
    DisableTimestamp bool        // Filter "time" field
    EnableTrace      bool        // Include "caller", "file", "line" fields
    EnableAccessLog  bool        // Message-only mode (ignores fields)
}
Rotation Detection

The hook automatically detects log rotation when CreatePath=true:

How it works:

  1. Sync timer runs every 1 second
  2. Compares file descriptor inode with disk file inode
  3. If different, closes old FD and opens new file
  4. Logging continues seamlessly

Supported rotation tools:

  • logrotate (Linux)
  • newsyslog (BSD)
  • Any tool that renames/moves log files

Detection latency: Up to 1 second (sync timer interval)

Error Handling

Construction Errors:

hook, err := hookfile.New(config.OptionsFile{
    Filepath: "",  // Missing
}, nil)
// err: "missing file path"

Runtime Errors:

  • Formatter errors during Fire() are returned
  • Write errors during Fire() are returned
  • File rotation errors logged to stderr, continue with old FD

Silent Behaviors:

  • Empty log data: Fire() returns nil without writing
  • Empty access log message: Fire() returns nil
  • Entry level not in LogLevel: Fire() returns nil (normal filtering)

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 gofmt and golint
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 Requirements
  • Add tests for new features
  • Use Ginkgo v2 / Gomega for test framework
  • Ensure zero race conditions with -race flag
  • Update examples if needed
Documentation Requirements
  • Update GoDoc comments for public APIs
  • Add runnable examples for new features
  • Update README.md and TESTING.md if needed
Pull Request Process
  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write clear commit messages
  4. Ensure all tests pass (go test -race ./...)
  5. Update documentation
  6. 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
  • 84.0% test coverage (target: >80%)
  • Zero race conditions detected with -race flag
  • Thread-safe implementation with file aggregation
  • Memory-safe with proper resource cleanup
  • Rotation-resilient with automatic detection
Security Considerations

No Security Vulnerabilities Identified:

  • No external dependencies beyond Go stdlib + internal golib
  • File permissions configurable (FileMode, PathMode)
  • No privilege escalation paths
  • Safe inode comparison for rotation detection
  • Proper error handling prevents crashes

Best Practices Applied:

  • Defensive nil checks in constructors
  • Proper error propagation
  • No panic paths in normal operation
  • Resource cleanup with defer and Close()
  • Reference counting prevents leaks
Future Enhancements (Non-urgent)

The following enhancements could be considered for future versions:

  1. Custom Sync Timer: Configurable rotation detection interval
  2. Size-Based Rotation: Built-in rotation based on file size
  3. Compression: Automatic compression of rotated files
  4. Metrics Export: Integration with Prometheus for hook metrics
  5. Custom Rotation Callbacks: User-defined rotation handlers

These are optional improvements and not required for production use. 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, rotation detection algorithm, typical use cases, and comprehensive usage examples. Provides detailed explanations of file-specific behavior.

  • TESTING.md - Comprehensive test suite documentation covering test architecture, BDD methodology with Ginkgo v2, coverage analysis (82.2%), and guidelines for writing new tests. Includes troubleshooting and CI integration examples.

External References
  • Logrus - Underlying structured logging framework. Essential for understanding hook behavior, formatters, and field handling.

  • Logrotate - Standard Linux log rotation utility. Compatible with this package's rotation detection.

  • Effective Go - Official Go programming guide covering best practices for interfaces, error handling, and logging patterns. The hookfile package follows these conventions.

  • Go Standard Library - Standard library documentation for os, io, and logging-related packages. Understanding os.File and inode handling is essential for rotation detection.


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/hookfile
Version: See releases for versioning

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
  • 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.

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.

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

type HookFile interface {
	logtps.Hook
}

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

func New(opt logcfg.OptionsFile, format logrus.Formatter) (HookFile, error)

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("")

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL