Skip to content

Commit de1f71e

Browse files
committed
Handle log streams correctly for default stderr, file-only, and file+stderr logging.
1 parent 9c9cdce commit de1f71e

File tree

4 files changed

+306
-2
lines changed

4 files changed

+306
-2
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package flags
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/spf13/pflag"
10+
logsapi "k8s.io/component-base/logs/api/v1"
11+
)
12+
13+
// getFlagString is a small helper to read a string flag from the given FlagSet.
14+
func getFlagString(fs *pflag.FlagSet, name string) string {
15+
if f := fs.Lookup(name); f != nil {
16+
return f.Value.String()
17+
}
18+
return ""
19+
}
20+
21+
// getFlagBool is a small helper to read a bool flag from the given FlagSet.
22+
func getFlagBool(fs *pflag.FlagSet, name string) bool {
23+
if f := fs.Lookup(name); f != nil {
24+
val, _ := fs.GetBool(name)
25+
return val
26+
}
27+
return false
28+
}
29+
30+
// ComputeLoggingOptions computes logsapi.LoggingOptions based on klog-related flags present in fs.
31+
//
32+
// Semantics:
33+
// - By default (no log-file OR logtostderr=true), logs go to os.Stderr.
34+
// - If --log-file is set AND --logtostderr=false, logs go to the file.
35+
// - If --alsologtostderr=true, logs also go to os.Stderr
36+
func ComputeLoggingOptions(fs *pflag.FlagSet) (*logsapi.LoggingOptions, error) {
37+
logFilePath := getFlagString(fs, "log-file")
38+
logToStderr := getFlagBool(fs, "logtostderr")
39+
alsoLogToStderr := getFlagBool(fs, "alsologtostderr")
40+
41+
// default: both to stderr
42+
var infoW, errW io.Writer = os.Stderr, os.Stderr
43+
44+
if logFilePath != "" && !logToStderr {
45+
dir := filepath.Dir(logFilePath)
46+
if err := os.MkdirAll(dir, 0o755); err != nil {
47+
return nil, fmt.Errorf("failed to create log directory %q: %w", dir, err)
48+
}
49+
f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to open log file %q: %w", logFilePath, err)
52+
}
53+
54+
// Primary output is now the file
55+
infoW = f
56+
errW = f
57+
58+
if alsoLogToStderr {
59+
infoW = io.MultiWriter(f, os.Stderr)
60+
errW = io.MultiWriter(f, os.Stderr)
61+
}
62+
}
63+
64+
return &logsapi.LoggingOptions{
65+
ErrorStream: errW,
66+
InfoStream: infoW,
67+
}, nil
68+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package flags
2+
3+
import (
4+
"io"
5+
"os"
6+
"path/filepath"
7+
"reflect"
8+
"strings"
9+
"testing"
10+
11+
"github.com/spf13/pflag"
12+
)
13+
14+
// --------------------- HELPERS ---------------------
15+
16+
func newTestFlagSet(t *testing.T, logFile string, logToStderr, alsoLogToStderr bool) *pflag.FlagSet {
17+
t.Helper()
18+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
19+
fs.String("log-file", "", "")
20+
fs.Bool("logtostderr", true, "")
21+
fs.Bool("alsologtostderr", false, "")
22+
23+
if logFile != "" {
24+
if err := fs.Set("log-file", logFile); err != nil {
25+
t.Fatalf("set log-file: %v", err)
26+
}
27+
}
28+
if err := fs.Set("logtostderr", boolToString(logToStderr)); err != nil {
29+
t.Fatalf("set logtostderr: %v", err)
30+
}
31+
if err := fs.Set("alsologtostderr", boolToString(alsoLogToStderr)); err != nil {
32+
t.Fatalf("set alsologtostderr: %v", err)
33+
}
34+
return fs
35+
}
36+
37+
func boolToString(b bool) string {
38+
if b {
39+
return "true"
40+
}
41+
return "false"
42+
}
43+
44+
// assertStreams checks the actual streams against expected values
45+
func assertStreams(t *testing.T, actualInfo, actualErr any, expectedInfo, expectedErr any) {
46+
t.Helper()
47+
48+
check := func(name string, actual, expected any) {
49+
if expected == nil {
50+
if actual != nil {
51+
t.Fatalf("%s: expected nil, got %T", name, actual)
52+
}
53+
return
54+
}
55+
56+
if reflect.TypeOf(actual) != reflect.TypeOf(expected) {
57+
t.Fatalf("%s type mismatch: expected %T, got %T", name, expected, actual)
58+
}
59+
60+
if f, ok := expected.(*os.File); ok {
61+
if actual.(*os.File) != f {
62+
t.Fatalf("%s pointer mismatch: expected same *os.File", name)
63+
}
64+
}
65+
if expected == os.Stderr {
66+
if actual != os.Stderr {
67+
t.Fatalf("%s mismatch: expected os.Stderr", name)
68+
}
69+
}
70+
71+
// For io.MultiWriter, type check is sufficient
72+
}
73+
74+
check("InfoStream", actualInfo, expectedInfo)
75+
check("ErrorStream", actualErr, expectedErr)
76+
}
77+
78+
// --------------------- TESTS ---------------------
79+
80+
func TestComputeLoggingOptions_DefaultToStderr(t *testing.T) {
81+
fs := newTestFlagSet(t, "", true, false)
82+
opts, err := ComputeLoggingOptions(fs)
83+
if err != nil {
84+
t.Fatalf("ComputeLoggingOptions error: %v", err)
85+
}
86+
assertStreams(t, opts.InfoStream, opts.ErrorStream, os.Stderr, os.Stderr)
87+
}
88+
89+
func TestComputeLoggingOptions_LogFileIgnoredWhenLogToStderrTrue(t *testing.T) {
90+
tmp := t.TempDir()
91+
logPath := filepath.Join(tmp, "subdir", "ca.log")
92+
93+
fs := newTestFlagSet(t, logPath, true, false)
94+
opts, err := ComputeLoggingOptions(fs)
95+
if err != nil {
96+
t.Fatalf("ComputeLoggingOptions error: %v", err)
97+
}
98+
99+
if _, statErr := os.Stat(logPath); !os.IsNotExist(statErr) {
100+
t.Fatalf("expected no log file created at %q, statErr=%v", logPath, statErr)
101+
}
102+
103+
assertStreams(t, opts.InfoStream, opts.ErrorStream, os.Stderr, os.Stderr)
104+
}
105+
106+
func TestComputeLoggingOptions_LogFileOnly(t *testing.T) {
107+
tmp := t.TempDir()
108+
logPath := filepath.Join(tmp, "nested", "onlyfile.log")
109+
110+
fs := newTestFlagSet(t, logPath, false, false)
111+
opts, err := ComputeLoggingOptions(fs)
112+
if err != nil {
113+
t.Fatalf("ComputeLoggingOptions error: %v", err)
114+
}
115+
116+
if _, statErr := os.Stat(logPath); statErr != nil {
117+
t.Fatalf("expected log file created at %q, err=%v", logPath, statErr)
118+
}
119+
120+
// Streams should be the same file writer
121+
file := opts.InfoStream.(*os.File)
122+
assertStreams(t, opts.InfoStream, opts.ErrorStream, file, file)
123+
124+
// Write and verify content
125+
msg := "hello-file-only\n"
126+
if _, err := file.Write([]byte(msg)); err != nil {
127+
t.Fatalf("write to file failed: %v", err)
128+
}
129+
data, err := os.ReadFile(logPath)
130+
if err != nil {
131+
t.Fatalf("read log file failed: %v", err)
132+
}
133+
if !strings.Contains(string(data), msg) {
134+
t.Fatalf("log file does not contain expected content")
135+
}
136+
}
137+
138+
func TestComputeLoggingOptions_LogFileAlsoToStderr(t *testing.T) {
139+
tmp := t.TempDir()
140+
logPath := filepath.Join(tmp, "withstderr", "combo.log")
141+
142+
fs := newTestFlagSet(t, logPath, false, true)
143+
opts, err := ComputeLoggingOptions(fs)
144+
if err != nil {
145+
t.Fatalf("ComputeLoggingOptions error: %v", err)
146+
}
147+
148+
// File must exist
149+
if _, statErr := os.Stat(logPath); statErr != nil {
150+
t.Fatalf("expected log file created at %q, err=%v", logPath, statErr)
151+
}
152+
153+
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY, 0o644)
154+
if err != nil {
155+
t.Fatalf("failed to open log file to build expected writer: %v", err)
156+
}
157+
defer file.Close()
158+
159+
expectedWriter := io.MultiWriter(os.Stderr, file)
160+
161+
// Assert streams against expected MultiWriter
162+
assertStreams(t, opts.InfoStream, opts.ErrorStream, expectedWriter, expectedWriter)
163+
164+
// Write and verify content appears in the file
165+
msg := "hello-also-stderr\n"
166+
if _, err := opts.InfoStream.Write([]byte(msg)); err != nil {
167+
t.Fatalf("write to InfoStream failed: %v", err)
168+
}
169+
data, err := os.ReadFile(logPath)
170+
if err != nil {
171+
t.Fatalf("read log file failed: %v", err)
172+
}
173+
if !strings.Contains(string(data), msg) {
174+
t.Fatalf("log file does not contain expected content")
175+
}
176+
}
177+
178+
func TestComputeLoggingOptions_CreateDirError(t *testing.T) {
179+
tmp := t.TempDir()
180+
notADirPath := filepath.Join(tmp, "notadir")
181+
if err := os.WriteFile(notADirPath, []byte("x"), 0o644); err != nil {
182+
t.Fatalf("prepare file failed: %v", err)
183+
}
184+
logPath := filepath.Join(notADirPath, "child", "ca.log")
185+
186+
fs := newTestFlagSet(t, logPath, false, false)
187+
_, err := ComputeLoggingOptions(fs)
188+
if err == nil || !strings.Contains(err.Error(), "failed to create log directory") {
189+
t.Fatalf("expected create dir error, got: %v", err)
190+
}
191+
}
192+
193+
func TestComputeLoggingOptions_OpenFileError(t *testing.T) {
194+
tmp := t.TempDir()
195+
dirAsFile := filepath.Join(tmp, "adir")
196+
if err := os.MkdirAll(dirAsFile, 0o755); err != nil {
197+
t.Fatalf("prepare dir failed: %v", err)
198+
}
199+
200+
fs := newTestFlagSet(t, dirAsFile, false, false)
201+
_, err := ComputeLoggingOptions(fs)
202+
if err == nil || !strings.Contains(err.Error(), "failed to open log file") {
203+
t.Fatalf("expected open file error, got: %v", err)
204+
}
205+
}
206+
207+
func TestComputeLoggingOptions_WriterInterfaceUsable(t *testing.T) {
208+
tmp := t.TempDir()
209+
logPath := filepath.Join(tmp, "writeusable", "ca.log")
210+
211+
fs := newTestFlagSet(t, logPath, false, false)
212+
opts, err := ComputeLoggingOptions(fs)
213+
if err != nil {
214+
t.Fatalf("ComputeLoggingOptions error: %v", err)
215+
}
216+
217+
file := opts.ErrorStream.(*os.File)
218+
n, err := file.Write([]byte("err-stream\n"))
219+
if err != nil || n == 0 {
220+
t.Fatalf("failed writing via ErrorStream: n=%d err=%v", n, err)
221+
}
222+
223+
data, err := os.ReadFile(logPath)
224+
if err != nil {
225+
t.Fatalf("read log file failed: %v", err)
226+
}
227+
if !strings.Contains(string(data), "err-stream") {
228+
t.Fatalf("log file does not contain expected err-stream content")
229+
}
230+
}

cluster-autoscaler/go.sum

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,4 +675,4 @@ sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxO
675675
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
676676
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
677677
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
678-
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
678+
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

cluster-autoscaler/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,13 @@ func main() {
370370
}
371371

372372
logs.InitLogs()
373-
if err := logsapi.ValidateAndApply(loggingConfig, featureGate); err != nil {
373+
374+
opts, err := flags.ComputeLoggingOptions(pflag.CommandLine)
375+
if err != nil {
376+
klog.Fatalf("Failed to configure logging: %v", err)
377+
}
378+
379+
if err := logsapi.ValidateAndApplyWithOptions(loggingConfig, opts, featureGate); err != nil {
374380
klog.Fatalf("Failed to validate and apply logging configuration: %v", err)
375381
}
376382

0 commit comments

Comments
 (0)