Skip to content

Commit 6e0bc77

Browse files
javicrespoHelge Waastad
authored andcommitted
Add in process log rotation (influxdata#5778)
1 parent 722b892 commit 6e0bc77

File tree

7 files changed

+428
-32
lines changed

7 files changed

+428
-32
lines changed

cmd/telegraf/telegraf.go

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

121121
// If no other options are specified, load the config file and run.
@@ -156,11 +156,16 @@ func runAgent(ctx context.Context,
156156
}
157157

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

165170
if *fTest {
166171
return ag.Test(ctx)

docs/CONFIGURATION.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ The agent table configures Telegraf and the defaults used across all plugins.
141141
Run telegraf in quiet mode (error log messages only).
142142
- **logfile**:
143143
Specify the log file name. The empty string means to log to stderr.
144+
- **logfile_rotation_interval**:
145+
Log file rotation time [interval][], e.g. "1d" means logs will rotated every day. Default is 0 => no rotation based on time.
146+
- **logfile_rotation_max_size**:
147+
The log file max [size][]. Log files will be rotated when they exceed this size. Default is 0 => no rotation based on file size.
148+
- **logfile_rotation_max_archives**:
149+
Maximum number of archives (rotated) files to keep. Older log files are deleted first.
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)
151+
Default is 0 => all rotated files are deleted. Use -1 to keep all archives.
144152

145153
- **hostname**:
146154
Override default hostname, if empty use os.Hostname()

internal/config/config.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ type AgentConfig struct {
145145
// Logfile specifies the file to send logs to
146146
Logfile string
147147

148+
// The log file rotation interval
149+
LogfileRotationInterval internal.Duration
150+
151+
// The log file max size. Logs will rotated when they exceed this size.
152+
LogfileRotationMaxSize internal.Size
153+
154+
// The max number of log archives to keep
155+
LogfileRotationMaxArchives int
156+
148157
// Quiet is the option for running in quiet mode
149158
Quiet bool
150159
Hostname string
@@ -273,6 +282,17 @@ var agentConfig = `
273282
quiet = false
274283
## Specify the log file name. The empty string means to log to stderr.
275284
logfile = ""
285+
## Rotation settings, only applicable when log file name is specified.
286+
## Log file rotation time interval, e.g. "1d" means logs will rotated every day. Default is 0 => no rotation based on time.
287+
# logfile_rotation_interval = "1d"
288+
## The log file max size. Log files will be rotated when they exceed this size. Default is 0 => no rotation based on file size.
289+
# logfile_rotation_max_size = "10 MB"
290+
## Maximum number of archives (rotated) files to keep. Older log files are deleted first.
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)
292+
## Default is 0 => all rotated files are deleted.
293+
## Use -1 to keep all archives.
294+
## Analogous to logrotate "rotate" setting http://man7.org/linux/man-pages/man8/logrotate.8.html
295+
# logfile_rotation_max_archives = 0
276296
277297
## Override default hostname, if empty use os.Hostname()
278298
hostname = ""

internal/rotate/file_writer.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package rotate
2+
3+
// Rotating things
4+
import (
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"sort"
10+
"strconv"
11+
"strings"
12+
"sync"
13+
"time"
14+
)
15+
16+
// FilePerm defines the permissions that Writer will use for all
17+
// the files it creates.
18+
const (
19+
FilePerm = os.FileMode(0644)
20+
DateFormat = "2006-01-02"
21+
)
22+
23+
// FileWriter implements the io.Writer interface and writes to the
24+
// filename specified.
25+
// Will rotate at the specified interval and/or when the current file size exceeds maxSizeInBytes
26+
// At rotation time, current file is renamed and a new file is created.
27+
// If the number of archives exceeds maxArchives, older files are deleted.
28+
type FileWriter struct {
29+
filename string
30+
filenameRotationTemplate string
31+
current *os.File
32+
interval time.Duration
33+
maxSizeInBytes int64
34+
maxArchives int
35+
expireTime time.Time
36+
bytesWritten int64
37+
sync.Mutex
38+
}
39+
40+
// NewFileWriter creates a new file writer.
41+
func NewFileWriter(filename string, interval time.Duration, maxSizeInBytes int64, maxArchives int) (io.WriteCloser, error) {
42+
if interval == 0 && maxSizeInBytes <= 0 {
43+
// No rotation needed so a basic io.Writer will do the trick
44+
return openFile(filename)
45+
}
46+
47+
w := &FileWriter{
48+
filename: filename,
49+
interval: interval,
50+
maxSizeInBytes: maxSizeInBytes,
51+
maxArchives: maxArchives,
52+
filenameRotationTemplate: getFilenameRotationTemplate(filename),
53+
}
54+
55+
if err := w.openCurrent(); err != nil {
56+
return nil, err
57+
}
58+
59+
return w, nil
60+
}
61+
62+
func openFile(filename string) (*os.File, error) {
63+
return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, FilePerm)
64+
}
65+
66+
func getFilenameRotationTemplate(filename string) string {
67+
// Extract the file extension
68+
fileExt := filepath.Ext(filename)
69+
// Remove the file extension from the filename (if any)
70+
stem := strings.TrimSuffix(filename, fileExt)
71+
return stem + ".%s-%s" + fileExt
72+
}
73+
74+
// Write writes p to the current file, then checks to see if
75+
// rotation is necessary.
76+
func (w *FileWriter) Write(p []byte) (n int, err error) {
77+
w.Lock()
78+
defer w.Unlock()
79+
if n, err = w.current.Write(p); err != nil {
80+
return 0, err
81+
}
82+
w.bytesWritten += int64(n)
83+
84+
if err = w.rotateIfNeeded(); err != nil {
85+
return 0, err
86+
}
87+
88+
return n, nil
89+
}
90+
91+
// Close closes the current file. Writer is unusable after this
92+
// is called.
93+
func (w *FileWriter) Close() (err error) {
94+
w.Lock()
95+
defer w.Unlock()
96+
97+
// Rotate before closing
98+
if err = w.rotate(); err != nil {
99+
return err
100+
}
101+
102+
if err = w.current.Close(); err != nil {
103+
return err
104+
}
105+
w.current = nil
106+
return nil
107+
}
108+
109+
func (w *FileWriter) openCurrent() (err error) {
110+
// In case ModTime() fails, we use time.Now()
111+
w.expireTime = time.Now().Add(w.interval)
112+
w.bytesWritten = 0
113+
w.current, err = openFile(w.filename)
114+
115+
if err != nil {
116+
return err
117+
}
118+
119+
// Goal here is to rotate old pre-existing files.
120+
// For that we use fileInfo.ModTime, instead of time.Now().
121+
// Example: telegraf is restarted every 23 hours and
122+
// the rotation interval is set to 24 hours.
123+
// With time.now() as a reference we'd never rotate the file.
124+
if fileInfo, err := w.current.Stat(); err == nil {
125+
w.expireTime = fileInfo.ModTime().Add(w.interval)
126+
}
127+
return nil
128+
}
129+
130+
func (w *FileWriter) rotateIfNeeded() error {
131+
if (w.interval > 0 && time.Now().After(w.expireTime)) ||
132+
(w.maxSizeInBytes > 0 && w.bytesWritten >= w.maxSizeInBytes) {
133+
if err := w.rotate(); err != nil {
134+
//Ignore rotation errors and keep the log open
135+
fmt.Printf("unable to rotate the file '%s', %s", w.filename, err.Error())
136+
}
137+
return w.openCurrent()
138+
}
139+
return nil
140+
}
141+
142+
func (w *FileWriter) rotate() (err error) {
143+
if err = w.current.Close(); err != nil {
144+
return err
145+
}
146+
147+
// Use year-month-date for readability, unix time to make the file name unique with second precision
148+
now := time.Now()
149+
rotatedFilename := fmt.Sprintf(w.filenameRotationTemplate, now.Format(DateFormat), strconv.FormatInt(now.Unix(), 10))
150+
if err = os.Rename(w.filename, rotatedFilename); err != nil {
151+
return err
152+
}
153+
154+
if err = w.purgeArchivesIfNeeded(); err != nil {
155+
return err
156+
}
157+
158+
return nil
159+
}
160+
161+
func (w *FileWriter) purgeArchivesIfNeeded() (err error) {
162+
if w.maxArchives == -1 {
163+
//Skip archiving
164+
return nil
165+
}
166+
167+
var matches []string
168+
if matches, err = filepath.Glob(fmt.Sprintf(w.filenameRotationTemplate, "*", "*")); err != nil {
169+
return err
170+
}
171+
172+
//if there are more archives than the configured maximum, then purge older files
173+
if len(matches) > w.maxArchives {
174+
//sort files alphanumerically to delete older files first
175+
sort.Strings(matches)
176+
for _, filename := range matches[:len(matches)-w.maxArchives] {
177+
if err = os.Remove(filename); err != nil {
178+
return err
179+
}
180+
}
181+
}
182+
return nil
183+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package rotate
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestFileWriter_NoRotation(t *testing.T) {
15+
tempDir, err := ioutil.TempDir("", "RotationNo")
16+
require.NoError(t, err)
17+
writer, err := NewFileWriter(filepath.Join(tempDir, "test"), 0, 0, 0)
18+
require.NoError(t, err)
19+
defer func() { writer.Close(); os.RemoveAll(tempDir) }()
20+
21+
_, err = writer.Write([]byte("Hello World"))
22+
require.NoError(t, err)
23+
_, err = writer.Write([]byte("Hello World 2"))
24+
require.NoError(t, err)
25+
files, _ := ioutil.ReadDir(tempDir)
26+
assert.Equal(t, 1, len(files))
27+
}
28+
29+
func TestFileWriter_TimeRotation(t *testing.T) {
30+
tempDir, err := ioutil.TempDir("", "RotationTime")
31+
require.NoError(t, err)
32+
interval, _ := time.ParseDuration("1s")
33+
writer, err := NewFileWriter(filepath.Join(tempDir, "test"), interval, 0, -1)
34+
require.NoError(t, err)
35+
defer func() { writer.Close(); os.RemoveAll(tempDir) }()
36+
37+
_, err = writer.Write([]byte("Hello World"))
38+
require.NoError(t, err)
39+
time.Sleep(1 * time.Second)
40+
_, err = writer.Write([]byte("Hello World 2"))
41+
require.NoError(t, err)
42+
files, _ := ioutil.ReadDir(tempDir)
43+
assert.Equal(t, 2, len(files))
44+
}
45+
46+
func TestFileWriter_SizeRotation(t *testing.T) {
47+
tempDir, err := ioutil.TempDir("", "RotationSize")
48+
require.NoError(t, err)
49+
maxSize := int64(9)
50+
writer, err := NewFileWriter(filepath.Join(tempDir, "test.log"), 0, maxSize, -1)
51+
require.NoError(t, err)
52+
defer func() { writer.Close(); os.RemoveAll(tempDir) }()
53+
54+
_, err = writer.Write([]byte("Hello World"))
55+
require.NoError(t, err)
56+
_, err = writer.Write([]byte("World 2"))
57+
require.NoError(t, err)
58+
files, _ := ioutil.ReadDir(tempDir)
59+
assert.Equal(t, 2, len(files))
60+
}
61+
62+
func TestFileWriter_DeleteArchives(t *testing.T) {
63+
tempDir, err := ioutil.TempDir("", "RotationDeleteArchives")
64+
require.NoError(t, err)
65+
maxSize := int64(5)
66+
writer, err := NewFileWriter(filepath.Join(tempDir, "test.log"), 0, maxSize, 2)
67+
require.NoError(t, err)
68+
defer func() { writer.Close(); os.RemoveAll(tempDir) }()
69+
70+
_, err = writer.Write([]byte("First file"))
71+
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)
76+
_, err = writer.Write([]byte("Second file"))
77+
require.NoError(t, err)
78+
time.Sleep(1 * time.Second)
79+
_, err = writer.Write([]byte("Third file"))
80+
require.NoError(t, err)
81+
82+
files, _ := ioutil.ReadDir(tempDir)
83+
assert.Equal(t, 3, len(files))
84+
85+
for _, tempFile := range files {
86+
var bytes []byte
87+
var err error
88+
path := filepath.Join(tempDir, tempFile.Name())
89+
if bytes, err = ioutil.ReadFile(path); err != nil {
90+
t.Error(err.Error())
91+
return
92+
}
93+
contents := string(bytes)
94+
95+
if contents != "" && contents != "Second file" && contents != "Third file" {
96+
t.Error("Should have deleted the eldest log file")
97+
return
98+
}
99+
}
100+
}
101+
102+
func TestFileWriter_CloseRotates(t *testing.T) {
103+
tempDir, err := ioutil.TempDir("", "RotationClose")
104+
require.NoError(t, err)
105+
defer os.RemoveAll(tempDir)
106+
maxSize := int64(9)
107+
writer, err := NewFileWriter(filepath.Join(tempDir, "test.log"), 0, maxSize, -1)
108+
require.NoError(t, err)
109+
110+
writer.Close()
111+
112+
files, _ := ioutil.ReadDir(tempDir)
113+
assert.Equal(t, 1, len(files))
114+
assert.Regexp(t, "^test\\.[^\\.]+\\.log$", files[0].Name())
115+
}

0 commit comments

Comments
 (0)