caddyserver/caddy · error

invalid log level: %v

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.

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)

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.