From f596a4dabfd2fa4e7347161a32570a8b3b91c631 Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Thu, 31 Oct 2024 20:01:45 +0100 Subject: [PATCH] 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 --- logutils/core.go | 118 ++++++++++++++++++++++++++++++++++++++++++ logutils/core_test.go | 61 ++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 logutils/core.go create mode 100644 logutils/core_test.go diff --git a/logutils/core.go b/logutils/core.go new file mode 100644 index 000000000..8f1aa1512 --- /dev/null +++ b/logutils/core.go @@ -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 + } +} diff --git a/logutils/core_test.go b/logutils/core_test.go new file mode 100644 index 000000000..efb7741c3 --- /dev/null +++ b/logutils/core_test.go @@ -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]) +}