Awesome
The simplest config loader for Go
konf offers an(other) opinion on how Go programs can read configuration without becoming coupled to a particular configuration source.
Features
- konf.Unmarshal for reading configuration to any type of object.
- konf.OnChange for registering callbacks while configuration changes.
- konf.Explain for understanding where the configuration is loaded from.
- Various providers for loading configuration from major clouds, AWS, Azure, and GCP with Notifier for notifying the changes of configuration.
- Zero dependencies in core module which supports loading configuration from environment variables,flags, and embed file system.
Benchmarks
The following benchmarks compare the performance of konf with spf13/viper and knadh/koanf, which are inspiration of konf.
Unmarshal (ns/op) | Unmarshal (allocs/op) | Get1 (ns/op) | Get (allocs/op) | |
---|---|---|---|---|
Konf | 41.09 | 4 | 16.71 | 1 |
Viper | 614.8 | 22 | 104.9 | 3 |
Koanf | 15949 | 657 | 7.898 | 1 |
Usage
Somewhere, early in an application's life, it will make a decision about which configuration source(s) (implementation) it actually wants to use. Something like:
//go:embed config
var config embed.FS
func main() {
var config konf.Config
// Load configuration from embed file system.
if err := config.Load(fs.New(config, "config/config.json")); err != nil {
// Handle error here.
}
// Load configuration from environment variables.
if err := config.Load(env.New(env.WithPrefix("server"))); err != nil {
// Handle error here.
}
// Watch the changes of configuration.
go func() {
if err := config.Watch(ctx); err != nil {
// Handle error here.
}
}()
konf.SetDefault(config)
// ... other setup code ...
}
Outside of this early setup, no other packages need to know about the choice of
configuration source(s). They read configuration in terms of functions in package konf
:
func (app *appObject) Run() {
// Server configuration with default values.
serverConfig := struct {
Host string
Port int
}{
Host: "localhost",
Port: "8080",
}
// Read the server configuration.
if err := konf.Unmarshal("server", &serverConfig); err != nil {
// Handle error here.
}
// Register callbacks while server configuration changes.
konf.OnChange(func() {
// Reconfig the application object.
}, "server")
// ... use cfg in app code ...
}
Design
It contains two APIs with two different sets of users:
- The
Config
type is intended for application authors. It provides a relatively small API which can be used everywhere you want to read configuration. It defers the actual configuration loading to theLoader
interface. - The
Loader
andWatcher
interface is intended for configuration source library implementers. They are pure interfaces which can be implemented to provide the actual configuration.
This decoupling allows application developers to write code in terms of *konf.Config
while the configuration source(s) is managed "up stack" (e.g. in or near main()
).
Application developers can then switch configuration sources(s) as necessary.
Change Notification
The providers for loading configuration from clouds periodically poll the configuration source.
It also supports watching the changes of configuration using corresponding notifier.
For example, the sns
notifier notifies the changes of appconfig
and s3
provider:
notifier := sns.NewNotifier("konf-test")
notifier.Register(s3Loader, appConfigLoader)
go func() {
if err := notifier.Start(ctx); err != nil {
// handle error
}
}()
Understand the configuration
While the configuration is loaded from multiple sources, static like environments or dynamic like AWS AppConfig,
it's hard to understand where a final value comes from. The Config.Explain
method provides information
about how Config resolve each value from loaders for the given path. One example explanation is like:
config.nest has value [map] is loaded by map.
Here are other value(loader)s:
- env(env)
Even more, the Config.Explain
blurs sensitive information (e.g. password, secret, api keys).
Observability
For watching the changes of configuration, it uses slog.Default()
for logging. You can change the logger
via option konf.WithLogHandler
. Furthermore, you also can register onStatus via option konf.WithOnStatus
to monitor the status of configuration loading/watching, e.g. recording metrics.
Configuration Providers
There are providers for the following configuration sources.
Loader | Load From | Watch Changes | Notifier |
---|---|---|---|
env | environment variables | ||
fs | fs.FS | ||
file | file | ✓ | |
flag | flag | ||
pflag | spf13/pflag | ||
appconfig | AWS AppConfig | ✓ | sns |
s3 | AWS S3 | ✓ | sns |
parameterstore | AWS ParameterStore | ✓ | sns |
azappconfig | Azure App Configuration | ✓ | azservicebus |
azblob | Azure Blob Storage | ✓ | azservicebus |
secretmanager | GCP Secret Manager | ✓ | pubsub |
gcs | GCP Cloud Storage | ✓ | pubsub |
cobra is supported through the pflag
loader, with the
pflag.WithFlagSet
option:
config.Load(kflag.New(&config, kflag.WithFlagSet(yourCobraCmd.Flags())))
Custom Configuration Providers
You can Custom provider by implementing the Loader
for static configuration loader (e.g fs
)
or both Loader
and Watcher
for dynamic configuration loader (e.g. appconfig
).
Footnotes
-
Comparing to Get in both Viper and Koanf only can get the primitive types, Get in Konf can get any type since it's just a wrapper of Unmarshal. So for complex struct, it only needs single Konf Get but may needs multiple Get(s) in Viper and Koanf. ↩