Overview
Custom hooks allow you to extend go_logs with your own log processing logic. This guide shows how to create hooks for various use cases including metrics collection, alerting, filtering, and integration with external systems.Quick Start
Simple Function Hook
The easiest way to create a hook:import "github.com/drossan/go_logs"
hook := go_logs.NewFuncHook(func(entry *go_logs.Entry) error {
// Your processing logic here
fmt.Printf("Log: %s\n", entry.Message)
return nil
})
logger := go_logs.New(
go_logs.WithHooks(hook),
)
Struct-Based Hook
For hooks that need state:type MyHook struct {
// Hook state
}
func (h *MyHook) Run(entry *go_logs.Entry) error {
// Your processing logic here
return nil
}
// Usage
hook := &MyHook{}
logger := go_logs.New(
go_logs.WithHooks(hook),
)
Hook Interface
All hooks must implement this interface:type Hook interface {
Run(entry *Entry) error
}
entry- Complete log entry with level, message, fields, timestamp, etc.
error- Return error if hook processing failed, nil otherwise
Entry Structure
The Entry passed to hooks contains:type Entry struct {
Level Level // Log level (TRACE, DEBUG, INFO, etc.)
Message string // Log message
Fields []Field // Structured key-value fields
Timestamp time.Time // When log was created
Caller *CallerInfo // Source file/line (if enabled)
StackTrace []byte // Stack trace (if enabled)
}
// Inspection
entry.HasField(key string) bool
entry.GetField(key string) Field
entry.GetFieldValue(key string) interface{}
entry.FieldCount() int
// Modification (returns new entry)
entry.Clone() *Entry
entry.WithFields(fields ...Field) *Entry
entry.WithLevel(level Level) *Entry
entry.WithMessage(msg string) *Entry
Common Hook Patterns
1. Metrics Collection Hook
import (
"github.com/drossan/go_logs"
"sync/atomic"
)
type MetricsHook struct {
totalLogs int64
errorLogs int64
warningLogs int64
}
func (h *MetricsHook) Run(entry *go_logs.Entry) error {
atomic.AddInt64(&h.totalLogs, 1)
switch entry.Level {
case go_logs.ErrorLevel, go_logs.FatalLevel:
atomic.AddInt64(&h.errorLogs, 1)
case go_logs.WarnLevel:
atomic.AddInt64(&h.warningLogs, 1)
}
return nil
}
func (h *MetricsHook) GetMetrics() (total, errors, warnings int64) {
return atomic.LoadInt64(&h.totalLogs),
atomic.LoadInt64(&h.errorLogs),
atomic.LoadInt64(&h.warningLogs)
}
// Usage
metrics := &MetricsHook{}
logger := go_logs.New(go_logs.WithHooks(metrics))
// Later: retrieve metrics
total, errors, warnings := metrics.GetMetrics()
fmt.Printf("Total: %d, Errors: %d, Warnings: %d\n", total, errors, warnings)
2. Level-Filtered Hook
type LevelFilteredHook struct {
minLevel go_logs.Level
handler func(*go_logs.Entry) error
}
func NewLevelFilteredHook(minLevel go_logs.Level, handler func(*go_logs.Entry) error) *LevelFilteredHook {
return &LevelFilteredHook{
minLevel: minLevel,
handler: handler,
}
}
func (h *LevelFilteredHook) Run(entry *go_logs.Entry) error {
// Fast-path: skip if below threshold
if !entry.Level.ShouldLog(h.minLevel) {
return nil
}
return h.handler(entry)
}
// Usage: Only process errors and above
hook := NewLevelFilteredHook(go_logs.ErrorLevel, func(entry *go_logs.Entry) error {
return sendAlert(entry)
})
3. Async Hook with Channel
import "context"
type AsyncHook struct {
entryChan chan *go_logs.Entry
ctx context.Context
cancel context.CancelFunc
}
func NewAsyncHook(bufferSize int) *AsyncHook {
ctx, cancel := context.WithCancel(context.Background())
h := &AsyncHook{
entryChan: make(chan *go_logs.Entry, bufferSize),
ctx: ctx,
cancel: cancel,
}
// Start background processor
go h.process()
return h
}
func (h *AsyncHook) Run(entry *go_logs.Entry) error {
// Non-blocking send (drops if buffer full)
select {
case h.entryChan <- entry.Clone():
default:
// Buffer full, drop entry
}
return nil
}
func (h *AsyncHook) process() {
for {
select {
case entry := <-h.entryChan:
// Process entry asynchronously
h.handleEntry(entry)
case <-h.ctx.Done():
return
}
}
}
func (h *AsyncHook) handleEntry(entry *go_logs.Entry) {
// Your async processing logic
sendToExternalSystem(entry)
}
func (h *AsyncHook) Stop() {
h.cancel()
close(h.entryChan)
}
// Usage
asyncHook := NewAsyncHook(1000) // Buffer 1000 entries
defer asyncHook.Stop()
logger := go_logs.New(go_logs.WithHooks(asyncHook))
4. Sampling Hook (Rate Limiting)
import (
"sync"
"time"
)
type SamplingHook struct {
interval time.Duration
maxCount int
handler func(*go_logs.Entry) error
mu sync.Mutex
count int
lastReset time.Time
}
func NewSamplingHook(interval time.Duration, maxCount int, handler func(*go_logs.Entry) error) *SamplingHook {
return &SamplingHook{
interval: interval,
maxCount: maxCount,
handler: handler,
lastReset: time.Now(),
}
}
func (h *SamplingHook) Run(entry *go_logs.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
now := time.Now()
// Reset counter if interval has passed
if now.Sub(h.lastReset) > h.interval {
h.count = 0
h.lastReset = now
}
// Check if within limit
if h.count >= h.maxCount {
return nil // Drop this entry
}
h.count++
return h.handler(entry)
}
// Usage: Max 100 alerts per minute
hook := NewSamplingHook(time.Minute, 100, func(entry *go_logs.Entry) error {
return sendAlert(entry)
})
5. Field-Based Routing Hook
type RoutingHook struct {
routes map[string]func(*go_logs.Entry) error
}
func NewRoutingHook() *RoutingHook {
return &RoutingHook{
routes: make(map[string]func(*go_logs.Entry) error),
}
}
func (h *RoutingHook) AddRoute(fieldKey string, handler func(*go_logs.Entry) error) {
h.routes[fieldKey] = handler
}
func (h *RoutingHook) Run(entry *go_logs.Entry) error {
// Route based on field presence
for key, handler := range h.routes {
if entry.HasField(key) {
if err := handler(entry); err != nil {
return err
}
}
}
return nil
}
// Usage
router := NewRoutingHook()
// Route user events to user log
router.AddRoute("user_id", func(entry *go_logs.Entry) error {
return logToUserFile(entry)
})
// Route payment events to audit log
router.AddRoute("payment_id", func(entry *go_logs.Entry) error {
return logToAuditSystem(entry)
})
logger := go_logs.New(go_logs.WithHooks(router))
6. Deduplication Hook
import (
"crypto/sha256"
"fmt"
"sync"
"time"
)
type DeduplicationHook struct {
window time.Duration
handler func(*go_logs.Entry) error
mu sync.Mutex
seen map[string]time.Time
}
func NewDeduplicationHook(window time.Duration, handler func(*go_logs.Entry) error) *DeduplicationHook {
return &DeduplicationHook{
window: window,
handler: handler,
seen: make(map[string]time.Time),
}
}
func (h *DeduplicationHook) Run(entry *go_logs.Entry) error {
// Create hash of message + level
hash := h.hash(entry)
h.mu.Lock()
defer h.mu.Unlock()
now := time.Now()
// Clean old entries
for k, t := range h.seen {
if now.Sub(t) > h.window {
delete(h.seen, k)
}
}
// Check if seen recently
if lastSeen, ok := h.seen[hash]; ok {
if now.Sub(lastSeen) < h.window {
return nil // Duplicate, skip
}
}
h.seen[hash] = now
return h.handler(entry)
}
func (h *DeduplicationHook) hash(entry *go_logs.Entry) string {
data := fmt.Sprintf("%s:%s", entry.Level, entry.Message)
sum := sha256.Sum256([]byte(data))
return fmt.Sprintf("%x", sum)
}
// Usage: Suppress duplicate errors within 1 minute
hook := NewDeduplicationHook(time.Minute, func(entry *go_logs.Entry) error {
return sendAlert(entry)
})
7. File Output Hook
import (
"encoding/json"
"os"
"sync"
)
type FileHook struct {
file *os.File
mu sync.Mutex
}
func NewFileHook(filename string) (*FileHook, error) {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
return &FileHook{file: file}, nil
}
func (h *FileHook) Run(entry *go_logs.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
// Write as JSON
data := map[string]interface{}{
"timestamp": entry.Timestamp,
"level": entry.Level.String(),
"message": entry.Message,
}
// Add fields
if len(entry.Fields) > 0 {
fields := make(map[string]interface{})
for _, f := range entry.Fields {
fields[f.Key()] = f.Value()
}
data["fields"] = fields
}
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
_, err = h.file.Write(append(jsonData, '\n'))
return err
}
func (h *FileHook) Close() error {
return h.file.Close()
}
// Usage: Log errors to separate file
fileHook, _ := NewFileHook("/var/log/errors.json")
defer fileHook.Close()
filteredHook := NewLevelFilteredHook(go_logs.ErrorLevel, fileHook.Run)
logger := go_logs.New(go_logs.WithHooks(filteredHook))
Best Practices
Keep Hooks FastHooks run synchronously. For slow operations, use async processing:
// Good: Fast hook with async processing
hook := go_logs.NewFuncHook(func(entry *go_logs.Entry) error {
select {
case eventChan <- entry.Clone():
default:
}
return nil
})
// Bad: Slow synchronous operation
hook := go_logs.NewFuncHook(func(entry *go_logs.Entry) error {
return http.Post("...", ...) // Blocks logging!
})
Level FilteringFilter early to avoid unnecessary processing:
func (h *MyHook) Run(entry *go_logs.Entry) error {
// Fast-path: check level first
if !entry.Level.ShouldLog(go_logs.ErrorLevel) {
return nil
}
// Expensive processing only for errors
return h.processError(entry)
}
Clone Entries for Async ProcessingAlways clone entries before passing to goroutines:
hook := go_logs.NewFuncHook(func(entry *go_logs.Entry) error {
cloned := entry.Clone() // Important!
go processAsync(cloned)
return nil
})
Thread SafetyMake hooks thread-safe if they maintain state:Or use atomic operations for counters:
type StatefulHook struct {
mu sync.Mutex
state map[string]interface{}
}
func (h *StatefulHook) Run(entry *go_logs.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
// Thread-safe state access
h.state[entry.Message] = entry.Timestamp
return nil
}
type CounterHook struct {
count int64
}
func (h *CounterHook) Run(entry *go_logs.Entry) error {
atomic.AddInt64(&h.count, 1)
return nil
}
Error HandlingReturn errors for failures, but know they won’t stop logging:
func (h *MyHook) Run(entry *go_logs.Entry) error {
if err := h.process(entry); err != nil {
// Error is logged but other hooks and writing continue
return fmt.Errorf("hook processing failed: %w", err)
}
return nil
}
Testing Hooks
import "testing"
func TestMetricsHook(t *testing.T) {
hook := &MetricsHook{}
// Create test entry
entry := &go_logs.Entry{
Level: go_logs.ErrorLevel,
Message: "test error",
Timestamp: time.Now(),
}
// Run hook
err := hook.Run(entry)
if err != nil {
t.Fatalf("Hook failed: %v", err)
}
// Verify metrics
total, errors, _ := hook.GetMetrics()
if total != 1 {
t.Errorf("Expected total=1, got %d", total)
}
if errors != 1 {
t.Errorf("Expected errors=1, got %d", errors)
}
}
Related
- Hook Interface - Base hook interface documentation
- SlackHook - Built-in Slack hook example
- Logger Options - WithHooks option
- Entry API - Entry methods for hooks