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
+}