diff --git a/event.go b/event.go index 638d485..96c73b5 100644 --- a/event.go +++ b/event.go @@ -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 @@ -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 diff --git a/fields/equality.go b/fields/equality.go index 9b330fb..cb9255d 100644 --- a/fields/equality.go +++ b/fields/equality.go @@ -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) @@ -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. diff --git a/fields/equality_value.go b/fields/equality_value.go index 1144a6a..3063c08 100644 --- a/fields/equality_value.go +++ b/fields/equality_value.go @@ -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. diff --git a/fields/value.go b/fields/exclude.go similarity index 100% rename from fields/value.go rename to fields/exclude.go diff --git a/fields/fields.go b/fields/fields.go index adbf3e4..c079029 100644 --- a/fields/fields.go +++ b/fields/fields.go @@ -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 @@ -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 diff --git a/fields/filtered.go b/fields/filtered.go new file mode 100644 index 0000000..4dae62f --- /dev/null +++ b/fields/filtered.go @@ -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 +} diff --git a/fields/filtered_test.go b/fields/filtered_test.go new file mode 100644 index 0000000..6df785e --- /dev/null +++ b/fields/filtered_test.go @@ -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 , + fmt.Println(filteredValue.Filter(filterContextWithLeveInfo)) + + // Will be , + 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 +} diff --git a/fields/key_spec.go b/fields/key_spec.go index 7cb10d1..55acd96 100644 --- a/fields/key_spec.go +++ b/fields/key_spec.go @@ -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. diff --git a/fields/lazy.go b/fields/lazy.go index d5718fd..6d38b9f 100644 --- a/fields/lazy.go +++ b/fields/lazy.go @@ -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{} } @@ -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} diff --git a/internal/demo/main.go b/internal/demo/main.go index 3d84aa2..b4d939c 100644 --- a/internal/demo/main.go +++ b/internal/demo/main.go @@ -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") } diff --git a/logger_core_fallback.go b/logger_core_fallback.go index 02a6a95..6e73a04 100644 --- a/logger_core_fallback.go +++ b/logger_core_fallback.go @@ -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 { diff --git a/logger_core_fallback_test.go b/logger_core_fallback_test.go index b51ab60..bc221be 100644 --- a/logger_core_fallback_test.go +++ b/logger_core_fallback_test.go @@ -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") diff --git a/native/formatter/json.go b/native/formatter/json.go index d5a17e9..6f754b6 100644 --- a/native/formatter/json.go +++ b/native/formatter/json.go @@ -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 { diff --git a/native/formatter/json_test.go b/native/formatter/json_test.go index 2bd1579..64c4c83 100644 --- a/native/formatter/json_test.go +++ b/native/formatter/json_test.go @@ -284,6 +284,27 @@ func Test_Json_encodeValuesChecked(t *testing.T) { "bar": aLazy("barAsLazy"), }), expected: `,"bar":"barAsLazy","foo":"foo"`, + }, { + name: "withStringAndFilteredRespected", + given: givenLogger.NewEvent(level.Info, map[string]interface{}{ + "foo": "foo", + "bar": fields.RequireMaximalLevel(level.Info, "barAsFiltered"), + }), + expected: `,"bar":"barAsFiltered","foo":"foo"`, + }, { + name: "withStringAndFilteredIgnored", + given: givenLogger.NewEvent(level.Info, map[string]interface{}{ + "foo": "foo", + "bar": fields.RequireMaximalLevel(level.Debug, "barAsFiltered"), + }), + expected: `,"foo":"foo"`, + }, { + name: "withoutExcluded", + given: givenLogger.NewEvent(level.Info, map[string]interface{}{ + "foo": "foo", + "bar": fields.Exclude, + }), + expected: `,"foo":"foo"`, }, { name: "withStringAndSomeLogger", given: givenLogger.NewEvent(0, map[string]interface{}{ diff --git a/native/formatter/text.go b/native/formatter/text.go index 13c7788..0760e24 100644 --- a/native/formatter/text.go +++ b/native/formatter/text.go @@ -48,7 +48,7 @@ var ( ) // Text is an implementation of Formatter which formats given log entries in a -// human readable format. Additionally it can also colorize the formatted +// human-readable format. Additionally, it can also colorize the formatted // output. type Text struct { // ColorMode defines when the output should be colorized. If not configured @@ -84,19 +84,19 @@ type Text struct { // MultiLineMessageAfterFields will force multiline messages // (if set to true) to be printed behind the fields; instead (if default) - // in front of them. If not set set DefaultMultiLineMessageAfterFields will + // in front of them. If not set DefaultMultiLineMessageAfterFields will // be used. MultiLineMessageAfterFields *bool // AllowMultiLineMessage will allow (if set to true) multiline messages to // be printed as multiline to the output, too. If set to false linebreaks - // will be replaced with ⏎. If not set set DefaultAllowMultiLineMessage will + // will be replaced with ⏎. If not set DefaultAllowMultiLineMessage will // be used. AllowMultiLineMessage *bool // PrintRootLogger will (if set to true) also print the field logger for the // root logger. If set to false the logger field will be only printed for - // every logger but not for the root one. If not set set + // every logger but not for the root one. If not set // DefaultPrintRootLogger will be used. PrintRootLogger *bool @@ -205,7 +205,7 @@ func (instance *Text) printLevelChecked(l level.Level, using log.Provider, h hin func (instance *Text) printFieldsChecked(using log.Provider, h hints.Hints, event log.Event, to encoding.TextEncoder, atLeastOneFieldPrinted *bool) execution.Execution { return func() error { return fields.SortedForEach(event, instance.getFieldSorter(), func(k string, v interface{}) error { - printed, err := instance.printField(event.GetLevel(), k, v, h, using, to) + printed, err := instance.printField(event, k, v, h, using, to) if printed { *atLeastOneFieldPrinted = printed } @@ -214,8 +214,14 @@ func (instance *Text) printFieldsChecked(using log.Provider, h hints.Hints, even } } -func (instance *Text) printField(l level.Level, k string, v interface{}, h hints.Hints, using log.Provider, to encoding.TextEncoder) (bool, error) { - if vl, ok := v.(fields.Lazy); ok { +func (instance *Text) printField(ctx fields.FilterContext, k string, v interface{}, h hints.Hints, using log.Provider, to encoding.TextEncoder) (bool, error) { + if vl, ok := v.(fields.Filtered); ok { + fv, shouldBeRespected := vl.Filter(ctx) + if !shouldBeRespected { + return false, nil + } + v = fv + } else if vl, ok := v.(fields.Lazy); ok { v = vl.Get() } if v == fields.Exclude { @@ -234,7 +240,7 @@ func (instance *Text) printField(l level.Level, k string, v interface{}, h hints if err != nil { return false, err } - return true, to.WriteString(` ` + instance.colorize(l, k, h) + `=` + string(b)) + return true, to.WriteString(` ` + instance.colorize(ctx.GetLevel(), k, h) + `=` + string(b)) } func (instance *Text) printMessageAsSingleLineIfRequiredChecked(message *string, predicate bool, to encoding.TextEncoder) execution.Execution { diff --git a/native/formatter/text_test.go b/native/formatter/text_test.go index c644035..d146282 100644 --- a/native/formatter/text_test.go +++ b/native/formatter/text_test.go @@ -227,7 +227,7 @@ func Test_Text_printTimestampChecked(t *testing.T) { }, { givenTimestamp: mustParseTime("2021-01-02T13:14:15.1234"), withColor: true, - expected: `13:14:15.123`, + expected: `13:14:15.123`, }, { givenTimestamp: time.Time{}, expected: ``, @@ -358,8 +358,10 @@ func Test_Text_printFieldsChecked(t *testing.T) { "foo5b": fields.LazyFunc(func() interface{} { return "" }), + "foo6a": fields.RequireMaximalLevel(level.Info, "bar6a"), + "foo6b": fields.RequireMaximalLevel(level.Debug, "bar6b"), }), - expected: " 3(foo1)=bar1 3(foo2)=2 3(foo4a)= 3(foo4b)= 3(foo5a)= 3(foo5b)=", + expected: " 3(foo1)=bar1 3(foo2)=2 3(foo4a)= 3(foo4b)= 3(foo5a)= 3(foo5b)= 3(foo6a)=bar6a", }} for i, c := range cases { @@ -382,6 +384,18 @@ func Test_Text_printFieldsChecked(t *testing.T) { } +type simpleFilterContext struct { + level.Level +} + +func (instance simpleFilterContext) GetLevel() level.Level { + return instance.Level +} + +func (instance simpleFilterContext) Get(string) (interface{}, bool) { + return nil, false +} + func Test_Text_printField(t *testing.T) { instance := NewText(func(text *Text) { text.ColorMode = color.ModeNever @@ -454,7 +468,7 @@ func Test_Text_printField(t *testing.T) { instance.PrintRootLogger = &c.givenShouldPrintRootLogger givenEncoder := encoding.NewBufferedTextEncoder() actualPrinted, actualErr := instance.printField( - c.givenLevel, + simpleFilterContext{c.givenLevel}, c.givenKey, c.givenValue, c.givenHints, @@ -479,7 +493,7 @@ func Test_Text_printField_failsWithValueFormatter(t *testing.T) { provider := recording.NewProvider() givenEncoder := encoding.NewBufferedTextEncoder() - _, actualErr := instance.printField(level.Info, "foo", "bar", nil, provider, givenEncoder) + _, actualErr := instance.printField(simpleFilterContext{level.Info}, "foo", "bar", nil, provider, givenEncoder) assert.ToBeSame(t, expectedErr, actualErr) assert.ToBeEqual(t, "", givenEncoder.String())