@marcel91200 / caddy Mirror · read-only / commits / d588d5c8
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 03:30 UTC

cmd: unbuffer default logger on startup failure (#8180)

* cmd: unbuffer default logger on startup failure Restore default logger to unbuffered console logger on cmdRun error so WrapCommandFuncForCobra can output the error. * add regression tests

Indra Gunawan committed Oct 10, 2026 at 05:22 UTC d588d5c8f0a48a44851a218bbfc3d6edc0e4be17
4 files changed +149 -9
cmd/commandfuncs.go
+3 -8
@@ -193,9 +193,9 @@ func cmdRun(fl Flags) (int, error) {
193 // so that we can hold onto logs until after
194 // the config is loaded (or fails to load)
195 // so that we can write the logs to the user's
196 - // configured output. we must be sure to flush
197 - // on any error before the config is loaded.
198 - logger, defaultLogger, logBuffer := caddy.BufferedLog()
196 + // configured output.
197 + logger, defaultLogger, _ := caddy.BufferedLog()
198 + defer caddy.UnbufferDefaultLogger(defaultLogger)
199
200 undoMaxProcs := setResourceLimits(logger)
201 defer undoMaxProcs()
@@ -214,7 +214,6 @@ func cmdRun(fl Flags) (int, error) {
214 // load all additional envs as soon as possible
215 err := handleEnvFileFlag(fl)
216 if err != nil {
217 - logBuffer.FlushTo(defaultLogger)
217 return caddy.ExitCodeFailedStartup, err
218 }
219
@@ -232,7 +231,6 @@ func cmdRun(fl Flags) (int, error) {
231 logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
232 resumeFlag = false
233 } else if err != nil {
235 - logBuffer.FlushTo(defaultLogger)
234 return caddy.ExitCodeFailedStartup, err
235 } else {
236 if configFlag == "" {
@@ -252,7 +250,6 @@ func cmdRun(fl Flags) (int, error) {
250 if !resumeFlag {
251 config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
252 if err != nil {
255 - logBuffer.FlushTo(defaultLogger)
253 return caddy.ExitCodeFailedStartup, err
254 }
255 }
@@ -283,7 +280,6 @@ func cmdRun(fl Flags) (int, error) {
280 // run the initial config
281 err = caddy.Load(config, true)
282 if err != nil {
286 - logBuffer.FlushTo(defaultLogger)
283 return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
284 }
285 // release the reference to the config so it can be GC'd
@@ -295,7 +291,6 @@ func cmdRun(fl Flags) (int, error) {
291 // also clear our ref to the buffer so it can get GC'd
292 logger = caddy.Log()
293 defaultLogger = nil //nolint:ineffassign,wastedassign
298 - logBuffer = nil //nolint:wastedassign,ineffassign
294 logger.Info("serving initial configuration")
295
296 // if we are to report to another process the successful start
cmd/commands_test.go
+55
@@ -11,10 +11,12 @@ import (
11 "strings"
12 "testing"
13
14 + "github.com/spf13/cobra"
15 "github.com/spf13/pflag"
16
17 "github.com/caddyserver/caddy/v2"
18 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
19 + "github.com/caddyserver/caddy/v2/internal"
20 )
21
22 // newFmtFlags builds a Flags value wired up the same way the fmt cobra command does.
@@ -309,3 +311,56 @@ func TestCommandsAreAvailable(t *testing.T) {
311 t.Errorf("expected %v, got %v", expectedCommandNames, commandNames)
312 }
313 }
314 +
315 +func TestCmdRun_UnbufferOnFailure(t *testing.T) {
316 + fs := pflag.NewFlagSet("run", pflag.ContinueOnError)
317 + fs.StringP("config", "c", "", "")
318 + fs.StringP("adapter", "a", "", "")
319 + fs.BoolP("resume", "r", false, "")
320 + fs.BoolP("environ", "e", false, "")
321 + fs.BoolP("watch", "w", false, "")
322 + fs.String("pidfile", "", "")
323 + fs.String("pingback", "", "")
324 + fs.StringSlice("envfile", nil, "")
325 + _ = fs.Set("config", filepath.Join(t.TempDir(), "nonexistent"))
326 +
327 + code, err := cmdRun(Flags{fs})
328 + if err == nil {
329 + t.Fatal("expected error from cmdRun with nonexistent config, got nil")
330 + }
331 + if code != caddy.ExitCodeFailedStartup {
332 + t.Errorf("expected exit code %d, got %d", caddy.ExitCodeFailedStartup, code)
333 + }
334 +
335 + // Regression test for #8180: caddy.Log() must not remain buffered after cmdRun failure
336 + if _, ok := caddy.Log().Core().(internal.LogBufferCoreInterface); ok {
337 + t.Error("caddy.Log() was left in a buffered state after cmdRun failed; should be unbuffered")
338 + }
339 +}
340 +
341 +func TestWrapCommandFuncForCobra_RunFailure(t *testing.T) {
342 + fs := pflag.NewFlagSet("run", pflag.ContinueOnError)
343 + fs.StringP("config", "c", "", "")
344 + fs.StringP("adapter", "a", "", "")
345 + fs.BoolP("resume", "r", false, "")
346 + fs.BoolP("environ", "e", false, "")
347 + fs.BoolP("watch", "w", false, "")
348 + fs.String("pidfile", "", "")
349 + fs.String("pingback", "", "")
350 + fs.StringSlice("envfile", nil, "")
351 + _ = fs.Set("config", filepath.Join(t.TempDir(), "nonexistent"))
352 +
353 + cmd := &cobra.Command{Use: "run"}
354 + cmd.Flags().AddFlagSet(fs)
355 +
356 + runE := WrapCommandFuncForCobra(cmdRun)
357 + err := runE(cmd, nil)
358 + if err == nil {
359 + t.Fatal("expected error from wrapped cmdRun, got nil")
360 + }
361 +
362 + // Regression test for #8180: caddy.Log() must not remain buffered after WrapCommandFuncForCobra
363 + if _, ok := caddy.Log().Core().(internal.LogBufferCoreInterface); ok {
364 + t.Error("caddy.Log() was left in a buffered state after WrapCommandFuncForCobra; should be unbuffered")
365 + }
366 +}
logging.go
+13
@@ -797,6 +797,19 @@ func BufferedLog() (*zap.Logger, *zap.Logger, *internal.LogBufferCore) {
797 return defaultLogger.logger, origLogger, bufferCore
798 }
799
800 +// UnbufferDefaultLogger restores the default logger's underlying zap.Logger to orig
801 +// if the default logger is still pointing to a buffer core, flushing any remaining buffered logs.
802 +func UnbufferDefaultLogger(orig *zap.Logger) {
803 + defaultLoggerMu.Lock()
804 + defer defaultLoggerMu.Unlock()
805 + if defaultLogger != nil && defaultLogger.logger != nil {
806 + if bufferCore, ok := defaultLogger.logger.Core().(*internal.LogBufferCore); ok {
807 + bufferCore.FlushTo(orig)
808 + defaultLogger.logger = orig
809 + }
810 + }
811 +}
812 +
813 var (
814 coloringEnabled = os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "xterm-mono"
815 defaultLogger, _ = newDefaultProductionLog()
logging_test.go
+78 -1
@@ -14,7 +14,16 @@
14
15 package caddy
16
17 -import "testing"
17 +import (
18 + "bytes"
19 + "strings"
20 + "testing"
21 +
22 + "go.uber.org/zap"
23 + "go.uber.org/zap/zapcore"
24 +
25 + "github.com/caddyserver/caddy/v2/internal"
26 +)
27
28 func TestCustomLog_loggerAllowed(t *testing.T) {
29 type fields struct {
@@ -104,3 +113,71 @@ func TestCustomLog_loggerAllowed(t *testing.T) {
113 })
114 }
115 }
116 +
117 +func TestUnbufferDefaultLogger(t *testing.T) {
118 + defaultLoggerMu.RLock()
119 + origDefault := defaultLogger
120 + defaultLoggerMu.RUnlock()
121 + t.Cleanup(func() {
122 + defaultLoggerMu.Lock()
123 + defaultLogger = origDefault
124 + defaultLoggerMu.Unlock()
125 + })
126 +
127 + // Set up an observing logger to verify buffer flushing and post-unbuffer logging
128 + buf := new(bytes.Buffer)
129 + destCore := zapcore.NewCore(
130 + zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
131 + zapcore.AddSync(buf),
132 + zapcore.InfoLevel,
133 + )
134 + destLogger := zap.New(destCore)
135 +
136 + defaultLoggerMu.Lock()
137 + defaultLogger = &defaultCustomLog{
138 + CustomLog: &CustomLog{},
139 + logger: destLogger,
140 + }
141 + defaultLoggerMu.Unlock()
142 +
143 + // 1. Engage buffered logging
144 + bufferedLogger, origLogger, _ := BufferedLog()
145 +
146 + // Verify default logger is now using the buffer core
147 + if _, ok := Log().Core().(*internal.LogBufferCore); !ok {
148 + t.Fatal("expected default logger core to be *internal.LogBufferCore")
149 + }
150 +
151 + // 2. Log while buffered
152 + bufferedLogger.Info("early startup log message")
153 +
154 + // Messages should not be written to destination yet while buffered
155 + if buf.Len() > 0 {
156 + t.Fatalf("expected destination buffer to be empty while buffered, got: %s", buf.String())
157 + }
158 +
159 + // 3. Unbuffer the default logger
160 + UnbufferDefaultLogger(origLogger)
161 +
162 + // Verify default logger core is no longer LogBufferCore
163 + if _, ok := Log().Core().(*internal.LogBufferCore); ok {
164 + t.Fatal("expected default logger core to no longer be *internal.LogBufferCore after UnbufferDefaultLogger")
165 + }
166 +
167 + // Verify early buffered logs were flushed to destination logger
168 + if !strings.Contains(buf.String(), "early startup log message") {
169 + t.Fatalf("expected destination buffer to contain early startup log message, got: %s", buf.String())
170 + }
171 +
172 + // 4. Log after unbuffering: should go directly to destination logger, not swallowed
173 + Log().Error("startup failed error")
174 + if !strings.Contains(buf.String(), "startup failed error") {
175 + t.Fatalf("expected destination buffer to contain error message, got: %s", buf.String())
176 + }
177 +
178 + // 5. Calling UnbufferDefaultLogger again when not buffered should be a safe no-op
179 + UnbufferDefaultLogger(origLogger)
180 + if _, ok := Log().Core().(*internal.LogBufferCore); ok {
181 + t.Fatal("unexpected buffer core after second unbuffer call")
182 + }
183 +}