Go configuration with talons
Switch branches/tags
Nothing to show
Clone or download
Pull request Compare This branch is 9 commits ahead, 86 commits behind spf13:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.gitignore
.travis.yml
LICENSE
README.md
falcon.go
falcon_test.go
flags.go
flags_test.go
util.go

README.md

falcon

Go configuration with talons!

What is Falcon?

Falcon is a fork of the popular viper project. The main purposes of this fork are as follows:

  • Remove remote configuration options for less dependencies
  • Remove HCL and Java properties formats for less dependencies
  • Remove jww logging for less dependencies
  • Remove singleton pattern, as it is a pain to deal with when unit testing
  • Implement any features that I (Matthieu Grieger) want to include

Falcon is a complete configuration solution for go applications. It is designed to work within an application, and can handle all types of configuration needs and formats. It supports:

  • Setting defaults
  • Reading from JSON, TOML, and YAML
  • Live watching and re-reading of config files (optional)
  • Reading from environment variables
  • Reading from command line flags
  • Reading from buffer
  • Setting explicit values

Falcon can be thought of as a registry for all of your applications configuration needs.

Why Falcon?

When building a modern application, you don’t want to worry about configuration file formats; you want to focus on building awesome software. Falcon is here to help with that.

Falcon does the following for you:

  1. Find, load, and unmarshal a configuration file in JSON, TOML, and YAML formats.
  2. Provide a mechanism to set default values for your different configuration options.
  3. Provide a mechanism to set override values for options specified through command line flags.
  4. Provide an alias system to easily rename parameters without breaking existing code.
  5. Make it easy to tell the difference between when a user has provided a command line or config file which is the same as the default.

Falcon uses the following precedence order. Each item takes precedence over the item below it:

  • Explicit call to Set
  • Flag
  • Env
  • Config
  • Default

Falcon configuration keys are case insensitive.

Putting Values into Falcon

Establishing Defaults

A good configuration system will support default values. A default value is not required for a key, but it's useful in the event that a key hasn’t been set via config file, environment variable, remote configuration or flag.

Examples:

config := falcon.New()

config.SetDefault("ContentDir", "content")
config.SetDefault("LayoutDir", "layouts")
config.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})

Reading Config Files

Falcon requires minimal configuration so it knows where to look for config files. Viper supports JSON, TOML, and YAML files. Falcon can search multiple paths, but currently a single Falcon instance only supports a single configuration file. Falcon does not default to any configuration search paths leaving defaults decision to an application.

Here is an example of how to use Falcon to search for and read a configuration file. None of the specific paths are required, but at least one path should be provided where a configuration file is expected.

config := falcon.New()

config.SetConfigName("config") // name of config file (without extension)
config.AddConfigPath("/etc/appname/")   // path to look for the config file in
config.AddConfigPath("$HOME/.appname")  // call multiple times to add many search paths
config.AddConfigPath(".")               // optionally look for config in the working directory
err := config.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
	panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

Watching and re-reading config files

Falcon supports the ability to have your application live read a config file while running.

Gone are the days of needing to restart a server to have a config take effect, Falcon powered applications can read an update to a config file while running and not miss a beat.

Simply tell the viper instance to watchConfig. Optionally you can provide a function for Falcon to run each time a change occurs.

Make sure you add all of the configPaths prior to calling WatchConfig()

config := falcon.New()
config.AddConfigPath("$HOME/.appname")

config.WatchConfig()
config.OnConfigChange(func(e fsnotify.Event) {
	fmt.Println("Config file changed:", e.Name)
})

Reading Config from io.Reader

Falcon predefines many configuration sources such as files, environment variables, and flags, but you are not bound to them. You can also implement your own required configuration source and feed it to Falcon.

config := falcon.New()

config.SetConfigType("yaml") // or config.SetConfigType("YAML")

// any approach to require this configuration into your program.
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
age: 35
eyes : brown
beard: true
`)

config.ReadConfig(bytes.NewBuffer(yamlExample))

config.Get("name") // this would be "steve"

Setting Overrides

These could be from a command line flag, or from your own application logic.

config := falcon.New()

config.Set("Verbose", true)
config.Set("LogFile", LogFile)

Registering and Using Aliases

Aliases permit a single value to be referenced by multiple keys.

config := falcon.New()

config.RegisterAlias("loud", "Verbose")

config.Set("verbose", true) // same result as next line
config.Set("loud", true)   // same result as prior line

config.GetBool("loud") // true
config.GetBool("verbose") // true

Working with Environment Variables

Falcon has full support for environment variables. This enables 12 factor applications out of the box. There are four methods that exist to aid working with ENV:

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvReplacer(string...) *strings.Replacer

When working with ENV variables, it’s important to recognize that Falcon treats ENV variables as case sensitive.

Falcon provides a mechanism to try to ensure that ENV variables are unique. By using SetEnvPrefix, you can tell Falcon to use add a prefix while reading from the environment variables. Both BindEnv and AutomaticEnv will use this prefix.

BindEnv takes one or two parameters. The first parameter is the key name, the second is the name of the environment variable. The name of the environment variable is case sensitive. If the ENV variable name is not provided, then Falcon will automatically assume that the key name matches the ENV variable name, but the ENV variable is IN ALL CAPS. When you explicitly provide the ENV variable name, it does not automatically add the prefix.

One important thing to recognize when working with ENV variables is that the value will be read each time it is accessed. Falcon does not fix the value when the BindEnv is called.

AutomaticEnv is a powerful helper especially when combined with SetEnvPrefix. When called, Falcon will check for an environment variable any time a Get() request is made. It will apply the following rules. It will check for a environment variable with a name matching the key uppercased and prefixed with the EnvPrefix if set.

SetEnvReplacer allows you to use a strings.Replacer object to rewrite Env keys to an extent. This is useful if you want to use - or something in your Get() calls, but want your environmental variables to use _ delimiters. An example of using it can be found in falcon_test.go.

Env example

config := falcon.New()

config.SetEnvPrefix("spf") // will be uppercased automatically
config.BindEnv("id")

os.Setenv("SPF_ID", "13") // typically done outside of the app

id := config.Get("id") // 13

Working with Flags

Falcon has the ability to bind to flags. Specifically, Falcon supports Pflags as used in the Cobra library.

Like BindEnv, the value is not set when the binding method is called, but when it is accessed. This means you can bind as early as you want, even in an init() function.

The BindPFlag() method provides this functionality.

Example:

config := falcon.New()

serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
falcon.BindPFlag("port", serverCmd.Flags().Lookup("port"))

The use of pflag in Falcon does not preclude the use of other packages that use the flag package from the standard library. The pflag package can handle the flags defined for the flag package by importing these flags. This is accomplished by a calling a convenience function provided by the pflag package called AddGoFlagSet().

Example:

package main

import (
	"flag"
	"github.com/spf13/pflag"
)

func main() {
	pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
	pflag.Parse()
    ...
}

Flag interfaces

Falcon provides two Go interfaces to bind other flag systems if you don't use Pflags.

FlagValue represents a single flag. This is a very simple example on how to implement this interface:

type myFlag struct {}
func (f myFlag) IsChanged() { return false }
func (f myFlag) Name() { return "my-flag-name" }
func (f myFlag) ValueString() { return "my-flag-value" }
func (f myFlag) ValueType() { return "string" }

Once your flag implements this interface, you can simply tell Falcon to bind it:

config := falcon.New()

config.BindFlagValue("my-flag-name", myFlag{})

FlagValueSet represents a group of flags. This is a very simple example on how to implement this interface:

type myFlagSet struct {
	flags []myFlag
}

func (f myFlagSet) VisitAll(fn func(FlagValue)) {
	for _, flag := range flags {
		fn(flag)	
	}
}

Once your flag set implements this interface, you can simply tell Falcon to bind it:

config := falcon.New()

fSet := myFlagSet{
	flags: []myFlag{myFlag{}, myFlag{}},
}
config.BindFlagValues("my-flags", fSet)

Getting Values From Falcon

In Falcon, there are a few ways to get a value depending on the value's type. The following functions and methods exist:

  • Get(key string) : interface{}
  • GetBool(key string) : bool
  • GetFloat64(key string) : float64
  • GetInt(key string) : int
  • GetString(key string) : string
  • GetStringMap(key string) : map[string]interface{}
  • GetStringMapString(key string) : map[string]string
  • GetStringSlice(key string) : []string
  • GetTime(key string) : time.Time
  • GetDuration(key string) : time.Duration
  • IsSet(key string) : bool

One important thing to recognize is that each Get function will return a zero value if it’s not found. To check if a given key exists, the IsSet() method has been provided.

Example:

config := falcon.New()

// ...

config.GetString("logfile") // case-insensitive Setting & Getting
if config.GetBool("verbose") {
    fmt.Println("verbose enabled")
}

Accessing nested keys

The accessor methods also accept formatted paths to deeply nested keys. For example, if the following JSON file is loaded:

{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

Falcon can access a nested field by passing a . delimited path of keys:

config := falcon.New()

// ...

config.GetString("datastore.metric.host") // (returns "127.0.0.1")

This obeys the precedence rules established above; the search for the root key (in this example, datastore) will cascade through the remaining configuration registries until found. The search for the sub-keys (metric and host), however, will not.

For example, if the metric key was not defined in the configuration loaded from file, but was defined in the defaults, Viper would return the zero value.

On the other hand, if the primary key was not defined, Viper would go through the remaining registries looking for it.

Lastly, if there exists a key that matches the delimited key path, its value will be returned instead. E.g.

{
    "datastore.metric.host": "0.0.0.0",
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

config.GetString("datastore.metric.host") //returns "0.0.0.0"

Extract sub-tree

Extract sub-tree from Falcon.

For example, falcon represents:

app:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

After executing:

config := falcon.New()

// ...

subv := config.Sub("app.cache1")

subv represents:

max-items: 100
item-size: 64

Suppose we have:

func NewCache(cfg *Falcon) *Cache {...}

which creates a cache based on config information formatted as subv. Now it's easy to create these 2 caches separately as:

config := falcon.New()

// ...

cfg1 := config.Sub("app.cache1")
cache1 := NewCache(cfg1)

cfg2 := config.Sub("app.cache2")
cache2 := NewCache(cfg2)

Unmarshaling

You also have the option of Unmarshaling all or a specific value to a struct, map, etc.

There are two methods to do this:

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : error

Example:

type config struct {
	Port int
	Name string
	PathMap string `mapstructure:"path_map"`
}

var C config

conf := falcon.New()

// ...

err := conf.Unmarshal(&C)
if err != nil {
	t.Fatalf("unable to decode into struct, %v", err)
}