Skip to content

Commit 83bf80e

Browse files
committed
CBG-3933 allow console logs to be rotated
1 parent a091f47 commit 83bf80e

File tree

7 files changed

+240
-147
lines changed

7 files changed

+240
-147
lines changed

base/logger.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ func FlushLogBuffers() {
6161

6262
// logCollationWorker will take log lines over the given channel, and buffer them until either the buffer is full, or the flushTimeout is exceeded.
6363
// This is to reduce the number of writes to the log files, in order to batch them up as larger collated chunks, whilst maintaining a low-level of latency with the flush timeout.
64-
func logCollationWorker(collateBuffer chan string, flushChan chan struct{}, collateBufferWg *sync.WaitGroup, logger *log.Logger, maxBufferSize int, collateFlushTimeout time.Duration) {
65-
64+
func logCollationWorker(loggerClosed chan struct{}, collateBuffer chan string, flushChan chan struct{}, collateBufferWg *sync.WaitGroup, logger *log.Logger, maxBufferSize int, collateFlushTimeout time.Duration) {
6665
// The initial duration of the timeout timer doesn't matter,
6766
// because we reset it whenever we buffer a log without flushing it.
6867
t := time.NewTimer(math.MaxInt64)
@@ -97,6 +96,8 @@ func logCollationWorker(collateBuffer chan string, flushChan chan struct{}, coll
9796
logger.Print(strings.Join(logBuffer, "\n"))
9897
logBuffer = logBuffer[:0]
9998
}
99+
case <-loggerClosed:
100+
return
100101
}
101102
}
102103
}

base/logger_audit_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,13 @@ func TestAuditLoggerGlobalFields(t *testing.T) {
132132
}
133133
for _, testCase := range testCases {
134134
t.Run(testCase.name, func(t *testing.T) {
135+
ResetGlobalTestLogging(t)
135136
ctx := TestCtx(t)
136137
if testCase.contextFields != nil {
137138
ctx = AuditLogCtx(ctx, testCase.contextFields)
138139
}
139140
logger, err := NewAuditLogger(ctx, &AuditLoggerConfig{FileLoggerConfig: FileLoggerConfig{Enabled: BoolPtr(true)}}, tmpdir, 0, nil, testCase.globalFields)
140141
require.NoError(t, err)
141-
defer assert.NoError(t, logger.Close())
142142
auditLogger.Store(logger)
143143

144144
startWarnCount := SyncGatewayStats.GlobalStats.ResourceUtilizationStats().WarnCount.Value()

base/logger_console.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import (
1919
"path/filepath"
2020
"strconv"
2121
"sync"
22-
23-
"gopkg.in/natefinch/lumberjack.v2"
2422
)
2523

2624
// ConsoleLogger is a file logger with a default output of stderr, and tunable log level/keys.
@@ -55,8 +53,12 @@ func NewConsoleLogger(ctx context.Context, shouldLogLocation bool, config *Conso
5553
config = &ConsoleLoggerConfig{}
5654
}
5755

56+
cancelCtx, cancelFunc := context.WithCancel(ctx)
57+
5858
// validate and set defaults
59-
if err := config.init(); err != nil {
59+
rotationDoneChan, err := config.init(cancelCtx)
60+
if err != nil {
61+
defer cancelFunc()
6062
return nil, err
6163
}
6264

@@ -67,23 +69,27 @@ func NewConsoleLogger(ctx context.Context, shouldLogLocation bool, config *Conso
6769
LogKeyMask: &logKey,
6870
ColorEnabled: *config.ColorEnabled,
6971
FileLogger: FileLogger{
70-
Enabled: AtomicBool{},
71-
logger: log.New(config.Output, "", 0),
72-
config: config.FileLoggerConfig,
72+
Enabled: AtomicBool{},
73+
logger: log.New(config.Output, "", 0),
74+
config: config.FileLoggerConfig,
75+
rotationDoneChan: rotationDoneChan,
76+
cancelFunc: cancelFunc,
77+
name: "console",
78+
closed: make(chan struct{}),
7379
},
7480
isStderr: config.FileOutput == "" && *config.Enabled,
7581
config: *config,
7682
}
7783
logger.Enabled.Set(*config.Enabled)
7884

7985
// Only create the collateBuffer channel and worker if required.
80-
if *config.CollationBufferSize > 1 {
86+
if *config.CollationBufferSize > 1 && !logger.isStderr {
8187
logger.collateBuffer = make(chan string, *config.CollationBufferSize)
8288
logger.flushChan = make(chan struct{}, 1)
8389
logger.collateBufferWg = &sync.WaitGroup{}
8490

8591
// Start up a single worker to consume messages from the buffer
86-
go logCollationWorker(logger.collateBuffer, logger.flushChan, logger.collateBufferWg, logger.logger, *config.CollationBufferSize, consoleLoggerCollateFlushTimeout)
92+
go logCollationWorker(logger.closed, logger.collateBuffer, logger.flushChan, logger.collateBufferWg, logger.logger, *config.CollationBufferSize, consoleLoggerCollateFlushTimeout)
8793
}
8894

8995
// We can only log the console log location itself when logging has previously been set up and is being re-initialized from a config.
@@ -179,29 +185,25 @@ func (l *ConsoleLogger) getConsoleLoggerConfig() *ConsoleLoggerConfig {
179185
}
180186

181187
// init validates and sets any defaults for the given ConsoleLoggerConfig
182-
func (lcc *ConsoleLoggerConfig) init() error {
188+
func (lcc *ConsoleLoggerConfig) init(ctx context.Context) (chan struct{}, error) {
183189
if lcc == nil {
184-
return errors.New("nil LogConsoleConfig")
190+
return nil, errors.New("nil LogConsoleConfig")
185191
}
186192

187193
if err := lcc.initRotationConfig("console", 0, 0); err != nil {
188-
return err
194+
return nil, err
189195
}
190196

197+
var rotationDoneChan chan struct{}
191198
// Default to os.Stderr if alternative output is not set
192199
if lcc.Output == nil && lcc.FileOutput == "" {
193200
lcc.Output = os.Stderr
194201
} else if lcc.FileOutput != "" {
195202
// Otherwise check permissions on the given output and create a Lumberjack logger
196203
if err := validateLogFileOutput(lcc.FileOutput); err != nil {
197-
return err
198-
}
199-
lcc.Output = &lumberjack.Logger{
200-
Filename: filepath.FromSlash(lcc.FileOutput),
201-
MaxSize: *lcc.Rotation.MaxSize,
202-
MaxAge: *lcc.Rotation.MaxAge,
203-
Compress: BoolDefault(lcc.Rotation.Compress, false),
204+
return nil, err
204205
}
206+
rotationDoneChan = lcc.initLumberjack(ctx, "console", filepath.FromSlash(lcc.FileOutput))
205207
}
206208

207209
// Default to disabled only when a log key or log level has not been specified
@@ -225,7 +227,7 @@ func (lcc *ConsoleLoggerConfig) init() error {
225227
newLevel := LevelInfo
226228
lcc.LogLevel = &newLevel
227229
} else if *lcc.LogLevel < LevelNone || *lcc.LogLevel > levelCount {
228-
return fmt.Errorf("invalid log level: %v", *lcc.LogLevel)
230+
return nil, fmt.Errorf("invalid log level: %v", *lcc.LogLevel)
229231
}
230232

231233
// If ColorEnabled is not explicitly set, use the value of $SG_COLOR
@@ -249,7 +251,7 @@ func (lcc *ConsoleLoggerConfig) init() error {
249251
lcc.CollationBufferSize = &bufferSize
250252
}
251253

252-
return nil
254+
return rotationDoneChan, nil
253255
}
254256

255257
func mustInitConsoleLogger(ctx context.Context, config *ConsoleLoggerConfig) *ConsoleLogger {

base/logger_file.go

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type FileLogger struct {
5151
// collateBuffer is used to store log entries to batch up multiple logs.
5252
collateBuffer chan string
5353
collateBufferWg *sync.WaitGroup
54+
closed chan struct{}
5455
flushChan chan struct{}
5556
level LogLevel
5657
name string
@@ -87,11 +88,11 @@ func NewFileLogger(ctx context.Context, config *FileLoggerConfig, level LogLevel
8788
config = &FileLoggerConfig{}
8889
}
8990

90-
rotationDoneChan := make(chan struct{})
9191
cancelCtx, cancelFunc := context.WithCancel(ctx)
9292

9393
// validate and set defaults
94-
if err := config.init(cancelCtx, level, name, logFilePath, minAge, rotationDoneChan); err != nil {
94+
rotationDoneChan, err := config.init(cancelCtx, level, name, logFilePath, minAge)
95+
if err != nil {
9596
cancelFunc()
9697
return nil, err
9798
}
@@ -103,6 +104,7 @@ func NewFileLogger(ctx context.Context, config *FileLoggerConfig, level LogLevel
103104
output: config.Output,
104105
logger: log.New(config.Output, "", 0),
105106
config: *config,
107+
closed: make(chan struct{}),
106108
cancelFunc: cancelFunc,
107109
rotationDoneChan: rotationDoneChan,
108110
}
@@ -119,7 +121,7 @@ func NewFileLogger(ctx context.Context, config *FileLoggerConfig, level LogLevel
119121
logger.collateBufferWg = &sync.WaitGroup{}
120122

121123
// Start up a single worker to consume messages from the buffer
122-
go logCollationWorker(logger.collateBuffer, logger.flushChan, logger.collateBufferWg, logger.logger, *config.CollationBufferSize, fileLoggerCollateFlushTimeout)
124+
go logCollationWorker(logger.closed, logger.collateBuffer, logger.flushChan, logger.collateBufferWg, logger.logger, *config.CollationBufferSize, fileLoggerCollateFlushTimeout)
123125
}
124126

125127
return logger, nil
@@ -152,21 +154,26 @@ func (l *FileLogger) Rotate() error {
152154

153155
// Close cancels the log rotation rotation and the underlying file descriptor for the active log file.
154156
func (l *FileLogger) Close() error {
157+
// cancelFunc will stop the log rotionation/deletion goroutine
158+
// once all log rotation is done and log output is closed, shut down the logCollationWorker
159+
defer close(l.closed)
155160
// cancel the log rotation goroutine and wait for it to stop
156161
if l.cancelFunc != nil {
157162
l.cancelFunc()
158163
}
164+
// wait for the rotation goroutine to stop
159165
if l.rotationDoneChan != nil {
160166
<-l.rotationDoneChan
161167
}
168+
162169
if c, ok := l.output.(io.Closer); ok {
163170
return c.Close()
164171
}
165172
return nil
166173
}
167174

168175
func (l *FileLogger) String() string {
169-
return "FileLogger(" + l.level.String() + ")"
176+
return "FileLogger(" + l.name + ")"
170177
}
171178

172179
// logf will put the given message into the collation buffer if it exists,
@@ -210,9 +217,9 @@ func (l *FileLogger) getFileLoggerConfig() *FileLoggerConfig {
210217
return &fileLoggerConfig
211218
}
212219

213-
func (lfc *FileLoggerConfig) init(ctx context.Context, level LogLevel, name string, logFilePath string, minAge int, rotationDoneChan chan struct{}) error {
220+
func (lfc *FileLoggerConfig) init(ctx context.Context, level LogLevel, name string, logFilePath string, minAge int) (chan struct{}, error) {
214221
if lfc == nil {
215-
return errors.New("nil LogFileConfig")
222+
return nil, errors.New("nil LogFileConfig")
216223
}
217224

218225
if lfc.Enabled == nil {
@@ -221,18 +228,12 @@ func (lfc *FileLoggerConfig) init(ctx context.Context, level LogLevel, name stri
221228
}
222229

223230
if err := lfc.initRotationConfig(name, defaultMaxSize, minAge); err != nil {
224-
return err
231+
return nil, err
225232
}
226233

227-
var rotateableLogger *lumberjack.Logger
234+
var rotationDoneChan chan struct{}
228235
if lfc.Output == nil {
229-
rotateableLogger = newLumberjackOutput(
230-
filepath.Join(filepath.FromSlash(logFilePath), logFilePrefix+name+".log"),
231-
*lfc.Rotation.MaxSize,
232-
*lfc.Rotation.MaxAge,
233-
BoolDefault(lfc.Rotation.Compress, true),
234-
)
235-
lfc.Output = rotateableLogger
236+
rotationDoneChan = lfc.initLumberjack(ctx, name, filepath.Join(filepath.FromSlash(logFilePath), logFilePrefix+name+".log"))
236237
}
237238

238239
if lfc.CollationBufferSize == nil {
@@ -244,9 +245,25 @@ func (lfc *FileLoggerConfig) init(ctx context.Context, level LogLevel, name stri
244245
lfc.CollationBufferSize = &bufferSize
245246
}
246247

248+
return rotationDoneChan, nil
249+
}
250+
251+
// initLumberjack will create a new Lumberjack logger from the given config settings. Returns a doneChan which fires when the log rotation is stopped.
252+
func (lfc *FileLoggerConfig) initLumberjack(ctx context.Context, name string, lumberjackFilename string) chan struct{} {
253+
rotationDoneChan := make(chan struct{})
254+
dir, path := filepath.Split(lumberjackFilename)
255+
prefix := path[0:strings.Index(path, ".")]
256+
rotateableLogger := &lumberjack.Logger{
257+
Filename: lumberjackFilename,
258+
MaxSize: *lfc.Rotation.MaxSize,
259+
MaxAge: *lfc.Rotation.MaxAge,
260+
Compress: BoolDefault(lfc.Rotation.Compress, true),
261+
}
262+
lfc.Output = rotateableLogger
263+
247264
var rotationTicker *time.Ticker
248265
var rotationTickerCh <-chan time.Time
249-
if i := lfc.Rotation.RotationInterval.Value(); i > 0 && rotateableLogger != nil {
266+
if i := lfc.Rotation.RotationInterval.Value(); i > 0 {
250267
rotationTicker = time.NewTicker(i)
251268
rotationTickerCh = rotationTicker.C
252269
}
@@ -268,7 +285,7 @@ func (lfc *FileLoggerConfig) init(ctx context.Context, level LogLevel, name stri
268285
close(rotationDoneChan)
269286
return
270287
case <-logDeletionTicker.C:
271-
err := runLogDeletion(ctx, logFilePath, level.String(), int(float64(*lfc.Rotation.RotatedLogsSizeLimit)*rotatedLogsLowWatermarkMultiplier), *lfc.Rotation.RotatedLogsSizeLimit)
288+
err := runLogDeletion(ctx, dir, prefix, int(float64(*lfc.Rotation.RotatedLogsSizeLimit)*rotatedLogsLowWatermarkMultiplier), *lfc.Rotation.RotatedLogsSizeLimit)
272289
if err != nil {
273290
WarnfCtx(ctx, "%s", err)
274291
}
@@ -281,10 +298,10 @@ func (lfc *FileLoggerConfig) init(ctx context.Context, level LogLevel, name stri
281298
}
282299
}
283300
}()
284-
285-
return nil
301+
return rotationDoneChan
286302
}
287303

304+
// initRotationConfig will validate the log rotation settings and set defaults where necessary.
288305
func (lfc *FileLoggerConfig) initRotationConfig(name string, defaultMaxSize, minAge int) error {
289306
if lfc.Rotation.MaxSize == nil {
290307
lfc.Rotation.MaxSize = &defaultMaxSize
@@ -322,20 +339,10 @@ func (lfc *FileLoggerConfig) initRotationConfig(name string, defaultMaxSize, min
322339
return nil
323340
}
324341

325-
func newLumberjackOutput(filename string, maxSize, maxAge int, compress bool) *lumberjack.Logger {
326-
return &lumberjack.Logger{
327-
Filename: filename,
328-
MaxSize: maxSize,
329-
MaxAge: maxAge,
330-
Compress: compress,
331-
}
332-
}
333-
334342
// runLogDeletion will delete rotated logs for the supplied logLevel. It will only perform these deletions when the
335343
// cumulative size of the logs are above the supplied sizeLimitMB.
336344
// logDirectory is the supplied directory where the logs are stored.
337-
func runLogDeletion(ctx context.Context, logDirectory string, logLevel string, sizeLimitMBLowWatermark int, sizeLimitMBHighWatermark int) (err error) {
338-
345+
func runLogDeletion(ctx context.Context, logDirectory string, logPrefix string, sizeLimitMBLowWatermark int, sizeLimitMBHighWatermark int) (err error) {
339346
sizeLimitMBLowWatermark = sizeLimitMBLowWatermark * 1024 * 1024 // Convert MB input to bytes
340347
sizeLimitMBHighWatermark = sizeLimitMBHighWatermark * 1024 * 1024 // Convert MB input to bytes
341348

@@ -352,7 +359,7 @@ func runLogDeletion(ctx context.Context, logDirectory string, logLevel string, s
352359
willDelete := false
353360
for i := len(files) - 1; i >= 0; i-- {
354361
file := files[i]
355-
if strings.HasPrefix(file.Name(), logFilePrefix+logLevel) && strings.HasSuffix(file.Name(), ".log.gz") {
362+
if strings.HasPrefix(file.Name(), logPrefix) && strings.HasSuffix(file.Name(), ".gz") {
356363
fi, err := file.Info()
357364
if err != nil {
358365
InfofCtx(ctx, KeyAll, "Couldn't get size of log file %q: %v - ignoring for cleanup calculation", file.Name(), err)
@@ -373,7 +380,7 @@ func runLogDeletion(ctx context.Context, logDirectory string, logLevel string, s
373380
if willDelete {
374381
for j := indexDeletePoint; j >= 0; j-- {
375382
file := files[j]
376-
if strings.HasPrefix(file.Name(), logFilePrefix+logLevel) && strings.HasSuffix(file.Name(), ".log.gz") {
383+
if strings.HasPrefix(file.Name(), logPrefix) && strings.HasSuffix(file.Name(), ".gz") {
377384
err = os.Remove(filepath.Join(logDirectory, file.Name()))
378385
if err != nil {
379386
return errors.New(fmt.Sprintf("Error deleting stale log file: %v", err))

base/logger_file_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ func TestRotatedLogDeletion(t *testing.T) {
129129
err = makeTestFile(2, logFilePrefix+"info-2019-02-02T12-10-00.log.gz", dir)
130130
assert.NoError(t, err)
131131
ctx := TestCtx(t)
132-
err = runLogDeletion(ctx, dir, "error", 3, 5)
132+
err = runLogDeletion(ctx, dir, logFilePrefix+"error", 3, 5)
133133
assert.NoError(t, err)
134-
err = runLogDeletion(ctx, dir, "info", 5, 7)
134+
err = runLogDeletion(ctx, dir, logFilePrefix+"info", 5, 7)
135135
assert.NoError(t, err)
136136
dirContents, err = os.ReadDir(dir)
137137
require.NoError(t, err)
@@ -164,7 +164,7 @@ func TestRotatedLogDeletion(t *testing.T) {
164164
dir = t.TempDir()
165165
err = makeTestFile(5, logFilePrefix+"error.log.gz", dir)
166166
assert.NoError(t, err)
167-
err = runLogDeletion(ctx, dir, "error", 2, 4)
167+
err = runLogDeletion(ctx, dir, logFilePrefix+"error", 2, 4)
168168
assert.NoError(t, err)
169169
dirContents, err = os.ReadDir(dir)
170170
require.NoError(t, err)
@@ -175,7 +175,7 @@ func TestRotatedLogDeletion(t *testing.T) {
175175
dir = t.TempDir()
176176
err = makeTestFile(1, logFilePrefix+"error.log.gz", dir)
177177
assert.NoError(t, err)
178-
err = runLogDeletion(ctx, dir, "error", 2, 4)
178+
err = runLogDeletion(ctx, dir, logFilePrefix+"error", 2, 4)
179179
assert.NoError(t, err)
180180
dirContents, err = os.ReadDir(dir)
181181
require.NoError(t, err)
@@ -192,7 +192,7 @@ func TestRotatedLogDeletion(t *testing.T) {
192192
assert.NoError(t, err)
193193
err = makeTestFile(1, logFilePrefix+"error-2019-01-01T12-00-00.log.gz", dir)
194194
assert.NoError(t, err)
195-
err = runLogDeletion(ctx, dir, "error", 2, 3)
195+
err = runLogDeletion(ctx, dir, logFilePrefix+"error", 2, 3)
196196
assert.NoError(t, err)
197197

198198
dirContents, err = os.ReadDir(dir)

base/logging_config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ func NewMemoryLogger(level LogLevel) *FileLogger {
155155
Enabled: AtomicBool{1},
156156
level: level,
157157
name: level.String(),
158+
closed: make(chan struct{}),
158159
}
159160
logger.output = &logger.buffer
160161
logger.logger = log.New(&logger.buffer, "", 0)

0 commit comments

Comments
 (0)