package config
import (
"errors"
"fmt"
"os"
"path"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type LoggerConfig struct {
Verbose bool `mapstructure:"verbose"`
Format string `mapstructure:"format"`
}
func (c *LoggerConfig) MarshalZerologObject(e *zerolog.Event) {
e.
Bool("verbose", c.Verbose).
Str("format", c.Format)
}
type SomethingClientConfig struct {
Value int `mapstructure:"value"`
}
func (c *SomethingClientConfig) MarshalZerologObject(event *zerolog.Event) {
event.
Int("value", c.Value)
}
type BusinessLogicConfig struct {
Threshold int `mapstructure:"threshold"`
}
func (c *BusinessLogicConfig) MarshalZerologObject(event *zerolog.Event) {
event.
Int("threshold", c.Threshold)
}
type MainConfig struct {
Something SomethingClientConfig `mapstructure:"something"`
BusinessLogic BusinessLogicConfig `mapstructure:"bl"`
Logger LoggerConfig `mapstructure:"logger"`
configFile string
}
func (c *MainConfig) MarshalZerologObject(event *zerolog.Event) {
event.
Str("config-file", c.configFile).
Object("logger", &c.Logger).
Object("something", &c.Something).
Object("bl", &c.BusinessLogic)
}
func addLoggerOptions() {
pflag.Bool("verbose", false, "log at debug level")
viper.BindPFlag("logger.verbose", pflag.Lookup("verbose"))
pflag.String("log-format", "json", "logging format (json, console)")
viper.BindPFlag("logger.format", pflag.Lookup("log-format"))
}
func addSomethingOptions() {
pflag.Int("something-value", 123, "value of something")
viper.BindPFlag("something.value", pflag.Lookup("something-value"))
viper.BindEnv("something.value", "SOMETHING_VALUE")
}
func addBLOptions() {
pflag.Int("bl-threshold", 100, "threshold for a business logic action")
viper.BindPFlag("bl.threshold", pflag.Lookup("bl-threshold"))
viper.BindEnv("bl.threshold", "BUSINESS_THRESHOLD")
}
func addConfigFromFile(configRelPath string) error {
mode := os.Getenv("RUN_MODE")
if mode == "" {
mode = "development"
}
viper.SetConfigName("example." + mode)
myName, err := os.Executable()
if err == nil && !strings.HasPrefix(configRelPath, "/") {
myDir := path.Dir(myName)
viper.AddConfigPath(path.Join(myDir, configRelPath))
} else {
viper.AddConfigPath(configRelPath)
}
err = viper.ReadInConfig()
if err != nil {
var notFound viper.ConfigFileNotFoundError
if !errors.As(err, ¬Found) {
return fmt.Errorf("failed to read config file at %s: %w", viper.ConfigFileUsed(), err)
}
}
return nil
}
func loadConfigFromFile() (MainConfig, error) {
path := os.Getenv("EXAMPLE_CONFIG")
if path == "" {
path = "."
}
err := addConfigFromFile(path)
if err != nil {
return MainConfig{}, err
}
var config MainConfig
err = viper.Unmarshal(&config, viper.DecodeHook(
mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToIPHookFunc(),
),
))
if err != nil {
return MainConfig{}, fmt.Errorf("failed to parse options: %w", err)
}
config.configFile = viper.ConfigFileUsed()
return config, nil
}
func GetMainConfig() (MainConfig, error) {
addLoggerOptions()
addSomethingOptions()
addBLOptions()
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
return loadConfigFromFile()
}