go get github.com/brumhard/alligotor
Alligotor is designed to be used as the configuration source for executables (not commands in a command line application) for example for api servers or any other long-running applications that need a startup config.
It takes only a few lines of code to get going, and it supports:
- setting defaults just like you're used to from for example json unmarshalling (see this example)
- reading from YAML and JSON files from io.Reader, local file system or fs.FS
- reading from environment variables
- reading from command line flags
- defining custom source to load config from your preferred source (e.g. etcd)
- extremely simple API
- support for every type (by implementing TextUnmarshaler) and out of the box support for many common ones
- autogenerated property names for each child property in the config, but still configurable via struct tags
- set overwrite order by defining the sources in the preferred order in
alligotor.New()
There are a lot of configuration packages for Go that give you the ability to load you configuration from several sources like env vars, command line flags or config files.
Alligotor was designed to have the least configuration effort possible (autogenerating the property names for the source trough reflection) while still keeping it customizable. So for example if a config struct looks like the following:
type cfg struct {
API struct {
Port int
}
}
The port value will be loaded by default from the env variable <PREFIX>_API_PORT
and the flag --api-port
without the
need to set that explicitly.
That's why if you keep the package defaults you only need one function call, and your config struct definition to fill this struct with values from environment variables, several config files and command line flags or your defined custom source.
Generally embedded structs are supported but certain use cases don't work. So for example in the following struct:
type DB struct {
Host string
}
type Config {
DB
}
You can set the value for the DB.Host with the env variable <PREFIX>_DB_HOST
but not with <PREFIX>_HOST
directly.
Since there is no nice way of representing arrays in all config sources (for example environment variables) it's currently not supported in these sources.
The ReadersSource
on the other hand can easily read arrays.
package main
import (
"github.com/brumhard/alligotor"
"go.uber.org/zap/zapcore"
"time"
)
func main() {
// define the config struct
cfg := struct {
SomeList []string
SomeMap map[string]string
API struct {
Enabled bool
LogLevel zapcore.Level
}
DB struct {
HostName string
Timeout time.Duration
}
}{
// could define defaults here
}
// get the values
_ = alligotor.Get(&cfg)
}
Just like with the json package alligotor only supports setting public properties since it relies on reflection.
As alligotor aims for good customizability, the Collector's constructor supports as many sources as you like. Included in the package are one source for env vars, one for config files (supporting readers, local file system or fs.FS) and one for cli flags (see sources).
It is shown in the following example.
// all predefined sources
_ = alligotor.New(
alligotor.NewFilesSource("./test_config.*"),
alligotor.NewEnvSource("TEST"),
alligotor.NewFlagsSource(),
)
// only from env vars with prefix "TEST" and custom separator
_ = alligotor.New(
alligotor.NewEnvSource("TEST", alligotor.WithEnvSeparator("::")),
)
As shown in the latter example, the sources support an option to set a custom separator. In case it is not set explicitly, it will be set to the defaults:
- env vars:
_
(underscore) - cli flags:
.
(dash)
For each of the following sources the following example config struct is used.
// example struct
type Config struct {
Enabled bool
Sub struct {
Port int
}
}
The source for environment variables can be used as follows:
_ = alligotor.New(
alligotor.NewEnvSource("TEST", alligotor.WithEnvSeparator("::")),
)
It supports setting a custom prefix as well as a custom separator. The separator is needed for nested config structs.
So for example for the example struct from above and the defined source configuration the value for the Port
field
will be read from TEST::SUB::PORT
.
Like with the other sources the properties name to look up can be changed by adding a struct tag. In that case
add config:"env=something"
as a struct tag for the Port
field and it will be read from TEST::SUB::SOMETHING
.
The source for command line flags can be used as follows:
_ = alligotor.New(
alligotor.NewFlagsSource(alligotor.WithFlagSeparator(".")),
)
It supports setting a custom separator, that is needed for nested structs.
So for example for the example struct from above and the defined source configuration the value for the Port
field
will be read from --sub.port
.
Like with the other sources the properties name to look up can be changed by adding a struct tag. In that case
add config:"flag=something"
as a struct tag for the Port
field and it will be read from --sub.something
.
In addition the struct tag can be defined as config:"flag=p"
to set the short name for the flag (-p
) or any of
config:"flag=p some"
or config:"flag=some p"
to overwrite the name and the short name.
To set a flags usage string in addition to the config
struct tag also the description
struct tag is read and set as
the flags usage that is returned when the user requests help with --help
or -h
.
The source for files can be used in one of the following ways:
_ = alligotor.New(
// any io.Reader is supported
alligotor.NewReadersSource(strings.NewReader(`{"key":"value"}`))
)
_ = alligotor.New(
// reads from local fs in this case
alligotor.NewFilesSource("dir/example_config.*", "test2/config.yml"),
)
_ = alligotor.New(
// fsys has a type implementing fs.FS in this case
alligotor.NewFSFilesSource(fsys, "dir/example_config.*", "test2/config.yml")
)
NewReadersSource
reads the config file from any io.Reader so for example a file or an http endpoint.
NewFilesSource
and NewFSFilesSource
are simple wrappers around the ReadersSource
to find the files using glob
patterns on any filesystem (either local FS or fs.FS). This differentiation is used since os.DirFS does not support
propper relative and absolute paths for the local filesystem.
Reading from files works as expected (just like json or yaml unmarshaling). The only difference is that it looks for fields in a case-insensitive manner.
Of course also here the name can be defined by setting the struct tag to for example config="file=something"
which
works just like the json or yaml struct tag.
Currently, only yaml and json files are supported but others will be added if needed.
Struct tags are used to overwrite the name for the env source that is generated by default. They are defined in the following format:
type Config struct {
Enabled bool `config:"key=value,key2=value2"`
}
where key could for example be file
or env
. The struct tag can also be consumed from custom sources from the Field
property Field.Configs()
, which contains a map from struct tag key to value.
Custom sources can be added by implementing the following interfaces. For an example on how to implement a config source take a look at the env source, which implements reading from environment variables in less than 100 lines of code.
Each config source needs to implement at least the following interface.
type ConfigSource interface {
Read(field *Field) (interface{}, error)
}
As shown it contains only one method that receives a Field instance and returns the value that was found for the field. For sources that only support setting values as strings (like for example environment variables) just return a byte slice containing the string and it will automatically be converted to the target type if possible. Any other type is used directly leading to an error on type mismatch.
You should not return structs directly since this could lead to errors if some struct properties are set and others are not. This would then overwrite the target with the zero value, which is not intended.
The received fields match directly to the fields in the config. So for example for a config struct like the following:
type Config struct {
Sub struct {
Field string
}
}
two fields will be send to the Read function
, one containing the whole sub struct and one referencing only the Field
property. The structs are included to enable structs that implement the TextUnmarshaler
interface. If no value is
found for a specific field nil should be returned in order to not override any existing value for that field with an
empty one.
If the custom config source depends on some initialization before reading the fields the ConfigSourceInitializer
interface can be implemented as well. The method is invoked right before calling the Read function. In the existing
sources this is used for example to read the the files to not do it for every field or read in the environment
variables.
type ConfigSourceInitializer interface {
Init(fields []Field) error
}