Skip to content

Commit

Permalink
feat: Add support for log-hooks (#74)
Browse files Browse the repository at this point in the history
Implemented using a custom struct HookEntry, to avoid exposing implementation-detail of logrus in our hooks. Might make the transition to another log-library easier.
  • Loading branch information
bendiknesbo authored Dec 28, 2023
1 parent d8e5a5e commit c52beb3
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 1 deletion.
10 changes: 9 additions & 1 deletion global_logger.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package logger

import "io"
import (
"context"
"io"
)

var globalLogger = New()

Expand Down Expand Up @@ -29,6 +32,11 @@ func WithFields(fields Fields) Entry {
return globalLogger.WithFields(fields)
}

// WithContext creates log entry using global logger
func WithContext(ctx context.Context) Entry {
return globalLogger.WithContext(ctx)
}

// Info uses global logger to log payload on "info" level
func Info(args ...interface{}) {
globalLogger.Info(args...)
Expand Down
71 changes: 71 additions & 0 deletions hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package logger

import (
"context"

"github.com/sirupsen/logrus"
)

// Hook defines the interface a custom Hook needs to implement
type Hook interface {
Fire(*HookEntry) (changed bool, err error)
}

// HookFunc can be used to convert a simple function to implement the Hook interface.
type HookFunc func(*HookEntry) (changed bool, err error)

// Fire redirects a function call to the function receiver
func (hf HookFunc) Fire(he *HookEntry) (changed bool, err error) {
return hf(he)
}

// HookEntry contains the fields provided for mutation in a hook.
type HookEntry struct {
// Contains all the fields set by the user.
Data Fields

// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
Level Level

// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
Message string

// Contains the context set by the user. Useful for hook processing etc.
Context context.Context
}

type customHook struct {
hook Hook
}

// Levels implements the logrus.Hook interface.
func (h *customHook) Levels() []logrus.Level {
return logrus.AllLevels
}

// Fire implements the logrus.Hook interface.
func (h *customHook) Fire(entry *logrus.Entry) error {
// Provide all entry-data so the hook can mutate them.
hookEntry := &HookEntry{
Data: Fields(entry.Data),
Level: mapLogrusLevelToLevel(entry.Level),
Message: entry.Message,
Context: entry.Context,
}
changed, err := h.hook.Fire(hookEntry)
if err != nil {
return err
}
if !changed {
return nil
}

// Mutate the actual logrus entry with the mutations done in the hook.
entry.Data = logrus.Fields(hookEntry.Data)
entry.Level = mapLevelToLogrusLevel(hookEntry.Level)
entry.Message = hookEntry.Message
entry.Context = hookEntry.Context

return nil
}
17 changes: 17 additions & 0 deletions level.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,20 @@ func mapLevelToLogrusLevel(l Level) logrus.Level {
// should never get here
return logrus.DebugLevel
}

func mapLogrusLevelToLevel(l logrus.Level) Level {
switch l {
case logrus.FatalLevel:
return LevelFatal
case logrus.ErrorLevel:
return LevelError
case logrus.WarnLevel:
return LevelWarn
case logrus.InfoLevel:
return LevelInfo
case logrus.DebugLevel:
return LevelDebug
}
// should never get here
return LevelDebug
}
6 changes: 6 additions & 0 deletions logger.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package logger

import (
"context"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -74,6 +75,11 @@ func (logger *Logger) WithFields(fields Fields) Entry {
return logger.logrusLogger.WithTime(logger.now()).WithFields(logrus.Fields(fields))
}

// WithContext forwards a logging call with fields
func (logger *Logger) WithContext(ctx context.Context) Entry {
return logger.logrusLogger.WithTime(logger.now()).WithContext(ctx)
}

// OutputHandler returns logger output handler
func (logger *Logger) OutputHandler() io.Writer {
return logger.output
Expand Down
52 changes: 52 additions & 0 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package logger

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -302,3 +303,54 @@ func TestDisableReportingCaller(t *testing.T) {
assertLogEntryDoesNotHaveKey(t, tee, "file")
assertLogEntryDoesNotHaveKey(t, tee, "function")
}

type myCtxKey struct{}

// Fire - implement Hook interface fire the entry
func testHook(entry *HookEntry) (bool, error) {
ctx := entry.Context
if ctx == nil {
return false, nil
}

val := ctx.Value(myCtxKey{})
if val == nil {
return false, nil
}

str, ok := val.(string)
if !ok || str == "" {
return false, nil
}

entry.Data["my-custom-log-key"] = str
return true, nil
}

func TestHookWithContext(t *testing.T) {
ctx := context.WithValue(context.Background(), myCtxKey{}, "my-custom-ctx-value")

buf := &bytes.Buffer{}
tee := io.TeeReader(buf, buf)
logger := New(WithOutput(buf), WithHookFunc(testHook))
logger.WithContext(ctx).Error("foobar")
assertLogEntryContains(t, tee, "my-custom-log-key", "my-custom-ctx-value")
}

func TestHookWithoutContext(t *testing.T) {
buf := &bytes.Buffer{}
tee := io.TeeReader(buf, buf)
logger := New(WithOutput(buf), WithHookFunc(testHook))
logger.Error("foobar")
assertLogEntryDoesNotHaveKey(t, tee, "my-custom-log-key")
}

func TestHookWithContext2(t *testing.T) {
ctx := context.WithValue(context.Background(), myCtxKey{}, "my-custom-ctx-value")

buf := &bytes.Buffer{}
tee := io.TeeReader(buf, buf)
logger := New(WithOutput(buf), WithHookFunc(testHook))
logger.WithContext(ctx).Error("foobar")
assertLogEntryContains(t, tee, "my-custom-log-key", "my-custom-ctx-value")
}
12 changes: 12 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,15 @@ func WithReportCaller(enable bool) LoggerOption {
l.reportCaller = enable
})
}

// WithHookFunc allows for connecting a hook to the logger, which will be triggered on all log-entries.
func WithHookFunc(hook HookFunc) LoggerOption {
return WithHook(hook)
}

// WithHook allows for connecting a hook to the logger, which will be triggered on all log-entries.
func WithHook(hook Hook) LoggerOption {
return LoggerOptionFunc(func(l *Logger) {
l.logrusLogger.Hooks.Add(&customHook{hook: hook})
})
}

0 comments on commit c52beb3

Please sign in to comment.