Skip to content

Commit

Permalink
Replace viper with koanf
Browse files Browse the repository at this point in the history
Reduces dependencies
  • Loading branch information
ccremer committed May 22, 2021
1 parent c9bfd65 commit 7e9768d
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 156 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ clean: ## Clean the project

.PHONY: test
test: ## Run unit tests
@go test -coverprofile cover.out ./...
@go test -coverprofile cover.out -v ./...

.PHONY: run
run: ## Run locally
Expand Down
18 changes: 13 additions & 5 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,26 @@ With https://ccremer.github.io/charts/fronius-exporter[fronius-exporter]
fronius-exporter --url http://symo.ip.or.hostname/solar_api/v1/GetPowerFlowRealtimeData.fcgi
----

Upon each call to `/metrics`, the exporter will do a GET request on the given URL, and translate the JSON
response to Prometheus metrics format.
Upon each call to `/metrics`, the exporter will do a GET request on the given URL, and translate the JSON response to Prometheus metrics format.

== Configuration

`fronius-exporter` can be configured with CLI flags. Call the binary with `--help` to get a list of options.
`fronius-exporter` can be configured with CLI flags.
Call the binary with `--help` to get a list of options.

[TIP]
====
All flags are also configurable with Environment variables.
Replace the `.` char with `_` and uppercase the names in order for them to be recognized, e.g. `--log.level debug` becomes `LOG_LEVEL=debug`.
CLI flags take precedence though.
* Replace the `-` char with `_` in the names and uppercase the names
* Replace the `.` delimiter with `__`
* CLI flags take precedence
.Following calls are requivalent
----
fronius-exporter --symo.url http://...
SYMO__URL=http://... fronius-exporter
----
====

== As a client API
Expand Down
87 changes: 69 additions & 18 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,55 @@ import (
"strings"
"time"

"github.com/knadh/koanf"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/posflag"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
)

// ParseConfig overrides internal config defaults with up CLI flags, environment variables and ensures basic validation.
func ParseConfig(version, commit, date string, fs *flag.FlagSet, args []string) *Configuration {
config := NewDefaultConfig()

setupCliFlags(fmt.Sprintf("version %s, %s, %s", version, commit, date), fs, config)

loadConfigHierarchy(fs, args, config)

postLoadProcess(config)

log.WithField("config", *config).Debug("Parsed config")
return config
}

func setupCliFlags(version string, fs *flag.FlagSet, config *Configuration) {
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s (version %s, %s, %s):\n", os.Args[0], version, commit, date)
fmt.Fprintf(os.Stderr, "Usage of %s (%s):\n", os.Args[0], version)
fs.PrintDefaults()
}
fs.String("bindAddr", config.BindAddr, "IP Address to bind to listen for Prometheus scrapes")
fs.String("bind-addr", config.BindAddr, "IP Address to bind to listen for Prometheus scrapes")
fs.String("log.level", config.Log.Level, "Logging level")
fs.BoolP("log.verbose", "v", config.Log.Verbose, "Shortcut for --log.level=debug")
fs.StringSlice("symo.header", []string{},
fs.StringSlice("symo.header", config.Symo.Headers,
"List of \"key: value\" headers to append to the requests going to Fronius Symo")
fs.StringP("symo.url", "u", config.Symo.URL, "Target URL of Fronius Symo device")
fs.Int64("symo.timeout", int64(config.Symo.Timeout.Seconds()),
"Timeout in seconds when collecting metrics from Fronius Symo. Should not be larger than the scrape interval")
if err := viper.BindPFlags(fs); err != nil {
log.WithError(err).Fatal("Could not bind flags")
}

if err := fs.Parse(args); err != nil {
log.WithError(err).Fatal("Could not parse flags")
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()

if err := viper.Unmarshal(config); err != nil {
log.WithError(err).Fatal("Could not read config")
}
}

func postLoadProcess(config *Configuration) {
config.Symo.Timeout *= time.Second
if config.Log.Verbose {
config.Log.Level = "debug"
}

var parsedHeaders []string
for _, header := range config.Symo.Headers {
parsedHeaders = splitHeaderStrings(header, parsedHeaders)
}
config.Symo.Headers = parsedHeaders

level, err := log.ParseLevel(config.Log.Level)
if err != nil {
log.WithError(err).Warn("Could not parse log level, fallback to info level")
Expand All @@ -54,8 +64,49 @@ func ParseConfig(version, commit, date string, fs *flag.FlagSet, args []string)
} else {
log.SetLevel(level)
}
log.WithField("config", *config).Debug("Parsed config")
return config
}

func splitHeaderStrings(rest string, headers []string) []string {
s := strings.TrimPrefix(rest, ",")
arr := strings.SplitN(s, ",", 2)
if v := arr[0]; v != "" {
headers = append(headers, strings.TrimSpace(v))
}
if len(arr) < 2 {
// No more key-value pairs to parse
return headers
}
return splitHeaderStrings(arr[1], headers)
}

func loadConfigHierarchy(fs *flag.FlagSet, args []string, config *Configuration) {
koanfInstance := koanf.New(".")

// Environment variables
if err := koanfInstance.Load(env.Provider("", ".", func(s string) string {
/*
Configuration can contain hierarchies (YAML, etc.) and CLI flags dashes.
To read environment variables with hierarchies and dashes we replace the hierarchy delimiter with double underscore and dashes with single underscore.
So that parent.child-with-dash becomes PARENT__CHILD_WITH_DASH
*/
s = strings.Replace(strings.ToLower(s), "__", ".", -1)
s = strings.Replace(strings.ToLower(s), "_", "-", -1)
return s
}), nil); err != nil {
log.WithError(err).Fatal("Could not parse flags")
}

// CLI Flags
if err := fs.Parse(args); err != nil {
log.WithError(err).Fatal("Could not parse flags")
}
if err := koanfInstance.Load(posflag.Provider(fs, ".", koanfInstance), nil); err != nil {
log.WithError(err).Fatal("Could not process flags")
}

if err := koanfInstance.Unmarshal("", &config); err != nil {
log.WithError(err).Fatal("Could not merge defaults with settings from environment variables")
}
}

// ConvertHeaders takes a list of `key=value` headers and adds those trimmed to the specified header struct. It ignores
Expand Down
58 changes: 55 additions & 3 deletions cfg/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestParseConfig(t *testing.T) {
},
},
"GivenFlags_WhenBindAddrSpecified_ThenOverridePort": {
args: []string{"--bindAddr", ":9090"},
args: []string{"--bind-addr", ":9090"},
verify: func(c *Configuration) {
assert.Equal(t, ":9090", c.BindAddr)
},
Expand All @@ -119,11 +119,20 @@ func TestParseConfig(t *testing.T) {
},
"GivenHeaderEnvVar_WhenMultipleHeadersSpecified_ThenFillArray": {
envs: map[string]string{
"SYMO_HEADER": "key1=value1, KEY2= value2",
"SYMO__HEADER": "key1=value1, KEY2= value2",
},
verify: func(c *Configuration) {
assert.Contains(t, c.Symo.Headers, "key1=value1")
assert.Contains(t, c.Symo.Headers, " KEY2= value2")
assert.Contains(t, c.Symo.Headers, "KEY2= value2")
},
},
"GivenHeaderEnvVarAndFlag_WhenMultipleHeadersSpecified_ThenTakePrecedenceFromCLI": {
envs: map[string]string{
"SYMO__HEADER": "key1=value1, KEY2= value2",
},
args: []string{"--symo.header", "key3=value3"},
verify: func(c *Configuration) {
assert.Equal(t, c.Symo.Headers, []string{"key3=value3"})
},
},
"GivenUrlFlag_ThenOverrideDefault": {
Expand Down Expand Up @@ -160,3 +169,46 @@ func unsetEnv(m map[string]string) {
os.Unsetenv(key)
}
}

func Test_parseHeaderString(t *testing.T) {
tests := map[string]struct {
given string
expected []string
}{
"GivenSingleHeader_WhenParsing_LeaveUnchanged": {
given: "key=value",
expected: []string{"key=value"},
},
"GivenTwoHeaders_WhenParsing_SplitInTwo": {
given: "key1=value1,key2=value2",
expected: []string{"key1=value1", "key2=value2"},
},
"GivenThreeHeaders_WhenParsing_SplitInThree": {
given: "key1=value1,key2=value2,key3=value3",
expected: []string{"key1=value1", "key2=value2", "key3=value3"},
},
"GivenMalformedHeaders_WhenParsing_RegardAsPartOfPreviousHeader": {
given: "key1=value1,key2value2",
expected: []string{"key1=value1", "key2value2"},
},
"GivenHeadersWithSpace_WhenParsing_TrimSpaceAfterComma": {
given: "key1=value1 , key2=value2",
expected: []string{"key1=value1", "key2=value2"},
},
"GivenHeadersWithTrailingComma_WhenParsing_IgnoreEmptyString": {
given: "key1=value1 ,",
expected: []string{"key1=value1"},
},
"GivenHeadersWithSpaces_WhenParsing_Include": {
given: "key1=value with space,",
expected: []string{"key1=value with space"},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
var result []string
result = splitHeaderStrings(tt.given, result)
assert.Equal(t, tt.expected, result)
})
}
}
16 changes: 8 additions & 8 deletions cfg/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import "time"
type (
// Configuration holds a strongly-typed tree of the configuration
Configuration struct {
Log LogConfig
Symo SymoConfig
BindAddr string
Log LogConfig `koanf:"log"`
Symo SymoConfig `koanf:"symo"`
BindAddr string `koanf:"bind-addr"`
}
// LogConfig configures the logging options
LogConfig struct {
Level string
Verbose bool
Level string `koanf:"level"`
Verbose bool `koanf:"verbose"`
}
// SymoConfig configures the Fronius Symo device
SymoConfig struct {
URL string
Timeout time.Duration
Headers []string `mapstructure:"header"`
URL string `koanf:"url"`
Timeout time.Duration `koanf:"timeout"`
Headers []string `koanf:"header"`
}
)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ module github.com/ccremer/fronius-exporter
go 1.16

require (
github.com/knadh/koanf v1.0.0
github.com/prometheus/client_golang v1.9.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.7.0
)
Loading

0 comments on commit 7e9768d

Please sign in to comment.