{"record":{"id":"cb0f7ae5ae3f567f","repo":"caddyserver/caddy","slug":"invalid-log-level-v","errorCode":null,"errorMessage":"invalid log level: %v","messagePattern":"invalid log level: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"logging.go","lineNumber":755,"sourceCode":"\tif IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) {\n\t\t// if interactive terminal, make output more human-readable by default\n\t\tencCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {\n\t\t\tencoder.AppendString(ts.UTC().Format(\"2006/01/02 15:04:05.000\"))\n\t\t}\n\t\tif coloringEnabled {\n\t\t\tencCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder\n\t\t}\n\n\t\treturn zapcore.NewConsoleEncoder(encCfg)\n\t}\n\treturn zapcore.NewJSONEncoder(encCfg)\n}\n\nfunc parseLevel(levelInput string) (zapcore.LevelEnabler, error) {\n\trepl := NewReplacer()\n\tlevel, err := repl.ReplaceOrErr(levelInput, true, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid log level: %v\", err)\n\t}\n\tlevel = strings.ToLower(level)\n\n\t// set up the log level\n\tswitch level {\n\tcase \"debug\":\n\t\treturn zapcore.DebugLevel, nil\n\tcase \"\", \"info\":\n\t\treturn zapcore.InfoLevel, nil\n\tcase \"warn\":\n\t\treturn zapcore.WarnLevel, nil\n\tcase \"error\":\n\t\treturn zapcore.ErrorLevel, nil\n\tcase \"panic\":\n\t\treturn zapcore.PanicLevel, nil\n\tcase \"fatal\":\n\t\treturn zapcore.FatalLevel, nil\n\tdefault:","sourceCodeStart":737,"sourceCodeEnd":773,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/logging.go#L737-L773","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Export the referenced environment variable in the environment Caddy runs in.","Provide an inline default: level {$LOG_LEVEL:INFO}.","Hardcode the level while debugging to isolate the placeholder problem."],"exampleFix":"# before\nexport LOG_LEVEL=   # unset/empty in env\nglobal { log { level {$LOG_LEVEL} } }\n\n# after\nglobal { log { level {$LOG_LEVEL:INFO} } }","handlingStrategy":"validation","validationCode":"// resolve level placeholders yourself before loading\nfunc resolvedLevel(envLookup func(string) (string, bool), raw string) (string, error) {\n    if v, ok := envLookup(strings.Trim(raw, \"{}$\")); ok && v != \"\" {\n        return v, nil\n    }\n    if def, ok := strings.CutPrefix(strings.Trim(raw, \"{}$\"), \"\"); ok && def != \"\" {\n        return def, nil\n    }\n    return \"\", fmt.Errorf(\"level placeholder %s unresolvable\", raw)\n}","typeGuard":null,"tryCatchPattern":"if err := caddy.Validate(cfg); err != nil {\n    if strings.Contains(err.Error(), \"invalid log level\") {\n        // placeholder failed to resolve — check env vars and defaults\n    }\n    return err\n}","preventionTips":["Always add defaults to placeholders: {$LOG_LEVEL:INFO}.","Run 'caddy validate' in the same environment (env vars) as production.","Keep an env-var manifest per deployment and diff it during releases."],"tags":["logging","environment-variables","placeholders","validation"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}