feat(logging)_: introduce custom zap.Core enabling runtime changes

Geth logger allows overriding the log level, format and writer at
runtime. To make it interchangeable with zap.Logger, a custom zap.Core
has been introduced to enable these features as well.

closes: #6023
This commit is contained in:
Patryk Osmaczko 2024-10-31 20:01:45 +01:00
parent 11cf42bedd
commit f596a4dabf
2 changed files with 179 additions and 0 deletions

118
logutils/core.go Normal file
View File

@ -0,0 +1,118 @@
package logutils
import (
"sync/atomic"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// encoderWrapper holds any zapcore.Encoder and ensures a consistent type for atomic.Value
type encoderWrapper struct {
zapcore.Encoder
}
// writeSyncerWrapper holds any zapcore.WriteSyncer and ensures a consistent type for atomic.Value
type writeSyncerWrapper struct {
zapcore.WriteSyncer
}
// Core wraps a zapcore.Core that can update its syncer and encoder at runtime
type Core struct {
encoder atomic.Value // encoderWrapper
syncer *atomic.Value // writeSyncerWrapper
level zap.AtomicLevel
next *Core
nextFields []zapcore.Field
}
var (
_ zapcore.Core = (*Core)(nil)
)
func NewCore(encoder zapcore.Encoder, syncer zapcore.WriteSyncer, atomicLevel zap.AtomicLevel) *Core {
core := &Core{
syncer: &atomic.Value{},
level: atomicLevel,
}
core.encoder.Store(encoderWrapper{Encoder: encoder})
core.syncer.Store(writeSyncerWrapper{WriteSyncer: syncer})
return core
}
func (core *Core) getEncoder() zapcore.Encoder {
return core.encoder.Load().(zapcore.Encoder)
}
func (core *Core) getSyncer() zapcore.WriteSyncer {
return core.syncer.Load().(zapcore.WriteSyncer)
}
func (core *Core) Enabled(lvl zapcore.Level) bool {
return core.level.Enabled(lvl)
}
func (core *Core) With(fields []zapcore.Field) zapcore.Core {
clonedEncoder := encoderWrapper{Encoder: core.getEncoder().Clone()}
for i := range fields {
fields[i].AddTo(clonedEncoder)
}
clone := *core
clone.encoder.Store(clonedEncoder)
core.next = &clone
core.nextFields = fields
return &clone
}
func (core *Core) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if core.Enabled(ent.Level) {
return ce.AddCore(ent, core)
}
return ce
}
func (core *Core) Write(ent zapcore.Entry, fields []zapcore.Field) error {
buf, err := core.getEncoder().EncodeEntry(ent, fields)
if err != nil {
return err
}
_, err = core.getSyncer().Write(buf.Bytes())
buf.Free()
if err != nil {
return err
}
if ent.Level > zapcore.ErrorLevel {
// Since we may be crashing the program, sync the output.
_ = core.Sync()
}
return err
}
func (core *Core) Sync() error {
return core.getSyncer().Sync()
}
func (core *Core) UpdateSyncer(newSyncer zapcore.WriteSyncer) {
core.syncer.Store(writeSyncerWrapper{WriteSyncer: newSyncer})
}
func (core *Core) UpdateEncoder(newEncoder zapcore.Encoder) {
core.encoder.Store(encoderWrapper{Encoder: newEncoder})
// Update next Cores with newEncoder
current := core
for current.next != nil {
clonedEncoder := encoderWrapper{Encoder: core.getEncoder().Clone()}
for i := range core.nextFields {
current.nextFields[i].AddTo(clonedEncoder)
}
current.next.encoder.Store(clonedEncoder)
current = current.next
}
}

61
logutils/core_test.go Normal file
View File

@ -0,0 +1,61 @@
package logutils
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func TestCore(t *testing.T) {
level := zap.NewAtomicLevelAt(zap.DebugLevel)
buffer1 := bytes.NewBuffer(nil)
buffer2 := bytes.NewBuffer(nil)
core := NewCore(
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
zapcore.AddSync(buffer1),
level,
)
parent := zap.New(core)
child := parent.Named("child")
childWithContext := child.With(zap.String("key1", "value1"))
childWithMoreContext := childWithContext.With(zap.String("key2", "value2"))
grandChild := childWithMoreContext.Named("grandChild")
parent.Debug("Status")
child.Debug("Super")
childWithContext.Debug("App")
childWithMoreContext.Debug("The")
grandChild.Debug("Best")
core.UpdateSyncer(zapcore.AddSync(buffer2))
core.UpdateEncoder(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()))
parent.Debug("Status")
child.Debug("Super")
childWithContext.Debug("App")
childWithMoreContext.Debug("The")
grandChild.Debug("Best")
fmt.Println(buffer1.String())
fmt.Println(buffer2.String())
// Ensure that the first buffer has the console encoder output
buffer1Lines := strings.Split(buffer1.String(), "\n")
require.Len(t, buffer1Lines, 5+1)
require.Regexp(t, `\s+child\s+`, buffer1Lines[1])
require.Regexp(t, `\s+child\.grandChild\s+`, buffer1Lines[4])
// Ensure that the second buffer has the JSON encoder output
buffer2Lines := strings.Split(buffer2.String(), "\n")
require.Len(t, buffer2Lines, 5+1)
require.Regexp(t, `"logger"\s*:\s*"child"`, buffer2Lines[1])
require.Regexp(t, `"logger"\s*:\s*"child\.grandChild"`, buffer2Lines[4])
}