Skip to content

Commit

Permalink
Allow exporter configuration via config file (#19)
Browse files Browse the repository at this point in the history
* go mod tidy

* go mod tidy

* Add support for loading settings from config file

* Add verbose option to "go test" for better test feedback

* Add tests for re-worked config code
  • Loading branch information
dylan-tock authored Oct 31, 2023
1 parent 0873b6e commit 49fd2b7
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 17 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Actual config files
maxctrl_exporter.yaml

# IDE specific files
.idea/
.vscode/
Expand All @@ -19,4 +22,4 @@
vendor/
maxctrl_exporter
bin/linux/maxctrl_exporter
bin/
bin/
2 changes: 1 addition & 1 deletion Makefile.common
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ common-test-short:
.PHONY: common-test
common-test:
@echo ">> running all tests"
GO111MODULE=$(GO111MODULE) $(GO) test $(test-flags) $(GOOPTS) $(pkgs)
GO111MODULE=$(GO111MODULE) $(GO) test -v $(test-flags) $(GOOPTS) $(pkgs)

.PHONY: common-format
common-format:
Expand Down
85 changes: 70 additions & 15 deletions maxctrl_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,41 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"strings"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gopkg.in/yaml.v2"
)

const (
metricsPath = "/metrics"
localIP = "0.0.0.0"
)

var (
maxScaleUrl string // URL of maxscale instance
maxScaleUsername string // Username for maxscale REST API authentication
maxScalePassword string // Password for maxscale REST API authentication
maxScaleExporterPort string // Port for this exporter to run on
maxScaleCACertificate string // File containing CA certificate
maxctrlExporterConfigFile string // File containing exporter config
)

type ConfigValues struct {
Url string `yaml:"url"`
Username string `yaml:"username"`
Password string `yaml:"password"`
ExporterPort string `yaml:"exporter_port"`
CACertificate string `yaml:"caCertificate"`
}

// MaxScale contains connection parameters to the server and metric maps
type MaxScale struct {
url string
Expand Down Expand Up @@ -316,28 +336,63 @@ func (m *MaxScale) parseThreadStatus(ch chan<- prometheus.Metric) error {
return nil
}

func main() {
maxScaleUrl := os.Getenv("MAXSCALE_URL")
if len(maxScaleUrl) == 0 {
maxScaleUrl = "http://127.0.0.1:8989"
// GetEnvVar - retrieves values of environment variables. If nothing is set, return the default value
func GetEnvVar(envName string, defaultValue string) string {
envVal := os.Getenv(envName)
if envVal == "" {
return defaultValue
}
return envVal
}

maxScaleUsername := os.Getenv("MAXSCALE_USERNAME")
if len(maxScaleUsername) == 0 {
maxScaleUsername = "admin"
func readConfigFile(fname string) {
yamlFile, err := os.ReadFile(fname)
if err != nil {
// If the file doesn't exist, just return
if errors.Is(err, fs.ErrNotExist) {
return
} else {
log.Printf("Could not open configuration file '%s': %v", maxctrlExporterConfigFile, err)
}
}
parseConfigFile(yamlFile)
}

maxScalePassword := os.Getenv("MAXSCALE_PASSWORD")
if len(maxScalePassword) == 0 {
maxScalePassword = "mariadb"
func parseConfigFile(contents []byte) {
var config ConfigValues
err := yaml.Unmarshal(contents, &config)
if err != nil {
log.Fatalf("Could not parse config file contents: %v", err)
}

maxScaleExporterPort := os.Getenv("MAXSCALE_EXPORTER_PORT")
if len(maxScaleExporterPort) == 0 {
maxScaleExporterPort = "8080"
if config.Url != "" {
maxScaleUrl = config.Url
}
if config.Username != "" {
maxScaleUsername = config.Username
}
if config.Password != "" {
maxScalePassword = config.Password
}
if config.ExporterPort != "" {
maxScaleExporterPort = config.ExporterPort
}
if config.CACertificate != "" {
maxScaleCACertificate = config.CACertificate
}
}

func setConfigFromEnvironmentVars() {
maxScaleUrl = GetEnvVar("MAXSCALE_URL", "http://127.0.0.1:8989")
maxScaleUsername = GetEnvVar("MAXSCALE_USERNAME", "admin")
maxScalePassword = GetEnvVar("MAXSCALE_PASSWORD", "mariadb")
maxScaleExporterPort = GetEnvVar("MAXSCALE_EXPORTER_PORT", "8080")
maxScaleCACertificate = GetEnvVar("MAXSCALE_CA_CERTIFICATE", "")
maxctrlExporterConfigFile = GetEnvVar("MAXCTRL_EXPORTER_CFG_FILE", "maxctrl_exporter.yaml")
}

maxScaleCACertificate := os.Getenv("MAXSCALE_CA_CERTIFICATE")
func main() {
setConfigFromEnvironmentVars()
readConfigFile(maxctrlExporterConfigFile)

log.Print("Starting MaxScale exporter")
log.Printf("Scraping MaxScale JSON API at: %s", maxScaleUrl)
Expand Down
5 changes: 5 additions & 0 deletions maxctrl_exporter.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
url: "http://127.0.0.1:8989"
username: "maxctrl_username"
password: "maxctrl_password"
exporterPort: "8080"
caCertificate: ""
185 changes: 185 additions & 0 deletions maxctrl_exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2023 Dylan Northrup [@dylan-tock on github]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"fmt"
"log"
"os"
"testing"
)

// Verify GetEnvVar does what it says on the tin
func TestGetInitializedEnvVar(t *testing.T) {
// Save possibly pre-existing value of env variable for restoration later
envVariableName := "maxctrlExporterTestEnvVariable"
previousValue := os.Getenv(envVariableName)
defer func() { os.Setenv(envVariableName, previousValue) }()
fallback := "fallback value"
envValue := "Test Value"
want := "Test Value"

os.Setenv(envVariableName, envValue)
got := GetEnvVar(envVariableName, fallback)

if want != got {
t.Fatalf("Did not get expected value from env variable. Wanted: '%s'. Got '%s'", want, got)
}
if previousValue == "" {
os.Unsetenv(envVariableName)
} else {
os.Setenv(envVariableName, previousValue)
}
}

func TestGetEmptyEnvVar(t *testing.T) {
// Save possibly pre-existing value of env variable for restoration later
envVariableName := "maxctrlExporterTestEnvVariable"
previousValue := os.Getenv(envVariableName)
defer func() { os.Setenv(envVariableName, previousValue) }()
fallback := "fallback value"
envValue := ""
want := fallback

os.Setenv(envVariableName, envValue)
got := GetEnvVar(envVariableName, fallback)

if want != got {
t.Fatalf("Did not get expected value from env variable. Wanted: '%s'. Got '%s'", want, got)
}
if previousValue == "" {
os.Unsetenv(envVariableName)
} else {
os.Setenv(envVariableName, previousValue)
}
}

func TestGettingConfigFromEnvironment(t *testing.T) {
maxScaleUrl = ""
maxScaleUsername = ""
maxScalePassword = ""
maxScaleExporterPort = ""
maxScaleCACertificate = ""
maxctrlExporterConfigFile = ""

keys := []string{"MAXSCALE_URL", "MAXSCALE_USERNAME", "MAXSCALE_PASSWORD", "MAXSCALE_EXPORTER_PORT",
"MAXSCALE_CA_CERTIFICATE", "MAXCTRL_EXPORTER_CFG_FILE"}

want := map[string]string{
keys[0]: "http://10.10.10.1:8989",
keys[1]: "userMcUserFace",
keys[2]: "secretPassword",
keys[3]: "8080",
keys[4]: "cert.pem",
keys[5]: "exporterConfig.yml",
}

for k, v := range want {
os.Setenv(k, v)
}

setConfigFromEnvironmentVars()
got := make(map[string]string)
got[keys[0]] = maxScaleUrl
got[keys[1]] = maxScaleUsername
got[keys[2]] = maxScalePassword
got[keys[3]] = maxScaleExporterPort
got[keys[4]] = maxScaleCACertificate
got[keys[5]] = maxctrlExporterConfigFile

for _, k := range keys {
if want[k] != got[k] {
log.Fatalf("Config key '%s' had unexpected value. wanted '%s' and got '%s'", k, want[k], got[k])
}
// Unset these as we test
os.Unsetenv(k)
}

for k, v := range want {
if k == "MAXSCALE_EXPORTER_PORT" || k == "MAXCTRL_EXPORTER_CFG_FILE" {
continue
}
os.Setenv(k, v)
}

for _, k := range keys {
if want[k] != got[k] {
log.Fatalf("Config key '%s' had unexpected value. wanted '%s' and got '%s'", k, want[k], got[k])
}
// Unset these as we test
os.Unsetenv(k)
}
}

// Test parsing config contents
func TestConfigParsing(t *testing.T) {
// Pre-initialize the variables
setConfigFromEnvironmentVars()

keys := []string{"url", "username", "password", "exporter_port", "caCertificate"}

want := map[string]string{
keys[0]: "http://10.10.10.1:8989",
keys[1]: "userMcUserFace",
keys[2]: "secretPassword",
keys[3]: "8080",
keys[4]: "",
}

contents := ""
for k, v := range want {
contents = fmt.Sprintf("%s%s: %s\n", contents, k, v)
}

// We have to call this to set up proper default values
parseConfigFile([]byte(contents))

got := make(map[string]string)
got[keys[0]] = maxScaleUrl
got[keys[1]] = maxScaleUsername
got[keys[2]] = maxScalePassword
got[keys[3]] = maxScaleExporterPort
got[keys[4]] = maxScaleCACertificate

for _, k := range keys {
if want[k] != got[k] {
log.Fatalf("Config key '%s' had unexpected value. wanted '%s' and got '%s'", k, want[k], got[k])
}
}

// Redo the test, but with some values missing from the config file
contents = ""
for k, v := range want {
if k == "exporter_port" || k == "caCertificate" {
continue
}
contents = fmt.Sprintf("%s%s: %s\n", contents, k, v)
}

// We have to call this to set up proper default values
setConfigFromEnvironmentVars()
parseConfigFile([]byte(contents))

got[keys[0]] = maxScaleUrl
got[keys[1]] = maxScaleUsername
got[keys[2]] = maxScalePassword
got[keys[3]] = maxScaleExporterPort
got[keys[4]] = maxScaleCACertificate

for _, k := range keys {
if want[k] != got[k] {
log.Fatalf("Config key '%s' had unexpected value. wanted '%s' and got '%s'", k, want[k], got[k])
}
}
}

0 comments on commit 49fd2b7

Please sign in to comment.