mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
@@ -15,6 +15,66 @@ Why do we need this?
|
||||
- Nowadays, the `--log-file` parameter is deprecated for Kubernetes components
|
||||
and should not be used anymore. `kube-log-runner` is a direct replacement.
|
||||
|
||||
## Flags
|
||||
|
||||
- -flush-interval
|
||||
The `-flush-interval` flag is a duration flag that specifies how frequently the log
|
||||
file content is flushed to disk. The default value is 0, meaning no periodic flushing.
|
||||
If set to a non-zero value, the log file is flushed at the specified interval.
|
||||
|
||||
Type: Duration
|
||||
|
||||
Default: 0 (flushing disabled)
|
||||
|
||||
Usage: When set to non-zero, the log file is flushed every specified interval.
|
||||
While this may not be necessary on Linux, on Windows, it ensures that recent log
|
||||
entries are written to disk in near real-time, which is particularly useful for
|
||||
users who need to monitor logs as they are generated without delay.
|
||||
|
||||
- -log-file-size
|
||||
The `-log-file-size` flag is an optional string flag that sets a size limit for
|
||||
the log file, triggering automatic log rotation when the specified size is reached.
|
||||
This is especially useful in production environments where the log file may become
|
||||
too large to view effectively.
|
||||
Beware that rotation can happen at arbitrary points in the byte stream emitted by the command.
|
||||
This can lead to splitting a log entry into two parts, with the first part in one file
|
||||
and the second part in the next file.
|
||||
|
||||
Type: String (expects a value in Resource.Quantity format, such as 10M or 500K)
|
||||
|
||||
Default: "0" (disabled, no automatic rotation of log files)
|
||||
|
||||
Usage: When set to a positive value, the log file will rotate upon reaching the specified
|
||||
size limit. The current log file’s contents will be saved to a backup file, and a new log
|
||||
file will be created at the path specified by the `-log-file` flag, ready for future log entries.
|
||||
|
||||
Backup File Naming Convention:
|
||||
`<original-file-name>-<timestamp><file-extension>`.
|
||||
* `<original-file-name>`: The name of the original log file, without the file extension.
|
||||
* `<timestamp>`: A timestamp is added to each backup file’s name to uniquely identify it
|
||||
based on the time it was created. The timestamp follows the format "20060102-150405".
|
||||
For example, a backup created on June 2, 2006, at 3:04:05 PM would include this timestamp.
|
||||
* `<file-extension>`: The original file’s extension (e.g., .log) remains unchanged.
|
||||
This naming convention ensures easy organization and retrieval of rotated log files based on their creation time.
|
||||
|
||||
- -log-file-age
|
||||
The `-log-file-age` flag is an optional time duration setting that defines how long
|
||||
old backup log files are retained. This flag is used alongside log rotation (enabled
|
||||
by setting a positive value for -log-file-size) to help manage storage by removing
|
||||
outdated backup logs.
|
||||
|
||||
Type: Duration
|
||||
|
||||
Default: 0 (disabled, no automatic deletion of backup files)
|
||||
|
||||
Usage: When -log-file-age is set to a positive duration (e.g., 24h for 24 hours)
|
||||
and log rotation is enabled, backup log files will be automatically deleted if
|
||||
the time when they were created (as encoded in the file name) is older than the
|
||||
specified duration from the current time.
|
||||
|
||||
This ensures that only recent backup logs are kept, preventing accumulation of old logs
|
||||
and reducing storage usage.
|
||||
|
||||
For example instead of running kube-apiserver like this:
|
||||
```bash
|
||||
"/bin/sh",
|
||||
@@ -49,6 +109,17 @@ kube-log-runner -log-file=/tmp/log echo "hello world"
|
||||
# Copy into log file and print to stdout (same as 2>&1 | tee -a /tmp/log).
|
||||
kube-log-runner -log-file=/tmp/log -also-stdout echo "hello world"
|
||||
|
||||
# Copy into log file and print to stdout (same as 2>&1 | tee -a /tmp/log),
|
||||
# will flush the logging file in 5s,
|
||||
# rotate the log file when its size exceedes 10 MB
|
||||
kube-log-runner -flush-interval=5s -log-file=/tmp/log -log-file-size=10M -also-stdout echo "hello world"
|
||||
|
||||
# Copy into log file and print to stdout (same as 2>&1 | tee -a /tmp/log),
|
||||
# will flush the logging file in 10s,
|
||||
# rotate the log file when its size exceedes 10 MB,
|
||||
# and clean up old rotated log files when their age are older than 168h (7 days)
|
||||
kube-log-runner -flush-interval=10s -log-file=/tmp/log -log-file-size=10M -log-file-age=168h -also-stdout echo "hello world"
|
||||
|
||||
# Redirect only stdout into log file (same as 1>>/tmp/log).
|
||||
kube-log-runner -log-file=/tmp/log -redirect-stderr=false echo "hello world"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package logrotation
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const timeLayout string = "20060102-150405"
|
||||
|
||||
type rotationFile struct {
|
||||
// required, the max size of the log file in bytes, 0 means no rotation
|
||||
maxSize int64
|
||||
// required, the max age of the log file, 0 means no cleanup
|
||||
maxAge time.Duration
|
||||
filePath string
|
||||
mut sync.Mutex
|
||||
file *os.File
|
||||
currentSize int64
|
||||
lasSyncTime time.Time
|
||||
flushInterval time.Duration
|
||||
}
|
||||
|
||||
func Open(filePath string, flushInterval time.Duration, maxSize int64, maxAge time.Duration) (io.WriteCloser, error) {
|
||||
w := &rotationFile{
|
||||
filePath: filePath,
|
||||
maxSize: maxSize,
|
||||
maxAge: maxAge,
|
||||
flushInterval: flushInterval,
|
||||
}
|
||||
|
||||
logFile, err := os.OpenFile(w.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.file = logFile
|
||||
|
||||
w.lasSyncTime = time.Now()
|
||||
|
||||
if w.maxSize > 0 {
|
||||
info, err := os.Stat(w.filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.currentSize = info.Size()
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface.
|
||||
func (w *rotationFile) Write(p []byte) (n int, err error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
n, err = w.file.Write(p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if w.flushInterval > 0 && time.Since(w.lasSyncTime) >= w.flushInterval {
|
||||
err = w.file.Sync()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.lasSyncTime = time.Now()
|
||||
}
|
||||
|
||||
if w.maxSize > 0 {
|
||||
w.currentSize += int64(len(p))
|
||||
|
||||
// if file size over maxsize rotate the log file
|
||||
if w.currentSize >= w.maxSize {
|
||||
err = w.rotate()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *rotationFile) rotate() error {
|
||||
// Get the file extension
|
||||
ext := filepath.Ext(w.filePath)
|
||||
|
||||
// Remove the extension from the filename
|
||||
pathWithoutExt := strings.TrimSuffix(w.filePath, ext)
|
||||
|
||||
rotateFilePath := pathWithoutExt + "-" + time.Now().Format(timeLayout) + ext
|
||||
|
||||
if w.filePath == rotateFilePath {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := w.file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Rename(w.filePath, rotateFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.file, err = os.OpenFile(w.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.currentSize = 0
|
||||
|
||||
if w.maxAge > 0 {
|
||||
go func() {
|
||||
err = w.clean(pathWithoutExt, ext)
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clean up the old log files in the format of
|
||||
// <basename>-<timestamp><ext>.
|
||||
// This should be safe enough to avoid false deletion.
|
||||
// This will work for multiple restarts of the same program.
|
||||
func (w *rotationFile) clean(pathWithoutExt string, ext string) error {
|
||||
ageTime := time.Now().Add(-w.maxAge)
|
||||
|
||||
directory := filepath.Dir(pathWithoutExt)
|
||||
basename := filepath.Base(pathWithoutExt) + "-"
|
||||
|
||||
dir, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = nil
|
||||
for _, v := range dir {
|
||||
if strings.HasPrefix(v.Name(), basename) && strings.HasSuffix(v.Name(), ext) {
|
||||
// Remove the prefix and suffix
|
||||
trimmed := strings.TrimPrefix(v.Name(), basename)
|
||||
trimmed = strings.TrimSuffix(trimmed, ext)
|
||||
|
||||
_, err = time.Parse(timeLayout, trimmed)
|
||||
if err == nil {
|
||||
info, errInfo := v.Info()
|
||||
if errInfo != nil {
|
||||
err = errInfo
|
||||
// Ignore the error while continue with the next clenup
|
||||
continue
|
||||
}
|
||||
|
||||
if ageTime.After(info.ModTime()) {
|
||||
err = os.Remove(filepath.Join(directory, v.Name()))
|
||||
if err != nil {
|
||||
// Ignore the error while continue with the next clenup
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *rotationFile) Close() error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
// Explicitly call file.Sync() to ensure data is written to disk
|
||||
err := w.file.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package logrotation
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLogrotationWrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flushInterval time.Duration
|
||||
maxSize int64
|
||||
maxAge time.Duration
|
||||
rotationExpected bool
|
||||
cleanupExpected bool
|
||||
}{
|
||||
{
|
||||
name: "no rotation",
|
||||
flushInterval: 0,
|
||||
maxSize: 0,
|
||||
maxAge: 10 * time.Hour,
|
||||
rotationExpected: false,
|
||||
cleanupExpected: false,
|
||||
},
|
||||
{
|
||||
name: "Rotation with flushInterval, maxSize, no cleanup",
|
||||
flushInterval: 5 * time.Second,
|
||||
maxSize: int64(1024 * 1024),
|
||||
maxAge: time.Duration(0),
|
||||
rotationExpected: true,
|
||||
cleanupExpected: false,
|
||||
},
|
||||
{
|
||||
name: "Rotation with maxSize and cleanup with maxAge",
|
||||
flushInterval: 0,
|
||||
maxSize: int64(1024 * 1024),
|
||||
maxAge: 24 * time.Hour,
|
||||
rotationExpected: true,
|
||||
cleanupExpected: true,
|
||||
},
|
||||
{
|
||||
name: "Rotation with flushInterval, maxSize and cleanup with maxAge",
|
||||
flushInterval: 10 * time.Second,
|
||||
maxSize: int64(1024 * 1024),
|
||||
maxAge: 24 * time.Hour,
|
||||
rotationExpected: true,
|
||||
cleanupExpected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "logrotation_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = os.RemoveAll(tmpDir); err != nil {
|
||||
t.Errorf("Failed to remove temp directory: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
logFilePath := filepath.Join(tmpDir, "test.log")
|
||||
flushInterval := tt.flushInterval
|
||||
maxSize := tt.maxSize
|
||||
maxAge := tt.maxAge
|
||||
|
||||
rotationFile, err := Open(logFilePath, flushInterval, maxSize, maxAge)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open RotationFile: %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
if err := rotationFile.Close(); err != nil {
|
||||
t.Errorf("Failed to close rotationFile: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
testData := []byte("This is a test log entry.")
|
||||
n, err := rotationFile.Write(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to RotationFile: %v", err)
|
||||
}
|
||||
if n != len(testData) {
|
||||
t.Errorf("Expected to write %d bytes, but wrote %d bytes", len(testData), n)
|
||||
}
|
||||
|
||||
// Check if data is written to the file
|
||||
content, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
if string(content) != string(testData) {
|
||||
t.Errorf("Expected log file content to be %q, but got %q", string(testData), string(content))
|
||||
}
|
||||
|
||||
// Write more data to trigger rotation
|
||||
largeData := make([]byte, 1024*1024) // 1 MB
|
||||
n, err = rotationFile.Write(largeData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write large data to RotationFile: %v", err)
|
||||
}
|
||||
if n != len(largeData) {
|
||||
t.Errorf("Expected to write %d bytes, but wrote %d bytes", len(largeData), n)
|
||||
}
|
||||
|
||||
// Check if rotation happened
|
||||
rotatedFiles, err := filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list rotated files: %v", err)
|
||||
}
|
||||
|
||||
if tt.rotationExpected {
|
||||
numberFiles := 1
|
||||
if len(rotatedFiles) != numberFiles {
|
||||
t.Errorf("Expected %d rotated log files, but found %d", numberFiles, len(rotatedFiles))
|
||||
}
|
||||
|
||||
// Check if new log file is created
|
||||
newContent, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read new log file: %v", err)
|
||||
}
|
||||
// Rotation after the write, new log file should be empty
|
||||
if len(newContent) != 0 {
|
||||
t.Errorf("Expected new log file content to be 0 bytes, but got %d bytes", len(newContent))
|
||||
}
|
||||
|
||||
// Check if content is append to the rotated log file
|
||||
content, err := os.ReadFile(rotatedFiles[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
if len(content) != len(testData)+len(largeData) {
|
||||
t.Errorf("Expected log file content to be %d bytes, but got %d bytes", len(testData)+len(largeData), len(content))
|
||||
}
|
||||
} else {
|
||||
numberFiles := 0
|
||||
if len(rotatedFiles) != numberFiles {
|
||||
t.Errorf("Expected %d rotated log files, but found %d", numberFiles, len(rotatedFiles))
|
||||
}
|
||||
|
||||
// Check if content is append to the existing log file
|
||||
content, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
if len(content) != len(testData)+len(largeData) {
|
||||
t.Errorf("Expected log file content to be %d bytes, but got %d bytes", len(testData)+len(largeData), len(content))
|
||||
}
|
||||
}
|
||||
|
||||
// Check if rotated file was able to be cleaned up
|
||||
if len(rotatedFiles) != 0 {
|
||||
err = os.Chtimes(rotatedFiles[0], time.Now(), time.Now().AddDate(0, 0, -10))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to change access time of rotated file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger another rotation and check if the old rotated file has been cleaned up
|
||||
time.Sleep(1 * time.Second)
|
||||
largeData2 := make([]byte, 1024*1025)
|
||||
_, err = rotationFile.Write(largeData2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to RotationFile: %v", err)
|
||||
}
|
||||
rotatedFiles2, err := filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list rotated files: %v", err)
|
||||
}
|
||||
|
||||
// Read the content of the log file
|
||||
content, err = os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if !tt.rotationExpected {
|
||||
if len(rotatedFiles2) != 0 {
|
||||
t.Errorf("Expected rotated log files to be 0, but found %d", len(rotatedFiles))
|
||||
}
|
||||
if len(content) != len(testData)+len(largeData)+len(largeData2) {
|
||||
t.Errorf("Expected log file content to be %d bytes, but got %d bytes", len(testData)+len(largeData)+len(largeData2), len(content))
|
||||
}
|
||||
} else {
|
||||
if tt.cleanupExpected {
|
||||
// clean up is performed in a async goroutine, wait for it to finish
|
||||
end := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(end) {
|
||||
rotatedFiles2, err = filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list rotated files: %v", err)
|
||||
}
|
||||
if len(rotatedFiles2) == 1 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
rotatedFiles2, err = filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list rotated files: %v", err)
|
||||
}
|
||||
if len(rotatedFiles2) != 1 {
|
||||
t.Errorf("Expected rotated log files to be 1, but found %d", len(rotatedFiles2))
|
||||
}
|
||||
if rotatedFiles[0] == rotatedFiles2[0] {
|
||||
t.Errorf("Expected rotated log files to be different, but found same")
|
||||
}
|
||||
} else if len(rotatedFiles2) != 2 {
|
||||
t.Errorf("Expected rotated log files to be 2, but found %d", len(rotatedFiles2))
|
||||
}
|
||||
// Rotation after the write, new log file should be empty
|
||||
if len(content) != 0 {
|
||||
t.Errorf("Expected new log file content to be 0 bytes, but got %d bytes", len(content))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package logrotation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RotationStub struct{}
|
||||
|
||||
func OpenStub(filePath string, flushInterval time.Duration, maxSize int64, maxAge time.Duration) (io.WriteCloser, error) {
|
||||
w := &RotationStub{}
|
||||
fmt.Printf("filePath: %s, flushInterval: %s, maxSize: %d, maxAge: %s", filePath, flushInterval, maxSize, maxAge)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// write func to satisfy io.writer interface
|
||||
func (w *RotationStub) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *RotationStub) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -26,23 +26,42 @@ import (
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/component-base/logs/kube-log-runner/internal/logrotation"
|
||||
)
|
||||
|
||||
type OpenFunc func(filePath string, flushInterval time.Duration, maxSize int64, maxAge time.Duration) (io.WriteCloser, error)
|
||||
|
||||
var (
|
||||
logFilePath = flag.String("log-file", "", "If non-empty, save stdout to this file")
|
||||
alsoToStdOut = flag.Bool("also-stdout", false, "useful with log-file, log to standard output as well as the log file")
|
||||
redirectStderr = flag.Bool("redirect-stderr", true, "treat stderr same as stdout")
|
||||
flushInterval *time.Duration
|
||||
logFilePath *string
|
||||
logFileSize *resource.QuantityValue
|
||||
logFileAge *time.Duration
|
||||
alsoToStdOut *bool
|
||||
redirectStderr *bool
|
||||
)
|
||||
|
||||
func initFlags() {
|
||||
logFileSize = &resource.QuantityValue{}
|
||||
flushInterval = flag.Duration("flush-interval", 0, "interval to flush log file to disk, default value 0, if non-zero, flush log file every interval")
|
||||
logFilePath = flag.String("log-file", "", "If non-empty, save stdout to this file")
|
||||
flag.Var(logFileSize, "log-file-size", "useful with log-file, in format of resource.quantity, default value 0, if non-zero, rotate log file when it reaches this size")
|
||||
logFileAge = flag.Duration("log-file-age", 0, "useful with log-file-size, in format of timeDuration, if non-zero, remove log files older than this duration")
|
||||
alsoToStdOut = flag.Bool("also-stdout", false, "useful with log-file, log to standard output as well as the log file")
|
||||
redirectStderr = flag.Bool("redirect-stderr", true, "treat stderr same as stdout")
|
||||
}
|
||||
|
||||
func main() {
|
||||
initFlags()
|
||||
flag.Parse()
|
||||
|
||||
if err := configureAndRun(); err != nil {
|
||||
if err := configureAndRun(logrotation.Open); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func configureAndRun() error {
|
||||
func configureAndRun(open OpenFunc) error {
|
||||
var (
|
||||
outputStream io.Writer = os.Stdout
|
||||
errStream io.Writer = os.Stderr
|
||||
@@ -53,8 +72,19 @@ func configureAndRun() error {
|
||||
return fmt.Errorf("not enough arguments to run")
|
||||
}
|
||||
|
||||
maxSize := logFileSize.Value()
|
||||
if maxSize < 0 {
|
||||
return fmt.Errorf("log-file-size must be non-negative quantity")
|
||||
}
|
||||
|
||||
// Check the time.duration is not negative
|
||||
if *logFileAge < 0 {
|
||||
return fmt.Errorf("log-file-age must be non-negative")
|
||||
}
|
||||
|
||||
if logFilePath != nil && *logFilePath != "" {
|
||||
logFile, err := os.OpenFile(*logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
logFile, err := open(*logFilePath, *flushInterval, maxSize, *logFileAge)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create log file %v: %w", *logFilePath, err)
|
||||
}
|
||||
@@ -95,10 +125,10 @@ func configureAndRun() error {
|
||||
// cmdInfo generates a useful look at what the command is for printing/debug.
|
||||
func cmdInfo(cmd *exec.Cmd) string {
|
||||
return fmt.Sprintf(
|
||||
`Command env: (log-file=%v, also-stdout=%v, redirect-stderr=%v)
|
||||
`Command env: (enable-flush=%v, log-file=%v, log-file-size=%v, log-file-age=%v, also-stdout=%v, redirect-stderr=%v)
|
||||
Run from directory: %v
|
||||
Executable path: %v
|
||||
Args (comma-delimited): %v`, *logFilePath, *alsoToStdOut, *redirectStderr,
|
||||
Args (comma-delimited): %v`, *flushInterval, *logFilePath, logFileSize.Value(), *logFileAge, *alsoToStdOut, *redirectStderr,
|
||||
cmd.Dir, cmd.Path, strings.Join(cmd.Args, ","),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/component-base/logs/kube-log-runner/internal/logrotation"
|
||||
)
|
||||
|
||||
// TestConfigureAndRun tests configureAndRun with various flag inputs.
|
||||
func TestConfigureAndRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commandLine []string
|
||||
outputWanted string
|
||||
}{
|
||||
{
|
||||
name: "No_log-file_flag",
|
||||
commandLine: []string{"echo", "hello"},
|
||||
outputWanted: "hello\n",
|
||||
},
|
||||
{
|
||||
name: "No_log-file_flag_with_extra_other_flags",
|
||||
commandLine: []string{"-flush-interval=5s", "-log-file-size=1Gi", "-log-file-age=24h30m", "echo", "hello"},
|
||||
outputWanted: "hello\n",
|
||||
},
|
||||
{
|
||||
name: "log-file_flag_only",
|
||||
commandLine: []string{"-log-file=test.log", "echo", "hello"},
|
||||
outputWanted: "filePath: test.log, flushInterval: 0s, maxSize: 0, maxAge: 0s",
|
||||
},
|
||||
{
|
||||
name: "log-file_flag_with_enable-flush_flag",
|
||||
commandLine: []string{"-log-file=test.log", "-flush-interval=10s", "echo", "hello"},
|
||||
outputWanted: "filePath: test.log, flushInterval: 10s, maxSize: 0, maxAge: 0s",
|
||||
},
|
||||
{
|
||||
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag",
|
||||
commandLine: []string{"-log-file=test.log", "-flush-interval=1m", "-log-file-size=15M", "echo", "hello"},
|
||||
outputWanted: "filePath: test.log, flushInterval: 1m0s, maxSize: 15000000, maxAge: 0s",
|
||||
},
|
||||
{
|
||||
name: "Invalid_CPU_format_log-file-size_flag",
|
||||
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=125m", "echo", "hello"},
|
||||
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1, maxAge: 0s",
|
||||
},
|
||||
{
|
||||
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag_and_log-file-age_flag",
|
||||
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=1Gi", "-log-file-age=24h30m", "echo", "hello"},
|
||||
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1073741824, maxAge: 24h30m0s",
|
||||
},
|
||||
{
|
||||
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag_and_log-file-age_flag_and_also-stdout_flag",
|
||||
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=1500", "-log-file-age=24h30m", "-also-stdout", "echo", "hello"},
|
||||
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1500, maxAge: 24h30m0shello\n",
|
||||
},
|
||||
{
|
||||
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag_and_log-file-age_flag_and_also-stdout_flag_and_redirect-stderr",
|
||||
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=1500", "-log-file-age=24h30m", "-also-stdout", "-redirect-stderr=false", "echo", "hello"},
|
||||
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1500, maxAge: 24h30m0shello\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resetFlags()
|
||||
err := flag.CommandLine.Parse(tt.commandLine)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse commandline: %v", err)
|
||||
return
|
||||
}
|
||||
// Capture the log output and compare
|
||||
originalStdout := os.Stdout
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create pipe: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
err = reader.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to close pipe reader: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
os.Stdout = writer
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
err = configureAndRun(logrotation.OpenStub)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to close pipe writer: %v", err)
|
||||
return
|
||||
}
|
||||
// <-done
|
||||
|
||||
_, err = buf.ReadFrom(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read from pipe: %v", err)
|
||||
return
|
||||
}
|
||||
output := buf.String()
|
||||
if output != tt.outputWanted {
|
||||
t.Errorf("Expected '%s' but got '%s'", tt.outputWanted, output)
|
||||
}
|
||||
|
||||
// Restore original os.Stdout
|
||||
os.Stdout = originalStdout
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegativeConfigureAndRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commandLine []string
|
||||
errorWanted string
|
||||
}{
|
||||
{
|
||||
name: "Empty_commandline",
|
||||
commandLine: []string{},
|
||||
errorWanted: "not enough arguments to run",
|
||||
},
|
||||
{
|
||||
name: "Invalid_log-file-size_flag",
|
||||
commandLine: []string{"-log-file-size=10MM", "test"},
|
||||
errorWanted: "invalid value \"10MM\" for flag -log-file-size: unable to parse quantity's suffix",
|
||||
},
|
||||
{
|
||||
name: "Negative_log-file-size_flag",
|
||||
commandLine: []string{"-log-file-size=-10M", "test"},
|
||||
errorWanted: "log-file-size must be non-negative quantity",
|
||||
},
|
||||
{
|
||||
name: "Invalid_log-file-age_flag",
|
||||
commandLine: []string{"-log-file-age=-10h", "test"},
|
||||
errorWanted: "log-file-age must be non-negative",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resetFlags()
|
||||
err := flag.CommandLine.Parse(tt.commandLine)
|
||||
if err != nil {
|
||||
if err.Error() != tt.errorWanted {
|
||||
t.Errorf("Expected error: %v, got: %v", tt.errorWanted, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = configureAndRun(logrotation.OpenStub)
|
||||
if err == nil || err.Error() != tt.errorWanted {
|
||||
t.Errorf("Expected error: %v, got: %v", tt.errorWanted, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func resetFlags() {
|
||||
// Reinitialize the flag to the default value
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
initFlags()
|
||||
}
|
||||
Reference in New Issue
Block a user