A config layer is a black box exactly when you most need to trust it (“why is
Port 8080 and not what’s in my file?”). ConfigureWithReport fills the struct
and returns a structured, per-field account of resolution: every source tried,
in the order tried, which one won, and the raw and converted values.
report.String() renders a focused trace — not every field, only what’s
actionable: the primary source, the count of defaults, and the fields that were
overridden away from the primary config or that are secret:
cfg, report, err := gostructor.ConfigureWithReport(&Config{},
gostructor.WithSources(gostructor.Env(), gostructor.JSONFile("config.json")))
if err != nil {
log.Fatal(err)
}
fmt.Println(report.String())
// Configuring main.Config: 4 fields
// [Primary Source] json (loaded 2 fields)
//
// Overrides & Secrets:
// Port int ⇐ env (override) = 9090
// APIKey string ⇐ env = ••••oken (secret)
report.Provenance() // map[string]string: field -> winning source name
report.PrimarySource() // "json": the non-default source that won the most fields
report.Fields is the machine view ([]FieldResolution, each with its
Attempts and an IsSecret flag); report.String() renders the focused
summary above. The plain Configure builds no report, so there’s zero overhead
when you don’t ask for one. WithTrace() logs the same report through your
WithLogger without changing call sites.
Flag a sensitive field with gos:"secret" and its value is masked everywhere
it would otherwise print — the report, trace logs, and *ConvertError
messages — while the real value still lands on your struct:
type Config struct {
APIKey string `cfg:"apiKey,env:APP_API_KEY" gos:"secret"`
}
// Default fully redacts ("••••••"). Override to reveal, e.g., the last 4 chars:
gostructor.WithMasker(func(_ gostructor.FieldContext, v any) string {
s, _ := v.(string)
if len(s) >= 4 {
return "••••" + s[len(s)-4:]
}
return "••••"
})
See examples/observability for a runnable end-to-end demo.