package log import ( "fmt" "os" "strconv" "strings" "github.com/juju/errors" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" "git.dev.m-and-m.ovh/mderasse/gocommon/convert" "git.dev.m-and-m.ovh/mderasse/gocommon/log/hooks/file" "git.dev.m-and-m.ovh/mderasse/gocommon/log/hooks/gelf" ) // envPrefix is the prefix that will be used for any environnement variable used to configure the logging system. const envPrefix = "LOG_" // CONFIG_FILE is the default file that will be searched to apply configuration from the filesystem. const defaultConfigFile = "log.yaml" // SECRET_NAME is the default name of the secret that will be searched in vault. const defaultSecretName = "log" // ConfigStruct represent the configuration of our logger system. type ConfigStruct struct { EnableStdOut *bool `yaml:"ensable_std_out"` ExtrasFields map[string]interface{} `yaml:"extra_fields"` FileConfig *file.ConfigStruct `yaml:"file_config"` GelfConfig *gelf.ConfigStruct `yaml:"gelf_config"` Level *string `yaml:"level"` Providers []ProviderName `yaml:"providers"` } func newDefaultConfig() *ConfigStruct { return &ConfigStruct{ Level: convert.ToPointer("debug"), EnableStdOut: convert.ToPointer(true), Providers: []ProviderName{ProviderName_NONE}, } } func loadConfigFromVault(secret string) (*ConfigStruct, error) { return nil, os.ErrNotExist } // loadConfigFromFile will read the given file and return a config struct. func loadConfigFromFile(path string) (*ConfigStruct, error) { if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, errors.Trace(err) } //nolint:gosec // we did compute the file path f, err := os.ReadFile(path) if err != nil { return nil, errors.Trace(err) } var config ConfigStruct if err := yaml.Unmarshal(f, &config); err != nil { return nil, errors.Trace(err) } return &config, nil } func loadConfig() (*ConfigStruct, error) { c, err := loadConfigFromVault(defaultSecretName) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } else if err == nil { return c, nil } c, err = loadConfigFromFile(defaultConfigFile) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } else if err == nil { return c, nil } return newDefaultConfig(), nil } // applyEnv will retrieve info from environment variable and overide those got by config / vault. // validity of the value is not checked here and will be check in a IsValid method. func (c *ConfigStruct) applyEnv() error { if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "LEVEL")); v != "" { c.Level = convert.ToPointer(v) } if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "ENABLE_STDOUT")); v != "" { b, err := strconv.ParseBool(v) if err != nil { return errors.NewNotValid(err, fmt.Sprintf("Invalid %s%s environment variable. Should be a boolean", envPrefix, "ENABLE_STDOUT")) } c.EnableStdOut = convert.ToPointer(b) } if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "PROVIDERS")); v != "" { providersStr := strings.Split(v, ",") var providers []ProviderName for _, ps := range providersStr { providers = append(providers, ProviderName(ps)) } c.Providers = providers } // Extra Fields if v := os.Getenv(fmt.Sprintf("%s%s", envPrefix, "EXTRA_FIELDS")); v != "" { extraFieldsPart := strings.Split(v, ",") for _, efp := range extraFieldsPart { extraFieldKV := strings.SplitN(efp, ":", 1) if len(extraFieldKV) != 2 { return errors.NotValidf(fmt.Sprintf("Invalid extra_field %s in environment variable. Should be a key1:value1,key2:value2 format", efp)) } c.ExtrasFields[extraFieldKV[0]] = extraFieldKV[1] } } return nil } // applyDefault will check the gaven config and will apply default on empty field. func (c *ConfigStruct) applyDefault() { defaultConfig := newDefaultConfig() if c.Level == nil || *c.Level == "" { c.Level = defaultConfig.Level } if c.EnableStdOut == nil { c.EnableStdOut = defaultConfig.EnableStdOut } if c.Providers == nil || len(c.Providers) == 0 { c.Providers = defaultConfig.Providers } } // IsValid will check a config struct. func (c *ConfigStruct) IsValid() error { if c.EnableStdOut == nil { return errors.NotValidf("EnableStdOut is empty") } if c.Level == nil { return errors.NotValidf("Level is empty") } else if _, err := logrus.ParseLevel(*c.Level); err != nil { return errors.NotValidf("Invalid Level. Allowed values: %s", strings.Join(convert.StringerSliceToStringSlice(logrus.AllLevels), ", ")) } if c.Providers == nil || len(c.Providers) == 0 { return errors.NotValidf("Providers is empty") } // Validating configuration for the chosen provider. for _, provider := range c.Providers { if !provider.IsValid() { return errors.NotValidf("Invalid Provider %s. Allowed values: %s", provider.String(), strings.Join(convert.StringerSliceToStringSlice(GetListProviderName()), ", ")) } switch provider { case ProviderName_FILE: if c.FileConfig == nil { return errors.NotValidf("file configuration is empty") } err := c.FileConfig.IsValid() if err != nil { return errors.Trace(err) } case ProviderName_GELF: if c.GelfConfig == nil { return errors.NotValidf("GELF configuration is empty") } err := c.GelfConfig.IsValid() if err != nil { return errors.Trace(err) } case ProviderName_NONE: if !*c.EnableStdOut { return errors.NotValidf("Provider set to none with StdOut disabled make no sense") } default: return errors.NotValidf("Provider not handled") } } return nil }