Skip to content

Commit 19901e0

Browse files
authored
support buffered logger (#44)
1 parent 14f3b4c commit 19901e0

3 files changed

Lines changed: 106 additions & 5 deletions

File tree

config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ type FileLogConfig struct {
3838
// Compression function for rotated files.
3939
// Currently only `gzip` and empty are supported, empty means compression disabled.
4040
Compression string `toml:"compression" json:"compression"`
41+
// IsBuffered is true means use buffered logger.
42+
IsBuffered bool `toml:"is-buffered" json:"is-buffered"`
43+
// BufferSize is the size of the buffer.
44+
BufferSize int `toml:"buffer-size" json:"buffer-size"`
45+
// BufferFlushInterval is the interval of buffer flush.
46+
BufferFlushInterval time.Duration `toml:"buffer-flush-interval" json:"buffer-flush-interval"`
4147
}
4248

4349
// Config serializes log related config in toml/json.

log.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,15 @@ func InitLogger(cfg *Config, opts ...zap.Option) (*zap.Logger, *ZapProperties, e
4949
if err != nil {
5050
return nil, nil, err
5151
}
52-
output = zapcore.AddSync(lg)
52+
if cfg.File.IsBuffered {
53+
output = &zapcore.BufferedWriteSyncer{
54+
WS: zapcore.AddSync(lg),
55+
Size: cfg.File.BufferSize,
56+
FlushInterval: cfg.File.BufferFlushInterval,
57+
}
58+
} else {
59+
output = zapcore.AddSync(lg)
60+
}
5361
} else {
5462
stdOut, _, err := zap.Open([]string{"stdout"}...)
5563
if err != nil {
@@ -258,6 +266,8 @@ func S() *zap.SugaredLogger {
258266

259267
// ReplaceGlobals replaces the global Logger and SugaredLogger, and returns a
260268
// function to restore the original values. It's safe for concurrent use.
269+
// Be careful when using this with buffered logger, the flush goroutine in
270+
// https://pkg.go.dev/go.uber.org/zap/zapcore#BufferedWriteSyncer will not be stopped.
261271
func ReplaceGlobals(logger *zap.Logger, props *ZapProperties) func() {
262272
// TODO: This globalMu can be replaced by atomic.Swap(), available since go1.17.
263273
globalMu.Lock()

zap_log_test.go

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,91 @@ func TestRotateLog(t *testing.T) {
197197
_ = os.RemoveAll(tempDir)
198198
}
199199

200+
func TestBufferedLog(t *testing.T) {
201+
tempDir, _ := os.MkdirTemp("/tmp", "buffered-tests-log")
202+
conf := &Config{
203+
Level: "info",
204+
File: FileLogConfig{
205+
Filename: tempDir + "/test.log",
206+
MaxSize: 10, // no rotate
207+
IsBuffered: true,
208+
BufferSize: 1024,
209+
BufferFlushInterval: 3 * time.Second,
210+
},
211+
}
212+
213+
bufferedLogger, _, err := InitLogger(conf)
214+
require.NoError(t, err)
215+
216+
logFile := filepath.Join(tempDir, "test.log")
217+
218+
bufferedLogger.Info("first message")
219+
content, err := os.ReadFile(logFile)
220+
if err == nil {
221+
require.Empty(t, content, "expected empty before time-based flush")
222+
} else {
223+
require.True(t, os.IsNotExist(err), "expected file not exist before time-based flush")
224+
}
225+
226+
require.Eventually(t, func() bool {
227+
data, err := os.ReadFile(logFile)
228+
return err == nil && strings.Contains(string(data), "first message")
229+
}, 5*time.Second, 100*time.Millisecond, "expected data flushed after interval")
230+
231+
longMsg := strings.Repeat("x", 200)
232+
for i := 0; i < 20; i++ { // Larger than buffer size
233+
bufferedLogger.Info("big message", zap.String("msg", longMsg))
234+
}
235+
236+
require.Eventually(t, func() bool {
237+
data, err := os.ReadFile(logFile)
238+
return err == nil && strings.Contains(string(data), "big message")
239+
}, 1*time.Second, 100*time.Millisecond, "expected data flushed after buffer size exceeded")
240+
241+
_ = os.RemoveAll(tempDir)
242+
}
243+
244+
func TestBufferedLogWithRotate(t *testing.T) {
245+
tempDir, _ := os.MkdirTemp("/tmp", "buffered-rotate-tests-log")
246+
conf := &Config{
247+
Level: "info",
248+
File: FileLogConfig{
249+
Filename: filepath.Join(tempDir, "test.log"),
250+
MaxSize: 1, // 1 MB
251+
IsBuffered: true,
252+
BufferSize: 4 * 1024,
253+
BufferFlushInterval: 5 * time.Second,
254+
},
255+
}
256+
257+
bufferedLogger, _, err := InitLogger(conf)
258+
require.NoError(t, err)
259+
260+
largeMsg := strings.Repeat("x", 200*1024)
261+
262+
for i := 0; i < 10; i++ {
263+
bufferedLogger.Info("rotating message", zap.String("msg", largeMsg))
264+
}
265+
266+
require.Eventually(t, func() bool {
267+
files, err := os.ReadDir(tempDir)
268+
if err != nil {
269+
return false
270+
}
271+
return len(files) >= 2
272+
}, 5*time.Second, 200*time.Millisecond, "expected log rotation after buffer flush")
273+
274+
files, _ := os.ReadDir(tempDir)
275+
var totalSize int64
276+
for _, f := range files {
277+
info, _ := f.Info()
278+
totalSize += info.Size()
279+
}
280+
require.Greater(t, totalSize, int64(1*1024*1024), "expected total written logs > 1MB")
281+
282+
_ = os.RemoveAll(tempDir)
283+
}
284+
200285
func TestErrorLog(t *testing.T) {
201286
ts := newTestLogSpy(t)
202287
conf := &Config{Level: "debug", DisableTimestamp: true}
@@ -232,8 +317,8 @@ func TestLogJSON(t *testing.T) {
232317
"backoff", time.Second,
233318
)
234319
logger.With(zap.String("connID", "1"), zap.String("traceID", "dse1121")).Info("new connection")
235-
ts.assertMessages("{\"level\":\"INFO\",\"caller\":\"zap_log_test.go:229\",\"message\":\"failed to fetch URL\",\"url\":\"http://example.com\",\"attempt\":3,\"backoff\":\"1s\"}",
236-
"{\"level\":\"INFO\",\"caller\":\"zap_log_test.go:234\",\"message\":\"new connection\",\"connID\":\"1\",\"traceID\":\"dse1121\"}")
320+
ts.assertMessages("{\"level\":\"INFO\",\"caller\":\"zap_log_test.go:314\",\"message\":\"failed to fetch URL\",\"url\":\"http://example.com\",\"attempt\":3,\"backoff\":\"1s\"}",
321+
"{\"level\":\"INFO\",\"caller\":\"zap_log_test.go:319\",\"message\":\"new connection\",\"connID\":\"1\",\"traceID\":\"dse1121\"}")
237322
}
238323

239324
func TestRotateLogWithCompress(t *testing.T) {
@@ -279,8 +364,8 @@ func TestCompressError(t *testing.T) {
279364
conf := &Config{
280365
Level: "info",
281366
File: FileLogConfig{
282-
Filename: tempDir + "/test.log",
283-
MaxSize: 1,
367+
Filename: tempDir + "/test.log",
368+
MaxSize: 1,
284369
Compression: "xxx",
285370
},
286371
}

0 commit comments

Comments
 (0)