Skip to content

Commit ec1d9a6

Browse files
authored
Refactor log package (#4734)
* Replace logConfig with zapcore.Core * Add SyncFunc type and tests * Change Sentry functions to accept a *sentry.Client This allows the caller to do the error handling. * Rename newCoreConfig to newCore * Remove redundant WithCore option * Update doc comments
1 parent 7c84b27 commit ec1d9a6

File tree

2 files changed

+120
-126
lines changed

2 files changed

+120
-126
lines changed

pkg/log/log.go

Lines changed: 32 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -16,62 +16,31 @@ import (
1616
"go.uber.org/zap/zapcore"
1717
)
1818

19-
type logConfig struct {
20-
core zapcore.Core
21-
cleanup func() error
22-
err error
23-
}
19+
type SyncFunc func() error
2420

2521
type SinkOption func(*sinkConfig)
2622

27-
// New creates a new log object with the provided configurations. If no sinks
28-
// are provided, a no-op sink will be used. Returns the logger and a cleanup
23+
// New creates a new log object with the provided sinks. If no sinks are
24+
// provided, a no-op sink will be used. Returns the logger and a cleanup
2925
// function that should be executed before the program exits.
30-
func New(service string, configs ...logConfig) (logr.Logger, func() error) {
31-
return NewWithCaller(service, false, configs...)
26+
func New(service string, cores ...zapcore.Core) (logr.Logger, SyncFunc) {
27+
return NewWithCaller(service, false, cores...)
3228
}
3329

34-
// NewWithCaller creates a new logger named after the specified service with the provided sink configurations. If
35-
// addCaller is true, call site information will be attached to each emitted log message. (This behavior can be disabled
36-
// on a per-sink basis using WithSuppressCaller.)
37-
func NewWithCaller(service string, addCaller bool, configs ...logConfig) (logr.Logger, func() error) {
38-
var cores []zapcore.Core
39-
var cleanupFuncs []func() error
40-
41-
// create cores for the logger
42-
for _, config := range configs {
43-
if config.err != nil {
44-
continue
45-
}
46-
cores = append(cores, config.core)
47-
if config.cleanup != nil {
48-
cleanupFuncs = append(cleanupFuncs, config.cleanup)
49-
}
50-
}
30+
// NewWithCaller creates a new logger named after the specified service with
31+
// the provided sinks. If addCaller is true, call site information will be
32+
// attached to each emitted log message. (This behavior can be disabled on a
33+
// per-sink basis using WithSuppressCaller.)
34+
func NewWithCaller(service string, addCaller bool, cores ...zapcore.Core) (logr.Logger, SyncFunc) {
5135
// create logger
5236
zapLogger := zap.New(zapcore.NewTee(cores...), zap.WithCaller(addCaller))
53-
cleanupFuncs = append(cleanupFuncs, zapLogger.Sync)
5437
logger := zapr.NewLogger(zapLogger).WithName(service)
5538

56-
// report the errors we encountered in the configs
57-
for _, config := range configs {
58-
if config.err != nil {
59-
logger.Error(config.err, "error configuring logger")
60-
}
61-
}
62-
63-
return logger, firstErrorFunc(cleanupFuncs...)
39+
return logger, zapLogger.Sync
6440
}
6541

66-
// WithSentry adds sentry integration to the logger. This configuration may
67-
// fail, in which case, sentry will not be added and execution will continue
68-
// normally.
69-
func WithSentry(opts sentry.ClientOptions, tags map[string]string) logConfig {
70-
client, err := sentry.NewClient(opts)
71-
if err != nil {
72-
return logConfig{err: err}
73-
}
74-
42+
// WithSentry adds sentry integration to the logger.
43+
func WithSentry(client *sentry.Client, tags map[string]string) zapcore.Core {
7544
// create sentry core
7645
cfg := zapsentry.Configuration{
7746
Tags: tags,
@@ -81,16 +50,14 @@ func WithSentry(opts sentry.ClientOptions, tags map[string]string) logConfig {
8150
}
8251
core, err := zapsentry.NewCore(cfg, zapsentry.NewSentryClientFromClient(client))
8352
if err != nil {
84-
return logConfig{err: err}
53+
// NewCore should never fail because NewSentryClientFromClient
54+
// never returns an error and NewCore only returns an error if
55+
// the zapsentry.Configuration is invalid, which would indicate
56+
// a programmer error.
57+
panic(err)
8558
}
8659

87-
return logConfig{
88-
core: core,
89-
cleanup: func() error {
90-
sentry.Flush(5 * time.Second)
91-
return nil
92-
},
93-
}
60+
return core
9461
}
9562

9663
type sinkConfig struct {
@@ -102,8 +69,8 @@ type sinkConfig struct {
10269
}
10370

10471
// WithJSONSink adds a JSON encoded output to the logger.
105-
func WithJSONSink(sink io.Writer, opts ...SinkOption) logConfig {
106-
return newCoreConfig(
72+
func WithJSONSink(sink io.Writer, opts ...SinkOption) zapcore.Core {
73+
return newCore(
10774
zapcore.NewJSONEncoder(defaultEncoderConfig()),
10875
zapcore.Lock(zapcore.AddSync(sink)),
10976
globalLogLevel,
@@ -112,8 +79,8 @@ func WithJSONSink(sink io.Writer, opts ...SinkOption) logConfig {
11279
}
11380

11481
// WithConsoleSink adds a console-style output to the logger.
115-
func WithConsoleSink(sink io.Writer, opts ...SinkOption) logConfig {
116-
return newCoreConfig(
82+
func WithConsoleSink(sink io.Writer, opts ...SinkOption) zapcore.Core {
83+
return newCore(
11784
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
11885
zapcore.Lock(zapcore.AddSync(sink)),
11986
globalLogLevel,
@@ -135,32 +102,23 @@ func defaultEncoderConfig() zapcore.EncoderConfig {
135102
return conf
136103
}
137104

138-
// WithCore adds any user supplied zap core to the logger.
139-
func WithCore(core zapcore.Core) logConfig {
140-
return logConfig{core: core}
141-
}
142-
143105
// AddSentry initializes a sentry client and extends an existing
144106
// logr.Logger with the hook.
145-
func AddSentry(l logr.Logger, opts sentry.ClientOptions, tags map[string]string) (logr.Logger, func() error, error) {
146-
return AddSink(l, WithSentry(opts, tags))
107+
func AddSentry(l logr.Logger, client *sentry.Client, tags map[string]string) (logr.Logger, SyncFunc, error) {
108+
return AddSink(l, WithSentry(client, tags))
147109
}
148110

149111
// AddSink extends an existing logr.Logger with a new sink. It returns the new logr.Logger, a cleanup function, and an
150112
// error.
151113
//
152114
// The new sink will not inherit any of the existing logger's key-value pairs. Key-value pairs can be added to the new
153115
// sink specifically by passing them to this function.
154-
func AddSink(l logr.Logger, sink logConfig, keysAndValues ...any) (logr.Logger, func() error, error) {
155-
if sink.err != nil {
156-
return l, nil, sink.err
157-
}
158-
116+
func AddSink(l logr.Logger, core zapcore.Core, keysAndValues ...any) (logr.Logger, SyncFunc, error) {
159117
// New key-value pairs cannot be ergonomically added directly to cores. logr has code to do it, but that code is not
160118
// exported. Rather than replicating it ourselves, we indirectly use it by creating a temporary logger for the new
161119
// core, adding the key-value pairs to the temporary logger, and then extracting the temporary logger's modified
162120
// core.
163-
newSinkLogger := zapr.NewLogger(zap.New(sink.core))
121+
newSinkLogger := zapr.NewLogger(zap.New(core))
164122
newSinkLogger = newSinkLogger.WithValues(keysAndValues...)
165123
newCoreLogger, err := getZapLogger(newSinkLogger)
166124
if err != nil {
@@ -186,7 +144,7 @@ func AddSink(l logr.Logger, sink logConfig, keysAndValues ...any) (logr.Logger,
186144

187145
zapLogger = zapLogger.WithOptions(newLoggerOptions...)
188146
newLogger := zapr.NewLogger(zapLogger)
189-
return newLogger, firstErrorFunc(zapLogger.Sync, sink.cleanup), nil
147+
return newLogger, zapLogger.Sync, nil
190148
}
191149

192150
// getZapLogger is a helper function that gets the underlying zap logger from a
@@ -241,31 +199,14 @@ func ToSlogger(l logr.Logger) *slog.Logger {
241199
return slog.New(logr.ToSlogHandler(l))
242200
}
243201

244-
// firstErrorFunc is a helper function that returns a function that executes
245-
// all provided args and returns the first error, if any.
246-
func firstErrorFunc(fs ...func() error) func() error {
247-
return func() error {
248-
var firstErr error = nil
249-
for _, f := range fs {
250-
if f == nil {
251-
continue
252-
}
253-
if err := f(); err != nil && firstErr == nil {
254-
firstErr = err
255-
}
256-
}
257-
return firstErr
258-
}
259-
}
260-
261-
// newCoreConfig is a helper function that creates a default sinkConfig,
202+
// newCore is a helper function that creates a default sinkConfig,
262203
// applies the options, then creates a zapcore.Core.
263-
func newCoreConfig(
204+
func newCore(
264205
defaultEncoder zapcore.Encoder,
265206
defaultSink zapcore.WriteSyncer,
266207
defaultLevel levelSetter,
267208
opts ...SinkOption,
268-
) logConfig {
209+
) zapcore.Core {
269210
conf := sinkConfig{
270211
encoder: defaultEncoder,
271212
sink: defaultSink,
@@ -288,5 +229,5 @@ func newCoreConfig(
288229
core = &suppressCallerCore{Core: core}
289230
}
290231

291-
return logConfig{core: core}
232+
return core
292233
}

pkg/log/log_test.go

Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,49 @@ import (
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313
"go.uber.org/zap"
14+
"go.uber.org/zap/zapcore"
1415
)
1516

17+
// testCore implement zapcore.Core with custom methods.
18+
type testCore struct {
19+
check func(zapcore.Entry, *zapcore.CheckedEntry) *zapcore.CheckedEntry
20+
enabled func(zapcore.Level) bool
21+
sync func() error
22+
with func([]zapcore.Field) zapcore.Core
23+
write func(zapcore.Entry, []zapcore.Field) error
24+
}
25+
26+
func (t *testCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
27+
if t.check != nil {
28+
return t.check(e, ce)
29+
}
30+
return ce
31+
}
32+
func (t *testCore) Enabled(l zapcore.Level) bool {
33+
if t.enabled != nil {
34+
return t.enabled(l)
35+
}
36+
return true
37+
}
38+
func (t *testCore) Sync() error {
39+
if t.sync != nil {
40+
return t.sync()
41+
}
42+
return nil
43+
}
44+
func (t *testCore) With(fields []zapcore.Field) zapcore.Core {
45+
if t.with != nil {
46+
return t.with(fields)
47+
}
48+
return t
49+
}
50+
func (t *testCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
51+
if t.write != nil {
52+
return t.write(ent, fields)
53+
}
54+
return nil
55+
}
56+
1657
func TestNew(t *testing.T) {
1758
var jsonBuffer, consoleBuffer bytes.Buffer
1859
logger, flush := New("service-name",
@@ -60,42 +101,18 @@ func TestSetLevel(t *testing.T) {
60101
assert.Equal(t, true, logger.GetSink().Enabled(2))
61102
}
62103

63-
func TestWithSentryFailure(t *testing.T) {
64-
var buffer bytes.Buffer
65-
logger, flush := New("service-name",
66-
WithSentry(sentry.ClientOptions{Dsn: "fail"}, nil),
67-
WithConsoleSink(&buffer),
68-
)
69-
logger.Info("yay")
70-
assert.Nil(t, flush())
71-
72-
assert.Contains(t, buffer.String(), "error configuring logger")
73-
assert.Contains(t, buffer.String(), "yay")
74-
}
75-
76-
func TestAddSentryFailure(t *testing.T) {
77-
var buffer bytes.Buffer
78-
logger, flush := New("service-name", WithConsoleSink(&buffer))
79-
logger, _, err := AddSentry(logger, sentry.ClientOptions{Dsn: "fail"}, nil)
80-
assert.NotNil(t, err)
81-
assert.NotContains(t, err.Error(), "unsupported")
82-
83-
logger.Info("yay")
84-
assert.Nil(t, flush())
85-
86-
assert.Contains(t, buffer.String(), "yay")
87-
}
88-
89104
func TestAddSentry(t *testing.T) {
90105
var buffer bytes.Buffer
91106
var sentryMessage string
92-
logger, _ := New("service-name", WithConsoleSink(&buffer))
93-
logger, flush, err := AddSentry(logger, sentry.ClientOptions{
107+
client, err := sentry.NewClient(sentry.ClientOptions{
94108
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
95109
sentryMessage = event.Message
96110
return nil
97111
},
98-
}, nil)
112+
})
113+
assert.Nil(t, err)
114+
logger, _ := New("service-name", WithConsoleSink(&buffer))
115+
logger, flush, err := AddSentry(logger, client, nil)
99116
assert.Nil(t, err)
100117

101118
logger.Error(nil, "oops")
@@ -110,14 +127,16 @@ func TestAddSentry(t *testing.T) {
110127
func TestWithSentry(t *testing.T) {
111128
var buffer bytes.Buffer
112129
var sentryMessage string
130+
client, err := sentry.NewClient(sentry.ClientOptions{
131+
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
132+
sentryMessage = event.Message
133+
return nil
134+
},
135+
})
136+
assert.Nil(t, err)
113137
logger, flush := New("service-name",
114138
WithConsoleSink(&buffer),
115-
WithSentry(sentry.ClientOptions{
116-
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
117-
sentryMessage = event.Message
118-
return nil
119-
},
120-
}, nil),
139+
WithSentry(client, nil),
121140
)
122141
logger.Info("yay")
123142
logger.Error(nil, "oops")
@@ -291,3 +310,37 @@ func TestToSlogger(t *testing.T) {
291310
parsedJSON,
292311
)
293312
}
313+
314+
func TestSyncFunc(t *testing.T) {
315+
var syncCalled bool
316+
core := testCore{
317+
sync: func() error {
318+
syncCalled = true
319+
return nil
320+
},
321+
}
322+
_, flush := New("service-name", &core)
323+
assert.NoError(t, flush())
324+
assert.True(t, syncCalled)
325+
}
326+
327+
func TestAddSinkStillSyncs(t *testing.T) {
328+
var syncCalled [2]bool
329+
core := testCore{
330+
sync: func() error {
331+
syncCalled[0] = true
332+
return nil
333+
},
334+
}
335+
l, _ := New("service-name", &core)
336+
_, flush, err := AddSink(l, &testCore{
337+
sync: func() error {
338+
syncCalled[1] = true
339+
return nil
340+
},
341+
})
342+
assert.NoError(t, err)
343+
assert.NoError(t, flush())
344+
assert.True(t, syncCalled[0])
345+
assert.True(t, syncCalled[1])
346+
}

0 commit comments

Comments
 (0)