Skip to content

Commit a41d708

Browse files
committed
log rotation: addressing PR feedback
1 parent cd92162 commit a41d708

File tree

7 files changed

+103
-68
lines changed

7 files changed

+103
-68
lines changed

cmd/telegraf/telegraf.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func runAgent(ctx context.Context,
114114
) error {
115115
// Setup default logging. This may need to change after reading the config
116116
// file, but we can configure it to use our logger implementation now.
117-
logger.SetupLogging(false, false, "", internal.Duration{Duration: 0}, internal.Size{Size: int64(0)}, 0)
117+
logger.SetupLogging(LogConfig{})
118118
log.Printf("I! Starting Telegraf %s", version)
119119

120120
// If no other options are specified, load the config file and run.
@@ -155,13 +155,16 @@ func runAgent(ctx context.Context,
155155
}
156156

157157
// Setup logging as configured.
158-
logger.SetupLogging(
159-
ag.Config.Agent.Debug || *fDebug,
160-
ag.Config.Agent.Quiet || *fQuiet,
161-
ag.Config.Agent.Logfile,
162-
ag.Config.Agent.LogfileRotationInterval,
163-
ag.Config.Agent.LogfileRotationMaxSize,
164-
ag.Config.Agent.LogfileRotationMaxArchives)
158+
logConfig := LogConfig{
159+
Debug: ag.Config.Agent.Debug || *fDebug,
160+
Quiet: ag.Config.Agent.Quiet || *fQuiet,
161+
Logfile: ag.Config.Agent.Logfile,
162+
RotationInterval: ag.Config.Agent.LogfileRotationInterval,
163+
RotationMaxSize: ag.Config.Agent.LogfileRotationMaxSize,
164+
RotationMaxArchives: ag.Config.Agent.LogfileRotationMaxArchives,
165+
}
166+
167+
logger.SetupLogging(logConfig)
165168

166169
if *fTest {
167170
return ag.Test(ctx)

docs/CONFIGURATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ The agent table configures Telegraf and the defaults used across all plugins.
147147
The log file max [size][]. Log files will be rotated when they exceed this size. Default is 0 => no rotation based on file size.
148148
- **logfile_rotation_max_archives**:
149149
Maximum number of archives (rotated) files to keep. Older log files are deleted first.
150-
This setting is only applicable if MaxAge and/or MaxSize settings have been specified (otherwise there is no rotation)
150+
This setting is only applicable if `logfile_rotation_interval` and/or `logfile_rotation_max_size` settings have been specified (otherwise there is no rotation)
151151
Default is 0 => all rotated files are deleted. Use -1 to keep all archives.
152152

153153
- **hostname**:

internal/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ var agentConfig = `
288288
## The log file max size. Log files will be rotated when they exceed this size. Default is 0 => no rotation based on file size.
289289
# logfile_rotation_max_size = "10 MB"
290290
## Maximum number of archives (rotated) files to keep. Older log files are deleted first.
291-
## This setting is only applicable if MaxAge and/or MaxSize settings have been specified (otherwise there is no rotation)
291+
## This setting is only applicable if logfile_rotation_interval and/or logfile_rotation_max_size settings have been specified (otherwise there is no rotation)
292292
## Default is 0 => all rotated files are deleted.
293293
## Use -1 to keep all archives.
294294
## Analogous to logrotate "rotate" setting http://man7.org/linux/man-pages/man8/logrotate.8.html

internal/rotate/file_writer.go

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"os"
88
"path/filepath"
99
"sort"
10-
"strconv"
1110
"strings"
1211
"sync"
1312
"time"
@@ -17,7 +16,7 @@ import (
1716
// the files it creates.
1817
const (
1918
FilePerm = os.FileMode(0644)
20-
DateFormat = "2006-01-02"
19+
DateFormat = "2006-01-02T150405"
2120
)
2221

2322
// FileWriter implements the io.Writer interface and writes to the
@@ -44,10 +43,15 @@ func NewFileWriter(filename string, interval time.Duration, maxSizeInBytes int64
4443
return openFile(filename)
4544
}
4645

47-
w := &FileWriter{filename: filename, interval: interval, maxSizeInBytes: maxSizeInBytes,
48-
maxArchives: maxArchives, filenameRotationTemplate: getFilenameRotationTemplate(filename)}
46+
w := &FileWriter{
47+
filename: filename,
48+
interval: interval,
49+
maxSizeInBytes: maxSizeInBytes,
50+
maxArchives: maxArchives,
51+
filenameRotationTemplate: getFilenameRotationTemplate(filename),
52+
}
4953

50-
if err := w.openCurrent(true); err != nil {
54+
if err := w.openCurrent(); err != nil {
5155
return nil, err
5256
}
5357

@@ -62,8 +66,8 @@ func getFilenameRotationTemplate(filename string) string {
6266
// Extract the file extension
6367
fileExt := filepath.Ext(filename)
6468
// Remove the file extension from the filename (if any)
65-
filenameWithoutExtension := strings.TrimSuffix(filename, fileExt)
66-
return filenameWithoutExtension + "-%s-%s" + fileExt
69+
stem := strings.TrimSuffix(filename, fileExt)
70+
return stem + ".%s" + fileExt
6771
}
6872

6973
// Write writes p to the current file, then checks to see if
@@ -90,7 +94,7 @@ func (w *FileWriter) Close() (err error) {
9094
defer w.Unlock()
9195

9296
// Rotate before closing
93-
if err = w.rotate(false); err != nil {
97+
if err = w.rotate(); err != nil {
9498
return err
9599
}
96100

@@ -101,21 +105,21 @@ func (w *FileWriter) Close() (err error) {
101105
return nil
102106
}
103107

104-
func (w *FileWriter) openCurrent(firstRun bool) (err error) {
105-
w.current, err = openFile(w.filename)
108+
func (w *FileWriter) openCurrent() (err error) {
109+
// In case ModTime() fails, we use time.Now()
106110
w.expireTime = time.Now().Add(w.interval)
107111
w.bytesWritten = 0
112+
w.current, err = openFile(w.filename)
108113

109114
if err != nil {
110115
return err
111116
}
112-
if !firstRun {
113-
return nil
114-
}
115117

116-
// Goal here is to rotate old pre-existing files. For that we use fileInfo.ModTime, instead of time.Now(), only when the FileWriter is created (firstRun)
117-
// Example: telegraf is restarted every 23 hours and the rotation interval is set to 24 hours. With time.now() as a reference we'd never rotate the file.
118-
// It's only necessary to use modtime when the filewriter is created, otherwise we assume that we've been continuously running, so time.now is fine.
118+
// Goal here is to rotate old pre-existing files.
119+
// For that we use fileInfo.ModTime, instead of time.Now().
120+
// Example: telegraf is restarted every 23 hours and
121+
// the rotation interval is set to 24 hours.
122+
// With time.now() as a reference we'd never rotate the file.
119123
if fileInfo, err := w.current.Stat(); err == nil {
120124
w.expireTime = fileInfo.ModTime().Add(w.interval)
121125
}
@@ -125,18 +129,20 @@ func (w *FileWriter) openCurrent(firstRun bool) (err error) {
125129
func (w *FileWriter) rotateIfNeeded() error {
126130
if (w.interval > 0 && time.Now().After(w.expireTime)) ||
127131
(w.maxSizeInBytes > 0 && w.bytesWritten >= w.maxSizeInBytes) {
128-
return w.rotate(true)
132+
if err := w.rotate(); err != nil {
133+
//Ignore rotation errors and keep the log open
134+
fmt.Printf("unable to rotate the file '%s', %s", w.filename, err.Error())
135+
}
136+
return w.openCurrent()
129137
}
130138
return nil
131139
}
132140

133-
func (w *FileWriter) rotate(createNewFile bool) (err error) {
141+
func (w *FileWriter) rotate() (err error) {
134142
if err = w.current.Close(); err != nil {
135143
return err
136144
}
137-
now := time.Now()
138-
// Use year-month-date for readability, unix time to make archive names unique
139-
rotatedFilename := fmt.Sprintf(w.filenameRotationTemplate, now.Format(DateFormat), strconv.FormatInt(now.UnixNano(), 10))
145+
rotatedFilename := fmt.Sprintf(w.filenameRotationTemplate, time.Now().Format(DateFormat))
140146
if err = os.Rename(w.filename, rotatedFilename); err != nil {
141147
return err
142148
}
@@ -145,9 +151,6 @@ func (w *FileWriter) rotate(createNewFile bool) (err error) {
145151
return err
146152
}
147153

148-
if createNewFile {
149-
return w.openCurrent(false)
150-
}
151154
return nil
152155
}
153156

@@ -158,7 +161,7 @@ func (w *FileWriter) purgeArchivesIfNeeded() (err error) {
158161
}
159162

160163
var matches []string
161-
if matches, err = filepath.Glob(fmt.Sprintf(w.filenameRotationTemplate, "*", "*")); err != nil {
164+
if matches, err = filepath.Glob(fmt.Sprintf(w.filenameRotationTemplate, "*")); err != nil {
162165
return err
163166
}
164167

internal/rotate/file_writer_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestFileWriter_SizeRotation(t *testing.T) {
5959
assert.Equal(t, 2, len(files))
6060
}
6161

62-
func TestFileWriter_deleteArchives(t *testing.T) {
62+
func TestFileWriter_DeleteArchives(t *testing.T) {
6363
tempDir, err := ioutil.TempDir("", "RotationDeleteArchives")
6464
require.NoError(t, err)
6565
maxSize := int64(5)
@@ -69,8 +69,13 @@ func TestFileWriter_deleteArchives(t *testing.T) {
6969

7070
_, err = writer.Write([]byte("First file"))
7171
require.NoError(t, err)
72+
// File names include the date with second precision
73+
// So, to force rotation with different file names
74+
// we need to wait
75+
time.Sleep(1 * time.Second)
7276
_, err = writer.Write([]byte("Second file"))
7377
require.NoError(t, err)
78+
time.Sleep(1 * time.Second)
7479
_, err = writer.Write([]byte("Third file"))
7580
require.NoError(t, err)
7681

@@ -106,5 +111,5 @@ func TestFileWriter_CloseRotates(t *testing.T) {
106111

107112
files, _ := ioutil.ReadDir(tempDir)
108113
assert.Equal(t, 1, len(files))
109-
assert.Regexp(t, "^test-[^\\.]+\\.log$", files[0].Name())
114+
assert.Regexp(t, "^test\\.[^\\.]+\\.log$", files[0].Name())
110115
}

logger/logger.go

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,32 @@ var prefixRegex = regexp.MustCompile("^[DIWE]!")
1818
// newTelegrafWriter returns a logging-wrapped writer.
1919
func newTelegrafWriter(w io.Writer) io.Writer {
2020
return &telegrafLog{
21-
writer: wlog.NewWriter(w),
21+
writer: wlog.NewWriter(w),
22+
internalWriter: w,
2223
}
2324
}
2425

26+
// LogConfig contains the log configuration settings
27+
type LogConfig struct {
28+
// will set the log level to DEBUG
29+
Debug bool
30+
//will set the log level to ERROR
31+
Quiet bool
32+
// will direct the logging output to a file. Empty string is
33+
// interpreted as stderr. If there is an error opening the file the
34+
// logger will fallback to stderr
35+
Logfile string
36+
// will rotate when current file at the specified time interval
37+
RotationInterval internal.Duration
38+
// will rotate when current file size exceeds this parameter.
39+
RotationMaxSize internal.Size
40+
// maximum rotated files to keep (older ones will be deleted)
41+
RotationMaxArchives int
42+
}
43+
2544
type telegrafLog struct {
26-
writer io.Writer
45+
writer io.Writer
46+
internalWriter io.Writer
2747
}
2848

2949
func (t *telegrafLog) Write(b []byte) (n int, err error) {
@@ -37,41 +57,32 @@ func (t *telegrafLog) Write(b []byte) (n int, err error) {
3757
}
3858

3959
func (t *telegrafLog) Close() error {
40-
closer, isCloser := t.writer.(io.Closer)
60+
closer, isCloser := t.internalWriter.(io.Closer)
4161
if !isCloser {
4262
return errors.New("the underlying writer cannot be closed")
4363
}
4464
return closer.Close()
4565
}
4666

4767
// SetupLogging configures the logging output.
48-
// debug will set the log level to DEBUG
49-
// quiet will set the log level to ERROR
50-
// logfile will direct the logging output to a file. Empty string is
51-
// interpreted as stderr. If there is an error opening the file the
52-
// logger will fallback to stderr.
53-
// logRotationInterval will rotate when current file at the specified time interval.
54-
// logRotationMaxSize will rotate when current file size exceeds this parameter.
55-
// logRotationMaxArchives maximum rotated files to keep (older ones will be deleted)
56-
func SetupLogging(debug, quiet bool, logfile string, logRotationInterval internal.Duration, logRotationMaxSize internal.Size, logRotationMaxArchives int) {
57-
setupLoggingAndReturnWriter(debug, quiet, logfile, logRotationInterval, logRotationMaxSize, logRotationMaxArchives)
68+
func SetupLogging(config LogConfig) {
69+
setupLoggingAndReturnWriter(config)
5870
}
5971

60-
func setupLoggingAndReturnWriter(debug, quiet bool, logfile string, logRotationInterval internal.Duration, logRotationMaxSize internal.Size,
61-
logRotationMaxArchives int) io.Writer {
72+
func setupLoggingAndReturnWriter(config LogConfig) io.Writer {
6273
log.SetFlags(0)
63-
if debug {
74+
if config.Debug {
6475
wlog.SetLevel(wlog.DEBUG)
6576
}
66-
if quiet {
77+
if config.Quiet {
6778
wlog.SetLevel(wlog.ERROR)
6879
}
6980

7081
var writer io.Writer
71-
if logfile != "" {
82+
if config.Logfile != "" {
7283
var err error
73-
if writer, err = rotate.NewFileWriter(logfile, logRotationInterval.Duration, logRotationMaxSize.Size, logRotationMaxArchives); err != nil {
74-
log.Printf("E! Unable to open %s (%s), using stderr", logfile, err)
84+
if writer, err = rotate.NewFileWriter(config.Logfile, config.RotationInterval.Duration, config.RotationMaxSize.Size, config.RotationMaxArchives); err != nil {
85+
log.Printf("E! Unable to open %s (%s), using stderr", config.Logfile, err)
7586
writer = os.Stderr
7687
}
7788
} else {

logger/logger_test.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ func TestWriteLogToFile(t *testing.T) {
1919
assert.NoError(t, err)
2020
defer func() { os.Remove(tmpfile.Name()) }()
2121

22-
SetupLogging(false, false, tmpfile.Name(), internal.Duration{Duration: 0}, internal.Size{Size: int64(0)}, 0)
22+
config := createBasicLogConfig(tmpfile.Name())
23+
SetupLogging(config)
2324
log.Printf("I! TEST")
2425
log.Printf("D! TEST") // <- should be ignored
2526

@@ -32,8 +33,9 @@ func TestDebugWriteLogToFile(t *testing.T) {
3233
tmpfile, err := ioutil.TempFile("", "")
3334
assert.NoError(t, err)
3435
defer func() { os.Remove(tmpfile.Name()) }()
35-
36-
SetupLogging(true, false, tmpfile.Name(), internal.Duration{Duration: 0}, internal.Size{Size: int64(0)}, 0)
36+
config := createBasicLogConfig(tmpfile.Name())
37+
config.Debug = true
38+
SetupLogging(config)
3739
log.Printf("D! TEST")
3840

3941
f, err := ioutil.ReadFile(tmpfile.Name())
@@ -45,8 +47,9 @@ func TestErrorWriteLogToFile(t *testing.T) {
4547
tmpfile, err := ioutil.TempFile("", "")
4648
assert.NoError(t, err)
4749
defer func() { os.Remove(tmpfile.Name()) }()
48-
49-
SetupLogging(false, true, tmpfile.Name(), internal.Duration{Duration: 0}, internal.Size{Size: int64(0)}, 0)
50+
config := createBasicLogConfig(tmpfile.Name())
51+
config.Quiet = true
52+
SetupLogging(config)
5053
log.Printf("E! TEST")
5154
log.Printf("I! TEST") // <- should be ignored
5255

@@ -59,8 +62,9 @@ func TestAddDefaultLogLevel(t *testing.T) {
5962
tmpfile, err := ioutil.TempFile("", "")
6063
assert.NoError(t, err)
6164
defer func() { os.Remove(tmpfile.Name()) }()
62-
63-
SetupLogging(true, false, tmpfile.Name(), internal.Duration{Duration: 0}, internal.Size{Size: int64(0)}, 0)
65+
config := createBasicLogConfig(tmpfile.Name())
66+
config.Debug = true
67+
SetupLogging(config)
6468
log.Printf("TEST")
6569

6670
f, err := ioutil.ReadFile(tmpfile.Name())
@@ -72,8 +76,9 @@ func TestWriteToTruncatedFile(t *testing.T) {
7276
tmpfile, err := ioutil.TempFile("", "")
7377
assert.NoError(t, err)
7478
defer func() { os.Remove(tmpfile.Name()) }()
75-
76-
SetupLogging(true, false, tmpfile.Name(), internal.Duration{Duration: 0}, internal.Size{Size: int64(0)}, 0)
79+
config := createBasicLogConfig(tmpfile.Name())
80+
config.Debug = true
81+
SetupLogging(config)
7782
log.Printf("TEST")
7883

7984
f, err := ioutil.ReadFile(tmpfile.Name())
@@ -94,8 +99,9 @@ func TestWriteToTruncatedFile(t *testing.T) {
9499
func TestWriteToFileInRotation(t *testing.T) {
95100
tempDir, err := ioutil.TempDir("", "LogRotation")
96101
require.NoError(t, err)
97-
writer := setupLoggingAndReturnWriter(false, false, filepath.Join(tempDir, "test.log"),
98-
internal.Duration{Duration: 0}, internal.Size{Size: int64(30)}, -1)
102+
config := createBasicLogConfig(filepath.Join(tempDir, "test.log"))
103+
config.RotationMaxSize = internal.Size{Size: int64(30)}
104+
writer := setupLoggingAndReturnWriter(config)
99105
// Close the writer here, otherwise the temp folder cannot be deleted because the current log file is in use.
100106
closer, isCloser := writer.(io.Closer)
101107
assert.True(t, isCloser)
@@ -116,3 +122,10 @@ func BenchmarkTelegrafLogWrite(b *testing.B) {
116122
w.Write(msg)
117123
}
118124
}
125+
126+
func createBasicLogConfig(filename string) LogConfig {
127+
return LogConfig{
128+
Logfile: filename,
129+
RotationMaxArchives: -1,
130+
}
131+
}

0 commit comments

Comments
 (0)