Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for chaining setups, by storing the setups in an intermediary #77

Merged
merged 4 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions caller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package logger

import (
"runtime"
"strings"
"sync"
)

var (

// qualified package name, cached at first use
goLoggerPackage string

// Positions in the call stack when tracing to report the calling method
minimumCallerDepth int

// Used for caller information initialisation
callerInitOnce sync.Once
)

const (
maximumCallerDepth int = 25
knownGoLoggerFrames int = 4
)

// getCaller retrieves the name of the first non-go-logger calling function
func getCaller() *runtime.Frame {
// cache this package's fully-qualified name
callerInitOnce.Do(func() {
pcs := make([]uintptr, maximumCallerDepth)
_ = runtime.Callers(0, pcs)

// dynamic get the package name and the minimum caller depth
for i := 0; i < maximumCallerDepth; i++ {
funcName := runtime.FuncForPC(pcs[i]).Name()
if strings.Contains(funcName, "getCaller") {
goLoggerPackage = getPackageName(funcName)
break
}
}

minimumCallerDepth = knownGoLoggerFrames
})

// Restrict the lookback frames to avoid runaway lookups
pcs := make([]uintptr, maximumCallerDepth)
depth := runtime.Callers(minimumCallerDepth, pcs)
frames := runtime.CallersFrames(pcs[:depth])

for f, again := frames.Next(); again; f, again = frames.Next() {
pkg := getPackageName(f.Function)

// If the caller isn't part of this package, we're done
if pkg != goLoggerPackage {
return &f //nolint:scopelint
}
}

// if we got here, we failed to find the caller's context
return nil
}

// getPackageName reduces a fully qualified function name to the package name
// There really ought to be to be a better way...
func getPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
lastSlash := strings.LastIndex(f, "/")
if lastPeriod > lastSlash {
f = f[:lastPeriod]
} else {
break
}
}

return f
}
154 changes: 81 additions & 73 deletions entry.go
Original file line number Diff line number Diff line change
@@ -1,91 +1,99 @@
package logger

import (
"runtime"
"strings"
"sync"
"context"

"github.com/sirupsen/logrus"
)

// Entry represents a logging entry and all supported method we use
type Entry interface {
Debugf(format string, args ...interface{})
Debug(args ...interface{})
Infof(format string, args ...interface{})
Info(args ...interface{})
Warnf(format string, args ...interface{})
Warn(args ...interface{})
Errorf(format string, args ...interface{})
Error(args ...interface{})
Fatalf(format string, args ...interface{})
Fatal(args ...interface{})
type Entry struct {
logger *Logger
fields Fields
context context.Context
}

var (
// WithError is a convenience wrapper for WithField("error", err)
func (e *Entry) WithError(err error) *Entry {
return e.WithField(errorKey, err)
}

// qualified package name, cached at first use
goLoggerPackage string
// WithField forwards a logging call with a field
func (e *Entry) WithField(key string, value interface{}) *Entry {
e.fields[key] = value
return e
}

// Positions in the call stack when tracing to report the calling method
minimumCallerDepth int
// WithFields forwards a logging call with fields
func (e *Entry) WithFields(fields Fields) *Entry {
for k, v := range fields {
e.fields[k] = v
}
return e
}

// Used for caller information initialisation
callerInitOnce sync.Once
)
// WithContext sets the context for the log-message. Useful when using hooks.
func (e *Entry) WithContext(ctx context.Context) *Entry {
e.context = ctx
return e
}

const (
maximumCallerDepth int = 25
knownGoLoggerFrames int = 4
)
// Info forwards a logging call in the (format, args) format
func (e *Entry) Info(args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Info(args...)
}

// getCaller retrieves the name of the first non-go-logger calling function
func getCaller() *runtime.Frame {
// cache this package's fully-qualified name
callerInitOnce.Do(func() {
pcs := make([]uintptr, maximumCallerDepth)
_ = runtime.Callers(0, pcs)

// dynamic get the package name and the minimum caller depth
for i := 0; i < maximumCallerDepth; i++ {
funcName := runtime.FuncForPC(pcs[i]).Name()
if strings.Contains(funcName, "getCaller") {
goLoggerPackage = getPackageName(funcName)
break
}
}

minimumCallerDepth = knownGoLoggerFrames
})

// Restrict the lookback frames to avoid runaway lookups
pcs := make([]uintptr, maximumCallerDepth)
depth := runtime.Callers(minimumCallerDepth, pcs)
frames := runtime.CallersFrames(pcs[:depth])

for f, again := frames.Next(); again; f, again = frames.Next() {
pkg := getPackageName(f.Function)

// If the caller isn't part of this package, we're done
if pkg != goLoggerPackage {
return &f //nolint:scopelint
}
}
// Infof forwards a logging call in the (format, args) format
func (e *Entry) Infof(format string, args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Infof(format, args...)
}

// if we got here, we failed to find the caller's context
return nil
// Error forwards an error logging call
func (e *Entry) Error(args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Error(args...)
}

// getPackageName reduces a fully qualified function name to the package name
// There really ought to be to be a better way...
func getPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
lastSlash := strings.LastIndex(f, "/")
if lastPeriod > lastSlash {
f = f[:lastPeriod]
} else {
break
}
}
// Errorf forwards an error logging call
func (e *Entry) Errorf(format string, args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Errorf(format, args...)
}

// Debug forwards a debugging logging call
func (e *Entry) Debug(args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Debug(args...)
}

// Debugf forwards a debugging logging call
func (e *Entry) Debugf(format string, args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Debugf(format, args...)
}

// Warn forwards a warning logging call
func (e *Entry) Warn(args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Warn(args...)
}

// Warnf forwards a warning logging call
func (e *Entry) Warnf(format string, args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Warnf(format, args...)
}

// Fatal forwards a fatal logging call
func (e *Entry) Fatal(args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Fatal(args...)
}

return f
// Fatalf forwards a fatal logging call
func (e *Entry) Fatalf(format string, args ...interface{}) {
logrusFields := logrus.Fields(e.fields)
e.logger.logrusLogger.WithContext(e.context).WithTime(e.logger.now()).WithFields(logrusFields).Fatalf(format, args...)
}
8 changes: 4 additions & 4 deletions global_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ func Global() *Logger {
}

// WithError is a convenience wrapper for WithField("err", err)
func WithError(err error) Entry {
func WithError(err error) *Entry {
return globalLogger.WithError(err)
}

// WithField creates log entry using global logger
func WithField(key string, value interface{}) Entry {
func WithField(key string, value interface{}) *Entry {
return globalLogger.WithField(key, value)
}

// WithFields creates log entry using global logger
func WithFields(fields Fields) Entry {
func WithFields(fields Fields) *Entry {
return globalLogger.WithFields(fields)
}

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

Expand Down
69 changes: 69 additions & 0 deletions global_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package logger

import (
"bytes"
"context"
"fmt"
"testing"
)

Expand Down Expand Up @@ -124,3 +126,70 @@ func TestGlobalLoggerConvenienveFunctions(t *testing.T) {
Info("but now I will")
assertLogEntryContains(t, buf, "msg", "but now I will")
}

func TestChainingSetup(t *testing.T) {
buf := &bytes.Buffer{}
oldOutput := globalLogger.output
oldLevel := globalLogger.level
oldNowFunc := globalLogger.now
defer func() {
// bring global logger to original state after tests
ConfigureGlobalLogger(WithOutput(oldOutput), WithLevel(oldLevel), WithNowFunc(oldNowFunc))
}()
ConfigureGlobalLogger(WithOutput(buf), WithLevel(LevelDebug), WithNowFunc(mockNowFunc), WithHookFunc(testHook))

ctx := context.WithValue(context.Background(), myCtxKey{}, "my-custom-ctx-value")
err := fmt.Errorf("some error")

{
// Start with global WithField
WithField("foo", "bar").WithContext(ctx).WithFields(Fields{"baz": "quoo", "number": 42}).WithError(err).Infof("hello %s", "world")
b := buf.Bytes() // get bytes for multiple-reads
buf.Reset() // Prepare for next log message

assertLogEntryContains(t, bytes.NewReader(b), "msg", "hello world")
assertLogEntryContains(t, bytes.NewReader(b), "foo", "bar")
assertLogEntryContains(t, bytes.NewReader(b), "my-custom-log-key", "my-custom-ctx-value")
assertLogEntryContains(t, bytes.NewReader(b), "baz", "quoo")
assertLogEntryContains(t, bytes.NewReader(b), "number", float64(42))
}

{
// Start with global WithFields
WithFields(Fields{"baz": "quoo", "number": 42}).WithField("foo", "bar").WithContext(ctx).WithError(err).Infof("hello %s", "world")
b := buf.Bytes() // get bytes for multiple-reads
buf.Reset() // Prepare for next log message

assertLogEntryContains(t, bytes.NewReader(b), "msg", "hello world")
assertLogEntryContains(t, bytes.NewReader(b), "foo", "bar")
assertLogEntryContains(t, bytes.NewReader(b), "my-custom-log-key", "my-custom-ctx-value")
assertLogEntryContains(t, bytes.NewReader(b), "baz", "quoo")
assertLogEntryContains(t, bytes.NewReader(b), "number", float64(42))
}

{
// Start with global WithError
WithError(err).WithFields(Fields{"baz": "quoo", "number": 42}).WithField("foo", "bar").WithContext(ctx).Infof("hello %s", "world")
b := buf.Bytes() // get bytes for multiple-reads
buf.Reset() // Prepare for next log message

assertLogEntryContains(t, bytes.NewReader(b), "msg", "hello world")
assertLogEntryContains(t, bytes.NewReader(b), "foo", "bar")
assertLogEntryContains(t, bytes.NewReader(b), "my-custom-log-key", "my-custom-ctx-value")
assertLogEntryContains(t, bytes.NewReader(b), "baz", "quoo")
assertLogEntryContains(t, bytes.NewReader(b), "number", float64(42))
}

{
// Start with global WithContext
WithContext(ctx).WithError(err).WithFields(Fields{"baz": "quoo", "number": 42}).WithField("foo", "bar").Infof("hello %s", "world")
b := buf.Bytes() // get bytes for multiple-reads
buf.Reset() // Prepare for next log message

assertLogEntryContains(t, bytes.NewReader(b), "msg", "hello world")
assertLogEntryContains(t, bytes.NewReader(b), "foo", "bar")
assertLogEntryContains(t, bytes.NewReader(b), "my-custom-log-key", "my-custom-ctx-value")
assertLogEntryContains(t, bytes.NewReader(b), "baz", "quoo")
assertLogEntryContains(t, bytes.NewReader(b), "number", float64(42))
}
}
6 changes: 3 additions & 3 deletions hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ type HookEntry struct {
Context context.Context
}

type customHook struct {
type logrusHook struct {
hook Hook
}

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

// Fire implements the logrus.Hook interface.
func (h *customHook) Fire(entry *logrus.Entry) error {
func (h *logrusHook) Fire(entry *logrus.Entry) error {
// Provide all entry-data so the hook can mutate them.
hookEntry := &HookEntry{
Data: Fields(entry.Data),
Expand Down
Loading
Loading