Skip to content

Commit bff6e2b

Browse files
committed
loop/server: use otelzap logger
1 parent 8817bb8 commit bff6e2b

File tree

5 files changed

+146
-7
lines changed

5 files changed

+146
-7
lines changed

pkg/logger/logger.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ func NewWith(cfgFn func(*zap.Config)) (Logger, error) {
9696
return &logger{core.Sugar()}, nil
9797
}
9898

99+
// NewCore returns a new Logger core from a modified [zap.Config].
100+
func NewCore(cfgFn func(*zap.Config)) (zapcore.Core, error) {
101+
cfg := zap.NewProductionConfig()
102+
cfgFn(&cfg)
103+
logger, err := cfg.Build()
104+
if err != nil {
105+
return nil, err
106+
}
107+
return logger.Core(), nil
108+
}
109+
99110
// NewWithSync returns a new Logger with a given SyncWriter.
100111
func NewWithSync(w io.Writer) Logger {
101112
core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(w), zapcore.InfoLevel)

pkg/logger/logger_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,25 @@ type differentLogger interface {
324324

325325
Sync() error
326326
}
327+
328+
func TestNewCore(t *testing.T) {
329+
// First core at Info (would drop Debug), second core at Debug
330+
obsCore, obsLogs := observer.New(zap.DebugLevel)
331+
332+
primaryCore, err := NewCore(func(cfg *zap.Config) {
333+
cfg.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
334+
})
335+
if err != nil {
336+
t.Fatalf("NewCore error: %v", err)
337+
}
338+
339+
lggr := NewWithCores(primaryCore, obsCore)
340+
341+
lggr.Debug("debug message should reach observer core")
342+
if got := obsLogs.Len(); got != 1 {
343+
t.Fatalf("expected 1 log in observer core, got %d", got)
344+
}
345+
if msg := obsLogs.All()[0].Message; msg != "debug message should reach observer core" {
346+
t.Fatalf("unexpected message: %s", msg)
347+
}
348+
}

pkg/loop/logger.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313
"go.uber.org/zap/zapcore"
1414
"golang.org/x/exp/slices"
1515

16+
otellog "go.opentelemetry.io/otel/log"
17+
1618
"github.com/smartcontractkit/chainlink-common/pkg/logger"
19+
"github.com/smartcontractkit/chainlink-common/pkg/logger/otelzap"
1720
)
1821

1922
// HCLogLogger returns an [hclog.Logger] backed by the given [logger.Logger].
@@ -162,13 +165,37 @@ func (h *hclSinkAdapter) Accept(_ string, level hclog.Level, msg string, args ..
162165

163166
// NewLogger returns a new [logger.Logger] configured to encode [hclog] compatible JSON.
164167
func NewLogger() (logger.Logger, error) {
165-
return logger.NewWith(func(cfg *zap.Config) {
166-
cfg.Level.SetLevel(zap.DebugLevel)
167-
cfg.EncoderConfig.LevelKey = "@level"
168-
cfg.EncoderConfig.MessageKey = "@message"
169-
cfg.EncoderConfig.TimeKey = "@timestamp"
170-
cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02T15:04:05.000000Z07:00")
171-
})
168+
return logger.NewWith(configureHCLogEncoder)
169+
}
170+
171+
// configureHCLogEncoder mutates cfg to use hclog-compatible field names and timestamp format.
172+
// NOTE: It also sets the log level to Debug to preserve prior behavior where each caller
173+
// manually set Debug before applying identical encoder tweaks. Centralizing avoids drift.
174+
// If a different level is desired, callers should override cfg.Level AFTER calling this helper.
175+
func configureHCLogEncoder(cfg *zap.Config) {
176+
cfg.Level.SetLevel(zap.DebugLevel)
177+
cfg.EncoderConfig.LevelKey = "@level"
178+
cfg.EncoderConfig.MessageKey = "@message"
179+
cfg.EncoderConfig.TimeKey = "@timestamp"
180+
cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02T15:04:05.000000Z07:00")
181+
}
182+
183+
// NewOtelLogger returns a logger with two cores:
184+
// 1. The primary JSON core configured via cfgFn (encoder keys changed to @level, @message, @timestamp).
185+
// 2. The otel core (otelzap.NewCore) which receives the raw zap.Entry and fields.
186+
//
187+
// Important:
188+
// The cfgFn only mutates the encoder config used to build the first core.
189+
// otelzap.NewCore implements zapcore.Core and does NOT use that encoder; it derives attributes from the zap.Entry
190+
// (Message, Level, Time, etc.) and zap.Fields directly. Therefore changing encoder keys here does NOT affect how
191+
// the otel core extracts data, and only the first core's JSON output format is altered.
192+
// This preserves backward compatibility for OTEL export while allowing hclog-compatible key names in the primary output.
193+
func NewOtelLogger(otelLogger otellog.Logger) (logger.Logger, error) {
194+
primaryCore, err := logger.NewCore(configureHCLogEncoder)
195+
if err != nil {
196+
return nil, err
197+
}
198+
return logger.NewWithCores(primaryCore, otelzap.NewCore(otelLogger)), nil
172199
}
173200

174201
// onceValue returns a function that invokes f only once and returns the value

pkg/loop/logger_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package loop
22

33
import (
4+
"context"
5+
"sync"
46
"testing"
57

8+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
9+
"github.com/smartcontractkit/chainlink-common/pkg/logger/otelzap"
610
"github.com/stretchr/testify/assert"
11+
sdklog "go.opentelemetry.io/otel/sdk/log"
712
)
813

914
func Test_removeArg(t *testing.T) {
@@ -38,3 +43,69 @@ func Test_removeArg(t *testing.T) {
3843
})
3944
}
4045
}
46+
47+
func TestNewOtelLogger(t *testing.T) {
48+
tests := []struct {
49+
name string
50+
logFn func(l logger.Logger)
51+
wantMsg string
52+
}{
53+
{
54+
name: "debug",
55+
logFn: func(l logger.Logger) {
56+
l.Debugw("hello world", "k", "v")
57+
},
58+
wantMsg: "hello world",
59+
},
60+
{
61+
name: "info",
62+
logFn: func(l logger.Logger) {
63+
l.Infow("info msg", "a", 1)
64+
},
65+
wantMsg: "info msg",
66+
},
67+
}
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
exp := &recordingExporter{}
71+
lp := sdklog.NewLoggerProvider(
72+
sdklog.WithProcessor(sdklog.NewSimpleProcessor(exp)),
73+
)
74+
otelLggr := lp.Logger("test-" + tt.name)
75+
76+
lggr, err := NewOtelLogger(otelLggr)
77+
if err != nil {
78+
t.Fatalf("NewOtelLogger error: %v", err)
79+
}
80+
81+
tt.logFn(lggr)
82+
83+
if len(exp.records) != 1 {
84+
t.Fatalf("expected 1 exported record, got %d", len(exp.records))
85+
}
86+
if got := exp.records[0].Body().AsString(); got != tt.wantMsg {
87+
t.Fatalf("unexpected body: got %q want %q", got, tt.wantMsg)
88+
}
89+
})
90+
}
91+
}
92+
93+
// recordingExporter captures exported log records (current sdk/log Export signature).
94+
type recordingExporter struct {
95+
mu sync.Mutex
96+
records []sdklog.Record
97+
}
98+
99+
func (r *recordingExporter) Export(_ context.Context, recs []sdklog.Record) error {
100+
r.mu.Lock()
101+
defer r.mu.Unlock()
102+
r.records = append(r.records, recs...)
103+
return nil
104+
}
105+
func (r *recordingExporter) ForceFlush(context.Context) error { return nil }
106+
func (r *recordingExporter) Shutdown(context.Context) error { return nil }
107+
108+
// Compile-time assertion that otelzap.NewCore still satisfies zapcore.Core usage pattern.
109+
// (Guards against accidental API break causing this test file to silently compile with stubs.)
110+
var _ = otelzap.NewCore
111+
var _ logger.Logger // silence unused import of logger in case future refactors remove usage

pkg/loop/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ func (s *Server) start() error {
148148
}
149149
beholder.SetClient(beholderClient)
150150
beholder.SetGlobalOtelProviders()
151+
152+
if beholderCfg.LogStreamingEnabled {
153+
otelLogger, err := NewOtelLogger(beholderClient.Logger)
154+
if err != nil {
155+
return fmt.Errorf("failed to enable log streaming: %w", err)
156+
}
157+
s.Logger = logger.Sugared(logger.Named(otelLogger, s.Logger.Name()))
158+
}
151159
}
152160

153161
s.promServer = NewPromServer(s.EnvConfig.PrometheusPort, s.Logger)

0 commit comments

Comments
 (0)