Skip to content

Commit

Permalink
Merge pull request #1 from ccremer/dev
Browse files Browse the repository at this point in the history
Implement Fronius exporter
  • Loading branch information
ccremer committed May 17, 2020
2 parents 1bd4f02 + 653b1d2 commit 7a672ed
Show file tree
Hide file tree
Showing 12 changed files with 615 additions and 3 deletions.
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
.PHONY: build fmt dist clean test
.PHONY: build fmt dist clean test run
SHELL := /usr/bin/env bash

build: fmt
@go build ./...
@go build .

fmt:
@[[ -z $$(go fmt ./...) ]]
Expand All @@ -11,7 +11,10 @@ dist: fmt
@goreleaser release --snapshot --rm-dist --skip-sign

clean:
@rm fronius-exporter c.out
@rm -rf fronius-exporter c.out dist

test: fmt
@go test -coverprofile c.out ./...

run:
@go run . -v
2 changes: 2 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ endif::[]
Scrapes a Fronius Photovoltaic power installation and converts sensor data to Prometheus metrics.
It has been tested with Fronius Symo 8.2-3-M (Software version 3.14.1-10).

image::examples/grafana.png[Grafana]

== Installing


Expand Down
73 changes: 73 additions & 0 deletions cfg/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cfg

import (
"fmt"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
"net/http"
"os"
"strings"
)

func ParseConfig(version, commit, date string, fs *flag.FlagSet, args []string) *Configuration {
config := NewDefaultConfig()

fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s (version %s, %s, %s):\n", os.Args[0], version, commit, date)
fs.PrintDefaults()
}
fs.String("bindAddr", 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{},
"List of \"key: value\" headers to append to the requests going to Fronius Symo")
fs.String("symo.url", config.Symo.Url, "Target URL of Fronius Symo device")
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")
}

if config.Log.Verbose {
config.Log.Level = "debug"
}
level, err := log.ParseLevel(config.Log.Level)
if err != nil {
log.WithError(err).Warn("Could not parse log level, fallback to info level")
config.Log.Level = "info"
log.SetLevel(log.InfoLevel)
} else {
log.SetLevel(level)
}
log.WithField("config", *config).Debug("Parsed config")
return config
}

func ConvertHeaders(headers []string, header *http.Header) {
for _, hd := range headers {
arr := strings.SplitN(hd, ":", 2)
if len(arr) < 2 {
log.WithFields(log.Fields{
"arg": hd,
"error": "cannot split: missing colon",
}).Warn("Could not parse header, ignoring")
continue
}
key := strings.TrimSpace(arr[0])
value := strings.TrimSpace(arr[1])
log.WithFields(log.Fields{
"key": key,
"value": value,
}).Debug("Using header")
header.Set(key, value)
}
}
143 changes: 143 additions & 0 deletions cfg/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package cfg

import (
flag "github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)

func TestConvertHeaders(t *testing.T) {
type args struct {
headers []string
header *http.Header
}
tests := []struct {
name string
args args
verify func(header *http.Header)
}{
{
name: "WhenEmptyArray_ThenDoNothing",
args: args{
headers: []string{},
header: &http.Header{},
},
verify: func(header *http.Header) {
assert.Empty(t, header)
},
},
{
name: "WhenInvalidEntry_ThenIgnore",
args: args{
headers: []string{"invalid"},
header: &http.Header{},
},
verify: func(header *http.Header) {
assert.Empty(t, header)
},
},
{
name: "WhenValidEntry_ThenParse",
args: args{
headers: []string{"Authentication: Bearer <token>"},
header: &http.Header{},
},
verify: func(header *http.Header) {
assert.Equal(t, "Bearer <token>", header.Get("Authentication"))
},
},
{
name: "GivenValidEntry_WhenSpacesAroundValues_ThenTrim",
args: args{
headers: []string{" Authentication: Bearer <token> "},
header: &http.Header{},
},
verify: func(header *http.Header) {
assert.Equal(t, "Bearer <token>", header.Get("Authentication"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ConvertHeaders(tt.args.headers, tt.args.header)
tt.verify(tt.args.header)
})
}
}

func TestParseConfig(t *testing.T) {
tests := []struct {
name string
args []string
want *Configuration
fs flag.FlagSet
verify func(c *Configuration)
}{
{
name: "GivenNoFlags_ThenReturnDefaultConfig",
args: []string{},
verify: func(c *Configuration) {
assert.Equal(t, "info", c.Log.Level)
},
},
{
name: "GivenLogFlags_WhenVerboseEnabled_ThenSetLoggingLevelToDebug",
args: []string{"-v"},
verify: func(c *Configuration) {
assert.Equal(t, "debug", c.Log.Level)
assert.Equal(t, true, c.Log.Verbose)
},
},
{
name: "GivenLogFlags_WhenLogLevelSpecified_ThenOverrideLogLevel",
args: []string{"--log.level=warn"},
verify: func(c *Configuration) {
assert.Equal(t, "warn", c.Log.Level)
},
},
{
name: "GivenLogFlags_WhenInvalidLogLevelSpecified_ThenSetLoggingLevelToInfo",
args: []string{"--log.level=invalid"},
verify: func(c *Configuration) {
assert.Equal(t, "info", c.Log.Level)
},
},
{
name: "GivenLogLevel_WhenVerboseEnabled_ThenSetLoggingLevelToDebug",
args: []string{"--log.level=fatal", "-v"},
verify: func(c *Configuration) {
assert.Equal(t, "debug", c.Log.Level)
assert.Equal(t, true, c.Log.Verbose)
},
},
{
name: "GivenFlags_WhenBindAddrSpecified_ThenOverridePort",
args: []string{"--bindAddr", ":9090"},
verify: func(c *Configuration) {
assert.Equal(t, ":9090", c.BindAddr)
},
},
{
name: "GivenHeaderFlags_WhenMultipleHeadersSpecified_ThenFillArray",
args: []string{"--symo.header", "key1:value1", "--symo.header", "KEY2: value2"},
verify: func(c *Configuration) {
assert.Contains(t, c.Symo.Headers, "key1:value1")
assert.Contains(t, c.Symo.Headers, "KEY2: value2")
},
},
{
name: "GivenUrlFlag_ThenOverrideDefault",
args: []string{"--symo.url", "myurl"},
verify: func(c *Configuration) {
assert.Equal(t, "myurl", c.Symo.Url)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ParseConfig("version", "commit", "date", &tt.fs, tt.args)
tt.verify(result)
})
}
}
38 changes: 38 additions & 0 deletions cfg/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cfg

import "time"

type (
// Configuration holds a strongly-typed tree of the configuration
Configuration struct {
Log LogConfig
Symo SymoConfig
BindAddr string
}
// LogConfig configures the logging options
LogConfig struct {
Level string
Verbose bool
}
// SymoConfig configures the Fronius Symo device
SymoConfig struct {
Url string
Timeout time.Duration
Headers []string `mapstructure:"header"`
}
)

// NewDefaultConfig retrieves the hardcoded configs with sane defaults
func NewDefaultConfig() *Configuration {
return &Configuration{
Log: LogConfig{
Level: "info",
},
Symo: SymoConfig{
Url: "http://symo.ip.or.hostname/solar_api/v1/GetPowerFlowRealtimeData.fcgi",
Timeout: 5 * time.Second,
Headers: []string{},
},
BindAddr: ":8080",
}
}
Binary file added examples/grafana.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7a672ed

Please sign in to comment.