Skip to content

Commit

Permalink
Added fields.RequireMaximalLevel based on fields.Filtered.
Browse files Browse the repository at this point in the history
  • Loading branch information
blaubaer committed Jan 3, 2023
1 parent 7fefa5a commit aceca76
Show file tree
Hide file tree
Showing 16 changed files with 277 additions and 40 deletions.
21 changes: 11 additions & 10 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,24 @@ import (

// Event represents an event which can be logged using a Logger (or CoreLogger).
//
// Contents
// # Contents
//
// Events containing always present content provided by GetLevel().
//
// They are providing additionally dynamic content (messages, errors, ...)
// which are accessible via ForEach() and Get(). None of this fields are
// required to exists by contract. The keys of these fields are defined by
// which are accessible via ForEach() and Get(). None of these fields are
// required to exist by contract. The keys of these fields are defined by
// Provider.GetFieldKeysSpec(). For example using fields.KeysSpec.GetMessage()
// it might be possible to get the key of the message.
//
// The keys are always of type string and should be only printable characters
// which can be printed in any context. Recommended are everything that matches:
// ^[a-zA-Z0-9._-]+$
//
// ^[a-zA-Z0-9._-]+$
//
// The values could be everything including nils.
//
// Immutability
// # Immutability
//
// Fields are defined as immutable. Calling the methods With, Withf, WithAll
// and Without always results in a new instance of Event that could be either
Expand All @@ -37,28 +38,28 @@ type Event interface {
ForEach(consumer func(key string, value interface{}) error) error

// Get will return for the given key the corresponding value if exists.
// Otherwise it will return nil.
// Otherwise, it will return nil.
Get(key string) (value interface{}, exists bool)

// Len returns the len of all key value pairs contained in this event which
// can be received by using ForEach() or Get().
Len() int

// With returns an variant of this Event with the given key
// With returns a variant of this Event with the given key
// value pair contained inside. If the given key already exists in the
// current instance this means it will be overwritten.
With(key string, value interface{}) Event

// Withf is similar to With but it adds classic fmt.Printf functions to it.
// Withf is similar to With, but it adds classic fmt.Printf functions to it.
// It is defined that the format itself will not be executed before the
// consumption of the value. (See fields.Fields.ForEach() and
// fields.Fields.Get())
Withf(key string, format string, args ...interface{}) Event

// WithError is similar to With but it adds an error as field.
// WithError is similar to With, but it adds an error as field.
WithError(error) Event

// WithAll is similar to With but it can consume more than one field at
// WithAll is similar to With, but it can consume more than one field at
// once. Be aware: There is neither a guarantee that this instance will be
// copied or not.
WithAll(map[string]interface{}) Event
Expand Down
4 changes: 2 additions & 2 deletions fields/equality.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func AreEqual(left, right Fields) (bool, error) {
return false, nil
}

// DefaultEquality is the default instance of a Equality. The initial
// DefaultEquality is the default instance of an Equality. The initial
// initialization of this global variable should be able to deal with the
// majority of the cases. There is also a shortcut function:
// AreEqual(left,right)
Expand Down Expand Up @@ -91,7 +91,7 @@ func (instance *EqualityImpl) AreFieldsEqual(left, right Fields) (bool, error) {
}

// NewEqualityFacade creates a re-implementation of Equality which uses the
// given provider to retrieve the actual instance of Equality in the moment when
// given provider to retrieve the actual instance of Equality at the moment when
// it is used. This is useful especially in cases where you want to deal with
// concurrency while creation of objects that need to hold a reference to an
// Equality.
Expand Down
2 changes: 1 addition & 1 deletion fields/equality_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (instance ValueEqualityFunc) AreValuesEqual(key string, left, right interfa
}

// NewValueEqualityFacade creates a re-implementation of ValueEquality which
// uses the given provider to retrieve the actual instance of ValueEquality in
// uses the given provider to retrieve the actual instance of ValueEquality at
// the moment when it is used. This is useful especially in cases where you want
// to deal with concurrency while creation of objects that need to hold a
// reference to an ValueEquality.
Expand Down
File renamed without changes.
13 changes: 7 additions & 6 deletions fields/fields.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Fields represents a collection of key value pairs.
//
// Key and Values
// # Key and Values
//
// The keys are always of type string and should be only printable characters
// which can be printed in any context. Recommended are everything that matches:
// ^[a-zA-Z0-9._-]+$
//
// ^[a-zA-Z0-9._-]+$
//
// The values could be everything including nils.
//
// Immutability
// # Immutability
//
// Fields are defined as immutable. Calling the methods With, Withf, Without and
// WithAll always results in a new instance of Fields that could be either
Expand All @@ -28,17 +29,17 @@ type Fields interface {
// Len returns the len of this Fields instance.
Len() int

// With returns an variant of this Fields with the given key
// With returns a variant of this Fields with the given key
// value pair contained inside. If the given key already exists in the
// current instance this means it will be overwritten.
With(key string, value interface{}) Fields

// Withf is similar to With but it adds classic fmt.Printf functions to it.
// Withf is similar to With, but it adds classic fmt.Printf functions to it.
// It is defined that the format itself will not be executed before the
// consumption of the value. (See ForEach() and Get())
Withf(key string, format string, args ...interface{}) Fields

// WithAll is similar to With but it can consume more than one field at
// WithAll is similar to With, but it can consume more than one field at
// once. Be aware: There is neither a guarantee that this instance will be
// copied or not.
WithAll(map[string]interface{}) Fields
Expand Down
58 changes: 58 additions & 0 deletions fields/filtered.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package fields

import "github.com/echocat/slf4g/level"

// Filtered is a value which will be executed on usage to retrieve the actual
// value or exclude it.
//
// This is useful in context where fields should be only respected based on a
// specific log level, another field has a specific value, ...
type Filtered interface {
// Filter is the method which will be called at the moment where the value
// should be consumed.
//
// Only if shouldBeRespected is true it will be respected by the consumers.
Filter(FilterContext) (value interface{}, shouldBeRespected bool)

// Get will return the original value (unfiltered).
Get() interface{}
}

// FilterContext provides information about the context where a field exists
// within.
type FilterContext interface {
// GetLevel provides the current level.Level of this context.
GetLevel() level.Level

// Get provides access to other fields within this context.
Get(key string) (value interface{}, exists bool)
}

// RequireMaximalLevel represents a filtered value which will only be consumed if the
// level.Level of the current context (for example logging events) is not bigger than
// the requested maximalLevel.
func RequireMaximalLevel(maximalLevel level.Level, value interface{}) Filtered {
return RequireMaximalLevelLazy(maximalLevel, LazyFunc(func() interface{} {
return value
}))
}

// RequireMaximalLevelLazy represents a filtered Lazy value which will only be consumed
// if the level.Level of the current context (for example logging events) is not bigger
// than requested maximalLevel.
func RequireMaximalLevelLazy(minimalLevel level.Level, value Lazy) Filtered {
return requireMaximalLevel{value, minimalLevel}
}

type requireMaximalLevel struct {
Lazy
level level.Level
}

func (instance requireMaximalLevel) Filter(ctx FilterContext) (value interface{}, shouldBeRespected bool) {
if ctx.GetLevel() > instance.level {
return nil, false
}

return instance.Get(), true
}
93 changes: 93 additions & 0 deletions fields/filtered_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package fields

import (
"fmt"
"github.com/echocat/slf4g/level"
"testing"

"github.com/echocat/slf4g/internal/test/assert"
)

var veryComplexValue = struct{}{}
var filterContextWithLeveInfo = filterContext{
level: level.Info,
}
var filterContextWithLeveDebug = filterContext{
level: level.Debug,
}

func ExampleRequireMaximalLevel() {
filteredValue := RequireMaximalLevel(level.Debug, veryComplexValue)

// Will be <nil>, <false>
fmt.Println(filteredValue.Filter(filterContextWithLeveInfo))

// Will be <veryComplexValue>, <true>
fmt.Println(filteredValue.Filter(filterContextWithLeveDebug))
}

func Test_RequireMaximalLevelLazy_Get(t *testing.T) {
expected := struct{ foo string }{foo: "bar"}
givenLazy := LazyFunc(func() interface{} { return expected })

actualInstance := RequireMaximalLevelLazy(level.Info, givenLazy)
actual := actualInstance.Get()

assert.ToBeEqual(t, expected, actual)
}

func Test_RequireMaximalLevelLazy_Filter_respected(t *testing.T) {
expected := struct{ foo string }{foo: "bar"}
givenLazy := LazyFunc(func() interface{} { return expected })

actualInstance := RequireMaximalLevelLazy(level.Debug, givenLazy)
actual, actualRespected := actualInstance.Filter(filterContextWithLeveDebug)

assert.ToBeEqual(t, expected, actual)
assert.ToBeEqual(t, true, actualRespected)
}

func Test_RequireMaximalLevelLazy_Filter_ignored(t *testing.T) {
givenLazy := LazyFunc(func() interface{} { return struct{ foo string }{foo: "bar"} })

actualInstance := RequireMaximalLevelLazy(level.Debug, givenLazy)
actual, actualRespected := actualInstance.Filter(filterContextWithLeveInfo)

assert.ToBeNil(t, actual)
assert.ToBeEqual(t, false, actualRespected)
}

func Test_RequireMaximalLevel_Filter_respected(t *testing.T) {
expected := struct{ foo string }{foo: "bar"}

actualInstance := RequireMaximalLevel(level.Debug, expected)
actual, actualRespected := actualInstance.Filter(filterContextWithLeveDebug)

assert.ToBeEqual(t, expected, actual)
assert.ToBeEqual(t, true, actualRespected)
}

func Test_RequireMaximalLevel_Filter_ignored(t *testing.T) {
actualInstance := RequireMaximalLevel(level.Debug, struct{ foo string }{foo: "bar"})
actual, actualRespected := actualInstance.Filter(filterContextWithLeveInfo)

assert.ToBeNil(t, actual)
assert.ToBeEqual(t, false, actualRespected)
}

type filterContext struct {
level level.Level
fields map[string]interface{}
}

func (instance filterContext) GetLevel() level.Level {
return instance.level
}

func (instance filterContext) Get(key string) (value interface{}, exists bool) {
if instance.fields == nil {
return nil, false
}
v, ok := instance.fields[key]
return v, ok
}
2 changes: 1 addition & 1 deletion fields/key_spec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package fields

// KeysSpec defines the keys for common usages inside of a Fields instance.
// KeysSpec defines the keys for common usages inside a Fields instance.
type KeysSpec interface {
// GetTimestamp returns the key where the timestamp is stored inside, if
// available.
Expand Down
6 changes: 3 additions & 3 deletions fields/lazy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import "fmt"
// Lazy is a value which CAN be initialized on usage.
//
// This is very useful in the context of Fields where sometimes the evaluating
// of values could be cost intensive but maybe you either might log stuff on a
// of values could be cost intensive, but maybe you either might log stuff on a
// level which might not be always enabled or the operation might be happening
// on an extra routine/thread.
type Lazy interface {
// Get is the method which will be called in the moment where the value
// Get is the method which will be called at the moment where the value
// should be consumed.
Get() interface{}
}
Expand All @@ -25,7 +25,7 @@ func (instance lazyFunc) Get() interface{} {
return instance()
}

// LazyFormat returns a value which will be execute the fmt.Sprintf action in
// LazyFormat returns a value which will be executed the fmt.Sprintf action at
// the moment when it will be consumed or in other words: Lazy.Get() is called.
func LazyFormat(format string, args ...interface{}) Lazy {
return &lazyFormat{format, args}
Expand Down
19 changes: 16 additions & 3 deletions internal/demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@ package main

import (
log "github.com/echocat/slf4g"
"github.com/echocat/slf4g/fields"
"github.com/echocat/slf4g/level"
)

func main() {
log.With("foo", "bar").Debug("hello, debug")
log.With("foo", "bar").Info("hello, info")
log.With("foo", 1).Warn("hello, warn")
log.With("foo", "bar").
Debug("hello, debug")

log.With("foo", "bar").
Info("hello, info")

log.With("foo", "bar").
With("filtered1", fields.RequireMaximalLevel(level.Info, "visibleUntilLevelInfo")).
With("filtered2", fields.RequireMaximalLevel(level.Debug, "visibleUntilLevelDebug")).
Info()

log.With("foo", 1).
Warn("hello, warn")

log.Error("hello, error")
}
8 changes: 7 additions & 1 deletion logger_core_fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ func (instance *fallbackCoreLogger) format(event Event, skipFrames uint16) []byt
loggerKey := instance.GetFieldKeysSpec().GetLogger()
timestampKey := instance.GetFieldKeysSpec().GetTimestamp()
if err := fields.SortedForEach(event, nil, func(k string, vp interface{}) error {
if vl, ok := vp.(fields.Lazy); ok {
if vl, ok := vp.(fields.Filtered); ok {
fv, shouldBeRespected := vl.Filter(event)
if !shouldBeRespected {
return nil
}
vp = fv
} else if vl, ok := vp.(fields.Lazy); ok {
vp = vl.Get()
}
if vp == fields.Exclude {
Expand Down
18 changes: 18 additions & 0 deletions logger_core_fallback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ func Test_fallbackCoreLogger_Log_withLazyValue(t *testing.T) {
assert.ToBeMatching(t, `^I.+logger_core_fallback_test.go:\d+] foo=666 logger="foo"`, buf.String())
}

func Test_fallbackCoreLogger_Log_withFilteredValue_respected(t *testing.T) {
instance, buf := newFallbackCoreLogger("foo")

instance.Log(instance.NewEvent(level.Info, nil).
With("foo", fields.RequireMaximalLevel(level.Info, 666)), 0)

assert.ToBeMatching(t, `^I.+logger_core_fallback_test.go:\d+] foo=666 logger="foo"`, buf.String())
}

func Test_fallbackCoreLogger_Log_withFilteredValue_ignored(t *testing.T) {
instance, buf := newFallbackCoreLogger("foo")

instance.Log(instance.NewEvent(level.Info, nil).
With("foo", fields.RequireMaximalLevel(level.Debug, 666)), 0)

assert.ToBeMatching(t, `^I.+logger_core_fallback_test.go:\d+] logger="foo"`, buf.String())
}

func Test_fallbackCoreLogger_Log_brokenCallDepth(t *testing.T) {
instance, buf := newFallbackCoreLogger("foo")

Expand Down
8 changes: 7 additions & 1 deletion native/formatter/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,13 @@ func (instance *Json) encodeValuesChecked(of log.Event, using log.Provider, to e
printRootLogger := instance.getPrintRootLogger()
loggerKey := using.GetFieldKeysSpec().GetLogger()
consumer := func(k string, v interface{}) error {
if vl, ok := v.(fields.Lazy); ok {
if vl, ok := v.(fields.Filtered); ok {
fv, shouldBeRespected := vl.Filter(of)
if !shouldBeRespected {
return nil
}
v = fv
} else if vl, ok := v.(fields.Lazy); ok {
v = vl.Get()
}
if v == fields.Exclude {
Expand Down
Loading

0 comments on commit aceca76

Please sign in to comment.