Introspection & Source Tracking¶
Confii tracks the origin of every configuration value -- which file it came from, which loader loaded it, how many times it was overridden, and the full override history. This makes debugging configuration issues straightforward.
Explain¶
Explain returns detailed resolution information for a key as a map. It tells you the current value, its source, the loader type, the active environment, and the override count.
Example output:
map[string]any{
"exists": true,
"key": "database.host",
"value": "prod-db.example.com",
"current_value": "prod-db.example.com",
"source": "config.yaml",
"loader_type": "YAMLLoader",
"environment": "production",
"override_count": 2,
"override_history": []map[string]any{
{"value": "localhost", "source": "base.yaml", "loader_type": "YAMLLoader"},
{"value": "staging-db", "source": "staging.yaml", "loader_type": "YAMLLoader"},
},
}
Override history requires debug mode
The override_history field is only populated when WithDebugMode(true) is enabled. Without it, you still get the override count but not the history.
Secret-backed values are redacted
Explain, Schema, GenerateDocs, GetSourceInfo,
GetOverrideHistory, GetConflicts, and the detached tracker returned by
SourceTracker replace values derived from secret references with a
redaction marker. sensitive_paths or WithSensitivePaths extends this
protection to values produced by custom loaders and hooks, including plain
literals that were never resolved from anywhere.
PrintDebugInfo and ExportDebugReport are deliberately outside this
list. They report what a layer contributed, verbatim, and their own
documentation calls their output sensitive operational data. A saved
version record is outside it too: it stores the materialized value
alongside its sensitivity metadata. Sensitivity
survives materialization, runtime mutation,
version persistence, and rollback. PrintDebugInfo and
ExportDebugReport are explicit sensitive-data operations and may contain
values; protect their output accordingly.
When the key does not exist, Explain returns the available keys to help with typos:
info := cfg.Explain("databse.host") // typo
// info["exists"] == false
// info["available_keys"] == ["database.host", "database.port", ...]
Schema¶
Schema returns type information for a key -- its Go type, current value, and whether it exists.
schema := cfg.Schema("database.port")
fmt.Println(schema)
// map[key:database.port exists:true value:5432 type:int]
Layers¶
Layers returns the source stack showing each loader, its type, and the keys it contributed. This visualizes the order of precedence.
layers := cfg.Layers()
for _, layer := range layers {
fmt.Printf("Source: %s (%s) - %d keys\n",
layer["source"],
layer["loader_type"],
layer["key_count"],
)
}
Example output:
Source: base.yaml (YAMLLoader) - 12 keys
Source: prod.yaml (YAMLLoader) - 5 keys
Source: APP (EnvironmentLoader) - 3 keys
Debug Mode¶
Enable WithDebugMode(true) to track the full override history for every key. Without it, Confii tracks sources and override counts but not the history chain.
cfg, err := confii.NewWithContext[any](ctx,
confii.WithLoaders(
loader.NewYAML("base.yaml"),
loader.NewYAML("prod.yaml"),
),
confii.WithDebugMode(true),
)
Performance
Debug mode stores additional data for every override. In production with many sources and keys, this increases memory usage. Enable it only when needed, or use it in staging/development environments.
Source Tracking Methods¶
SourcePlan¶
Returns the environment model and ordered loader roles from the last successful
load. Unlike general override history, Conflicts is limited to keys written
by both environment models in explicit hybrid mode.
plan := cfg.SourcePlan()
fmt.Println(plan.Strategy)
for _, layer := range plan.Layers {
fmt.Printf("%d. %s [%s] (%d keys)\n",
layer.Order, layer.Source, layer.Role, len(layer.Keys))
}
for _, conflict := range plan.Conflicts {
fmt.Printf("%s: %v; last writer=%s\n",
conflict.Key, conflict.Sources, conflict.LastWriter)
}
The returned plan is defensively copied. Mutating its layer keys or conflict source slices cannot affect the live configuration or subsequent calls.
SourceTracker¶
SourceTracker() returns a detached, redacted tracker snapshot for advanced
queries. Calling its mutation methods changes only that detached tracker and
cannot alter Config introspection or published configuration state.
GetSourceInfo¶
Returns the full SourceInfo struct for a key:
info := cfg.GetSourceInfo("database.host")
if info != nil {
fmt.Printf("Key: %s\n", info.Key)
fmt.Printf("Value: %v\n", info.Value)
fmt.Printf("Source: %s\n", info.SourceFile)
fmt.Printf("Loader: %s\n", info.LoaderType)
fmt.Printf("Overrides: %d\n", info.OverrideCount)
fmt.Printf("Timestamp: %s\n", info.Timestamp)
}
The SourceInfo struct:
type SourceInfo struct {
Key string `json:"key"`
Value any `json:"value"`
SourceFile string `json:"source_file"`
LoaderType string `json:"loader_type"`
LineNumber int `json:"line_number,omitempty"`
Environment string `json:"environment,omitempty"`
OverrideCount int `json:"override_count"`
History []OverrideEntry `json:"history,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
LineNumber is the one-based line in the source that supplied the effective
value. Only loaders implementing confii.PositionalLoader can report it. Among
the bundled loaders that is YAML; JSON, TOML, and INI decode through libraries
that expose no per-key location, so their keys report 0 — meaning unknown,
not line zero. When a later layer overrides a key, the line follows the layer
that supplied the winning value, and a layer that cannot report positions
leaves an already-known line intact.
To report positions from a custom loader, implement the optional interface
alongside Load:
// Positions returns the one-based line of each key, by dotted path.
func (l *MyLoader) Positions() map[string]int { return l.positions }
Confii type-asserts for it after every load; loaders that do not implement it are unaffected.
GetOverrideHistory¶
Returns the override history chain for a key. Requires WithDebugMode(true).
history := cfg.GetOverrideHistory("database.host")
for i, entry := range history {
fmt.Printf("%d. value=%v from=%s via=%s\n",
i+1, entry.Value, entry.Source, entry.LoaderType)
}
GetConflicts¶
Returns all keys that have been overridden at least once:
conflicts := cfg.GetConflicts()
for key, info := range conflicts {
fmt.Printf("%s was overridden %d time(s), final source: %s\n",
key, info.OverrideCount, info.SourceFile)
}
Auditing overrides
Use GetConflicts in CI to detect unexpected overrides. If a key you expect to come from base.yaml is being overridden by another source, this tells you immediately.
GetSourceStatistics¶
Returns aggregated statistics about configuration sources:
stats := cfg.GetSourceStatistics()
fmt.Println(stats)
// map[
// total_keys:15
// sources:map[base.yaml:10 prod.yaml:5]
// loader_types:map[YAMLLoader:15]
// total_overrides:3
// ]
FindKeysFromSource¶
Returns all keys that originated from sources matching a substring pattern:
keys := cfg.FindKeysFromSource("prod.yaml")
fmt.Println(keys)
// [database.host cache.ttl logging.level]
Debug Reports¶
PrintDebugInfo¶
Returns a human-readable debug report for a specific key, or all keys if the argument is empty:
Output:
Key: database.host
Value: prod-db.example.com
Source: prod.yaml
Loader: YAMLLoader
Overrides: 1
History:
1. localhost (from base.yaml via YAMLLoader)
ExportDebugReport¶
Exports the full source tracking data as a JSON file:
The output is a JSON object keyed by config path, with each value being the full SourceInfo struct.
Documentation Generation¶
GenerateDocs¶
Generate a reference document from the current config state:
Output:
Full Example¶
package main
import (
"context"
"fmt"
"log"
confii "github.com/confiify/confii-go/v2"
"github.com/confiify/confii-go/v2/loader"
)
func main() {
ctx := context.Background()
cfg, err := confii.NewWithContext[any](ctx,
confii.WithLoaders(
loader.NewYAML("base.yaml"),
loader.NewYAML("prod.yaml"),
loader.NewEnvironment("APP"),
),
confii.WithEnv("production"),
confii.WithDebugMode(true),
)
if err != nil {
log.Fatal(err)
}
// Explain a key
info := cfg.Explain("database.host")
fmt.Printf("database.host comes from: %s (overridden %v times)\n",
info["source"], info["override_count"])
// Show layers
for _, layer := range cfg.Layers() {
fmt.Printf(" %s (%s): %d keys\n",
layer["source"], layer["loader_type"], layer["key_count"])
}
// Find conflicts
for key, si := range cfg.GetConflicts() {
fmt.Printf(" conflict: %s overridden %d time(s)\n", key, si.OverrideCount)
}
// Export full report
_ = cfg.ExportDebugReport("debug-report.json")
fmt.Println("Debug report saved to debug-report.json")
}