A pluggable hierarchical configuration management library with zero dependencies, for golang.
With dep:
dep ensure -add github.com/moogar0880/venom
Or with go get:
go get github.com/moogar0880/venom
This library aims to provide the basic building blocks of a configuration management library. It exposes as many aspects as the standard lib will reasonably support, such as:
- Explicitly set defaults or overrides.
- Find and load JSON config files.
- Load configuration files from a directory, with the option to recursively load configs from sub-directories.
- Load configs from environment variables.
- Load command line arguments using the
flags
library.
These mechanisms are exposed in a completely extendible way which will allow you to easily perform the following actions:
- Specify your own config level precedence (eg, environment variables can be made to override command line arguments).
- Default behavior can be replaced with custom behavior by writing a custom
Resolver
. - Custom config levels can be easily implemented by writing a custom
Resolver
.
You can easily set default values for configuration options that are not required to be specified by config levels with a higher precedence.
// simple values can easily be set as defaults
venom.SetDefault("verbose", false)
venom.SetDefault("hostname", "localhost")
// more complex objects can also be set
venom.SetDefault("log", map[string]interface{
"level": "INFO",
"fields": map[string]interface{
"origin": "my_awesome_app",
"category": "categories",
},
})
Venom allows you to specify custom file loaders for specific file types. By
default the only implementation is the JSONLoader
which loads any file with
an extension of .json
using json.Unmarshal
.
If you wish to implement your own type of config file reader you need only to
implement the IOFileLoader
interface:
type IOFileLoader func(io.Reader) (map[string]interface{}, error)
Once you have any custom IOFileLoader
s registered via RegisterExtension
,
you can easily load a single file or all files in a directory.
// load a single file
venom.LoadFile("config.json")
// load all files within a directory
venom.LoadDirectory("/etc/conf.d", false)
// recursively load all files within a directory or it's sub-directories
venom.LoadDirectory("/etc/conf.d", true)
You can easily set values which overrides all other values for a single configuration.
venom.SetOverride("verbose", true)
Configuration values may be loaded from any set environment variables, which
is enabled by default in the global venom instance and when creating a custom
venom instance via venom.Default()
.
Note: Environment variables are case sensitive and should be set as uppercase strings with words that are separated by an underscore ("_") character.
package main
import (
"fmt"
"os"
"github.com/moogar0880/venom"
)
func main() {
os.Setenv("LOG_LEVEL", "INFO")
fmt.Println(venom.Get("log.level")) // Output: "INFO"
}
To specify a custom environment variable prefix, you can simply create and
register a new EnvironmentVariableResolver
.
package main
import (
"fmt"
"os"
"github.com/moogar0880/venom"
)
func main() {
envVarResolver := &venom.EnvironmentVariableResolver{
Prefix: "MYSERVICE",
}
venom.RegisterResolver(venom.EnvironmentLevel, envVarResolver)
os.Setenv("LOG_LEVEL", "IGNORED")
os.Setenv("MYSERVICE_LOG_LEVEL", "INFO")
fmt.Println(venom.Get("log.level")) // Output: "INFO"
}
To perform specific character mapping translations for environment variables
you can provide a custom KeyTranslator
to an EnvironmentVariableResolver
.
package main
import (
"fmt"
"os"
"unicode"
"github.com/moogar0880/venom"
)
func main() {
envVarResolver := &venom.EnvironmentVariableResolver{
// Provide a custom KeyTranslator to an EnvironmentVariableResolver
Translator: func(b byte) byte {
switch b {
case '-':
// Convert all hypens to underscores.
return '_'
default:
// And otherwise translate all other characters to their
// uppercase equivalents.
return byte(unicode.ToUpper(rune(b)))
}
},
}
venom.RegisterResolver(venom.EnvironmentLevel, envVarResolver)
os.Setenv("LOG_LEVEL", "IGNORED")
os.Setenv("MYSERVICE_LOG_LEVEL", "INFO")
fmt.Println(venom.Get("log.level")) // Output: "INFO"
}
By default commandline flags can be parsed using the standard lib flag
library. There are two different ways that you can approach loading command
line configs, and they both involve creating and registering a new
FlagsetResolver
instance with Venom.
Note: Venom will parse flags if they have not already been parsed, but
Venom will not attempt to re-parse an already parsed FlagSet
.
If you configure your commandline options to be loaded from the global flags
FlagSet, you can pass a zero-value FlagsetResolver
when registering the
resolver with Venom.
fs.String("log-level", "WARNING", "set log level")
flagResolver := &venom.FlagsetResolver{}
venom.RegisterResolver(venom.FlagLevel, flagResolver)
You can initialize Venom to log to stdout (default) on reads and writes or provide a logging interface of your choice.
venDef := venom.NewLoggable()
venDef.SetDefault("baz", "bee")
venDef.Get("baz")
// 2019-04-21T10:01:39.529Z[venom]: writing level=0 key=baz val=bee
// 2019-04-21T10:01:39.529Z[venom]: reading key=baz val=bee exist=true
There is a tiny bit of overhead to use a custom logger but is straight forward to do. For example, using sirupsen/logrus:
// create a wrapping struct to hold the logging mechanism
type LogrusLogger struct {
log *logrus.Entry
}
// create read and write methods which have these signatures
// and configure the log line as you choose
func (l *LogrusLogger) LogWrite(level venom.ConfigLevel, key string, val interface{}) {
l.log.WithFields(logrus.Fields{"level": level,"key": key,"val": val}).Info("writing config value")
}
func (l *LogrusLogger) LogRead(key string, val interface{}, bl bool) {
l.log.WithFields(logrus.Fields{"key": key,"val": val,"exist": bl}).Info("read config value")
}
lgr := logrus.New()
l := logrus.NewEntry(lgr)
ven := venom.NewLoggableWith(&LogrusLogger{log: l})
ven.SetDefault("foo", "bar")
ven.Get("foo")
// INFO[0000] writing config value fields.level=0 key=foo val=bar
// INFO[0000] read config value exist=true key=foo val=bar
You also have the option to provide a custom flag.FlagSet
instance via a
FlagsetResolver
.
Note: When providing a FlagSet
, you may also provide the arguments to be
parsed if the FlagSet
has not already been parsed. The arguments will default
to os.Args[1:]
if none are specified.
fs := flag.NewFlagSet("example", flag.ContinueOnError)
fs.String("log-level", "WARNING", "set log level")
flagResolver := &venom.FlagsetResolver{
Flags: fs,
}
venom.RegisterResolver(venom.FlagLevel, flagResolver)
Consuming applications are able to define their own ConfigLevel
s in order to
define configuration values with higher or lower precedence. For example, to
set configs at levels above Override
(not recommended), you could do
something similar to the following:
const MySuperImportantLevel venom.ConfigLevel = venom.OverrideLevel + 1
venom.SetLevel(MySuperImportantLevel, "verbose", true)
There are several ways to access config values from Venom:
Get(key string) interface{}
Find(key string) (interface{}, bool)
If you're unsure whether or not a config value has been set, Find
will return
an optional boolean value indicating whether or not the value has been
specified. Otherwise, Get
will return nil
in the event that a config has not
been specified.
Venom automatically nests config values that are specified as separated by the
value of Delim
, which defaults to "."
. This means that you can express
more complex config structures when setting and reading variables.
venom.SetDefault("log.level", "INFO")
fmt.Printf("%v", venom.Get("log")) // Output: map[string]interface{"level": "INFO"}
fmt.Printf("%v", venom.Get("log.level")) // Output: "INFO"
Venom exposes the ability to alias one key to another. This allows applications to more easily modify their configuration without breaking backwards compatibility when doing so.
venom.SetDefault("log.enabled", true)
venom.Alias("verbose", "log.enabled")
fmt.Println(venom.Get("verbose")) // Output: true
Venom supports the ability to unmarshal configuration data into struct values
and also introduces the new venom
struct field tag, which allows callers
to specify different configuration fields to set to fields. Nested structs
Note: that errors are returned if the stored type and the type of the struct field do not match.
Note: Nested structs are supported and will carry the context of the name of the parent config. See the following example for more on this
type LoggingConfig struct {
// this field will be loaded from log.level if Unmarshalled via the
// following Config struct
Level string `venom:"level"`
}
type Config struct {
Log LoggingConfig `venom:"log"`
}
var c Config
err := venom.Unmarshal(nil, &c)
As of 0.2.0, venom now exposes optional functions for defining Venom instances that are safe for concurrent goroutine access.
Two functions are exposed to provide new goroutine safe Venom instances, the behavior of which mimics the functions unsafe counterparts:
// generate a new, empty, venom instance that is safe for concurrent access
ven := venom.NewSafe()
// or generate a new venom instance with some default levels applied to it
ven = venom.DefaultSafe()
The above examples show how to use the global venom instance for the sake of brevity, but you can create your own venom instances to use directly.
ven := venom.New()
ven.SetDefault("verbose", false)
Additionally, as of 0.2.0, you can define your own ConfigStore
to control how
venom manages it's underlying configuration storage. This can be achieved by
creating a new Venom instance with a predefined ConfigStore
via the
NewWithStore
function.
ven := venom.NewWithStore(venom.NewSafeConfigStore())
ven.SetDefault("verbose", false)
goos: darwin
goarch: amd64
pkg: github.com/moogar0880/venom
BenchmarkVenomGet/single_ConfigLevel_with_one_key/value_pair-8 20000000 84.6 ns/op 16 B/op 1 allocs/op
BenchmarkVenomGet/many_key/value_pairs_in_a_single_ConfigLevel-8 20000000 102 ns/op 16 B/op 1 allocs/op
BenchmarkVenomGet/many_key/value_pairs_spread_across_multiple_ConfigLevels-8 20000000 99.2 ns/op 16 B/op 1 allocs/op
BenchmarkVenomWrite/single_key/value_pair_in_one_ConfigLevel-8 20000000 89.0 ns/op 16 B/op 1 allocs/op
BenchmarkVenomWrite/many_key/value_pairs_in_one_ConfigLevel-8 2000000 648 ns/op 121 B/op 3 allocs/op
BenchmarkVenomWrite/many_nested_key/value_pairs_in_one_ConfigLevel-8 1000000 2055 ns/op 922 B/op 7 allocs/op
BenchmarkVenomWrite/many_key/value_pairs_in_many_ConfigLevels-8 2000000 589 ns/op 121 B/op 3 allocs/op
BenchmarkVenomWrite/many_nested_key/value_pairs_in_many_ConfigLevels-8 1000000 1679 ns/op 922 B/op 7 allocs/op