Skip to content
Open
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
23 changes: 18 additions & 5 deletions log/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ func Errorf(ctx context.Context, err error, format string, args ...interface{})

// From returns a copied logger from the context that you can use to access logger API.
func From(ctx context.Context) Logger {
f := getFields(ctx)
return f.configureLogger(ctx, f.getCopiedLogger().(fieldSetter))
return from(ctx, getFields(ctx))
}

// Info logs from context at the debug level.
Expand All @@ -45,6 +44,21 @@ func Infof(ctx context.Context, format string, args ...interface{}) {
Fields{}.Infof(ctx, format, args...)
}

// RegisterListener registers callbacks that are called after a log.
func RegisterListener(ctx context.Context, callback func(context.Context, Fields)) context.Context{
cbList, isList := ctx.Value(listenerKey{}).([]func(context.Context, Fields))
Comment thread
anzdaddy marked this conversation as resolved.
if !isList {
cbList = []func(context.Context, Fields){callback}
} else {
cbList = append(cbList, callback)
}
return context.WithValue(
context.WithValue(ctx, canonicalFieldsKey{}, frozen.NewMapBuilder(0)),
listenerKey{},
cbList,
)
}

// Suppress will ensure that suppressed keys are not logged.
func Suppress(keys ...string) Fields {
return Fields{}.Suppress(keys...)
Expand Down Expand Up @@ -162,9 +176,8 @@ func (f Fields) WithLogger(logger Logger) Fields {

// String returns a string that represent the current fields
func (f Fields) String(ctx context.Context) string {
fields := &fieldsCollector{}
f.configureLogger(ctx, fields)
return fields.fields.String()
m, _ := f.getResolvedFields(ctx)
return m.String()
}

// MergedString returns a string that represents the current fields merged by fields in context
Expand Down
26 changes: 26 additions & 0 deletions log/canonical.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package log

import (
"context"

"github.com/arr-ai/frozen"
)

// OnLog is a callback that is called when fields and configurations are added to the logger.
func OnLog(ctx context.Context, fields Fields) {
if fields := ctx.Value(canonicalFieldsKey{}); fields == nil {
Info(ctx, "Canonical fields have not been set up properly, fields will not be collected")
return
}
addCanonicalFields(getCanonicalFields(ctx), fields)
}

// CanonicalLog logs all the collected fields from previous logs in non-verbose mode.
func CanonicalLog(ctx context.Context) {
from(ctx, Fields{getCanonicalFields(ctx).Finish()}).Info()
}

// WithCanonicalLog creates a canonical fields collector.
func WithCanonicalLog(ctx context.Context) context.Context {
return context.WithValue(ctx, canonicalFieldsKey{}, frozen.NewMapBuilder(0))
}
62 changes: 62 additions & 0 deletions log/canonical_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package log

import (
"context"
"testing"

"github.com/alecthomas/assert"
"github.com/arr-ai/frozen"
)

func TestCanonicalLog(t *testing.T) {
t.Parallel()

ctx := WithCanonicalLog(context.Background())
logger := newMockLogger()

mb := getCanonicalFields(ctx)

addValues(mb, logger)
setLogMockAssertion(logger, mb.Finish().Without(frozen.NewSet(loggerKey{})))
logger.On("Info")

// add the same values again because Finish would remove the values from the map builder.
addValues(mb, logger)
CanonicalLog(ctx)

logger.AssertExpectations(t)
}

func TestOnLogWithoutCanonicalFieldsSetup(t *testing.T) {
t.Parallel()

logger := newMockLogger()
logger.On("Info", "Canonical fields have not been set up properly, fields will not be collected")
setLogMockAssertion(logger, frozen.NewMap())
OnLog(WithLogger(logger).Onto(context.Background()), Fields{frozen.NewMap()})
logger.AssertExpectations(t)
}

func TestOnLog(t *testing.T) {
t.Parallel()

ctx := WithCanonicalLog(context.Background())
mb := getCanonicalFields(ctx)
addValues(mb, newMockLogger())

testField := frozen.NewMap().With("additional", "value").With("1", 1).With("byte", 'l')
OnLog(ctx, Fields{testField})

m := mb.Finish()
for i := testField.Range(); i.Next(); {
val, exists := m.Get(i.Key())
assert.True(t, exists && val == i.Value())
}
}

func addValues(mb *frozen.MapBuilder, logger Logger) {
mb.Put("test", 1)
mb.Put("test2", 'k')
mb.Put("test3", "string value")
mb.Put(loggerKey{}, logger)
}
70 changes: 63 additions & 7 deletions log/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
const errMsgKey = "error_message"

type fieldsContextKey struct{}
type canonicalFieldsKey struct{}
type listenerKey struct {}
type loggerKey struct{}
type suppress struct{}
type ctxRef struct{ ctxKey interface{} }
Expand All @@ -30,10 +32,26 @@ func (f Fields) getCopiedLogger() Logger {
return logger.(copyable).Copy()
}

func (f Fields) configureLogger(ctx context.Context, logger fieldSetter) Logger {
func configureLogger(logger fieldSetter, fields frozen.Map, configs []Config) Logger {
return applyConfiguration(logger.(Logger), configs).(fieldSetter).PutFields(fields)
}

func applyConfiguration(logger Logger, configs []Config) Logger {
for _, c := range configs {
err := c.Apply(logger.(Logger))
if err != nil {
//TODO: should decide whether it should panic or not
panic(err)
}
}
return logger
}

func (f Fields) getResolvedFields(ctx context.Context) (frozen.Map, []Config) {
fields := f.m
var toSuppress frozen.SetBuilder
toSuppress.Add(loggerKey{})
configs := make([]Config, 0)
for i := fields.Range(); i.Next(); {
switch k := i.Value().(type) {
case ctxRef:
Expand All @@ -52,20 +70,58 @@ func (f Fields) configureLogger(ctx context.Context, logger fieldSetter) Logger
toSuppress.Add(i.Key())
case Config:
toSuppress.Add(i.Key())
err := k.Apply(logger.(Logger))
if err != nil {
//TODO: should decide whether it should panic or not
panic(err)
}
configs = append(configs, k)
}
}
return logger.PutFields(fields.Without(toSuppress.Finish()))
return fields.Without(toSuppress.Finish()), configs
}

func (f Fields) with(key, val interface{}) Fields {
return Fields{f.m.With(key, val)}
}

func getCanonicalFields(ctx context.Context) *frozen.MapBuilder {
mb, exists := ctx.Value(canonicalFieldsKey{}).(*frozen.MapBuilder)
if !exists {
return frozen.NewMapBuilder(0)
}
return mb
}

func addCanonicalFields(mb *frozen.MapBuilder, fields Fields) {
for i := fields.m.Range(); i.Next(); {
switch k := i.Key().(type) {
case Config, Logger:
mb.Put(k, i.Value())
default:
if f, exists := mb.Get(k); exists {
if val, isList := f.([]interface{}); isList {
mb.Put(k, append(val, i.Value()))
} else {
mb.Put(k, []interface{}{f, i.Value()})
}
} else {
mb.Put(k, i.Value())
}
}
}
}

func doCallbacks(ctx context.Context, fields Fields) {
callbacks := ctx.Value(listenerKey{})
if callbacks != nil {
for _, c := range callbacks.([]func(context.Context, Fields)) {
c(ctx, fields)
}
}
}

func from(ctx context.Context, f Fields) Logger {
fields, configs := f.getResolvedFields(ctx)
doCallbacks(ctx, Fields{fields})
return configureLogger(f.getCopiedLogger().(fieldSetter), fields, configs)
}

func getFields(ctx context.Context) Fields {
fields, exists := ctx.Value(fieldsContextKey{}).(frozen.Map)
if !exists {
Expand Down
26 changes: 22 additions & 4 deletions log/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/arr-ai/frozen"
"github.com/stretchr/testify/assert"
)

type key1 struct{}
Expand Down Expand Up @@ -75,7 +76,7 @@ func TestConfigureLogger(t *testing.T) {
for i := c.contextFields.Range(); i.Next(); {
ctx = context.WithValue(ctx, i.Key(), i.Value())
}
Fields{c.unresolveds}.configureLogger(ctx, Logger(logger).(fieldSetter))
logger = configureLogger(Logger(logger).(fieldSetter), c.expected, []Config{}).(*mockLogger)
logger.AssertExpectations(t)
})
}
Expand All @@ -86,7 +87,7 @@ func TestConfigureLoggerWithConfigs(t *testing.T) {

//TODO: add more configs
testCase := getUnresolvedFieldsCases()[0]
unresolveds := Fields{testCase.unresolveds}.WithConfigs(NewJSONFormat())
unresolved := Fields{testCase.unresolveds}.WithConfigs(NewJSONFormat())
expected := testCase.expected

logger := newMockLogger()
Expand All @@ -97,7 +98,24 @@ func TestConfigureLoggerWithConfigs(t *testing.T) {
for i := testCase.contextFields.Range(); i.Next(); {
ctx = context.WithValue(ctx, i.Key(), i.Value())
}

unresolveds.configureLogger(ctx, Logger(logger).(fieldSetter))
resolved, configs := unresolved.getResolvedFields(ctx)
configureLogger(Logger(logger).(fieldSetter), resolved, configs)
logger.AssertExpectations(t)
}

func TestRegisterListener(t *testing.T) {
t.Parallel()

mockListener1 := OnLog

ctx := RegisterListener(context.Background(), mockListener1)
cbs, isCallbackList := ctx.Value(listenerKey{}).([]func(context.Context, Fields))
assert.True(t, isCallbackList)
assert.Equal(t, 1, len(cbs))

mockListener2 := func(context.Context, Fields){}
ctx = RegisterListener(ctx, mockListener2)
cbs, isCallbackList = ctx.Value(listenerKey{}).([]func(context.Context, Fields))
assert.True(t, isCallbackList)
assert.Equal(t, 2, len(cbs))
}