caddyserver/caddy · error

invalid log level

Error message

invalid log level: %v

What it means

parseLevel first runs the level string through the replacer (placeholders like {$ENV_VAR}); if replacement itself errors — unset env var without a default, or malformed placeholder syntax — the level is invalid before it is even matched against the known level names.

Solutions

  1. Export the referenced environment variable in the environment Caddy runs in.
  2. Provide an inline default: level {$LOG_LEVEL:INFO}.
  3. Hardcode the level while debugging to isolate the placeholder problem.

Example fix

# before
export LOG_LEVEL=   # unset/empty in env
global { log { level {$LOG_LEVEL} } }

# after
global { log { level {$LOG_LEVEL:INFO} } }
Defensive patterns

Strategy: validation

Validate before calling

// resolve level placeholders yourself before loading
func resolvedLevel(envLookup func(string) (string, bool), raw string) (string, error) {
    if v, ok := envLookup(strings.Trim(raw, "{}$")); ok && v != "" {
        return v, nil
    }
    if def, ok := strings.CutPrefix(strings.Trim(raw, "{}$"), ""); ok && def != "" {
        return def, nil
    }
    return "", fmt.Errorf("level placeholder %s unresolvable", raw)
}

Try / catch

if err := caddy.Validate(cfg); err != nil {
    if strings.Contains(err.Error(), "invalid log level") {
        // placeholder failed to resolve — check env vars and defaults
    }
    return err
}

Prevention

When it happens

Trigger: level (or stacktrace level) set to {$LOG_LEVEL} where LOG_LEVEL is unset and no default suffix is provided; placeholder braces unbalanced; placeholder containing an invalid default after the colon.

Common situations: Docker/systemd deployments where the env var is injected in one environment but missing in another; CI running caddy validate without the production env; copy-pasted configs referencing variables that were never exported.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/cb0f7ae5ae3f567f. Report an issue: GitHub.

Appendix: source

Thrown at logging.go:755

	if IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) {
		// if interactive terminal, make output more human-readable by default
		encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
			encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))
		}
		if coloringEnabled {
			encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
		}

		return zapcore.NewConsoleEncoder(encCfg)
	}
	return zapcore.NewJSONEncoder(encCfg)
}

func parseLevel(levelInput string) (zapcore.LevelEnabler, error) {
	repl := NewReplacer()
	level, err := repl.ReplaceOrErr(levelInput, true, true)
	if err != nil {
		return nil, fmt.Errorf("invalid log level: %v", err)
	}
	level = strings.ToLower(level)

	// set up the log level
	switch level {
	case "debug":
		return zapcore.DebugLevel, nil
	case "", "info":
		return zapcore.InfoLevel, nil
	case "warn":
		return zapcore.WarnLevel, nil
	case "error":
		return zapcore.ErrorLevel, nil
	case "panic":
		return zapcore.PanicLevel, nil
	case "fatal":
		return zapcore.FatalLevel, nil
	default:

View on GitHub (pinned to 50e54ee279)