Как стать автором
Обновить

Комментарии 35

Гениально класс круто

Представляю zerocfg — конфигурацию без лишних усилий

Неудачное название потому, что конфликтует с ZeroConf, Zero-configuration networking

ZeroConf - это общепринятое название для этой технологии. Любой сетевик, увидев слово zerocfg, употреблённое совсем в другом смысле, будет несколько озадачен...

я изначально хотел zeroconf, но изза совпадения названия с технологией сделал zerocfg

а в целом название референсит zerolog

Всё равно очень похоже. Мне прям глаза резануло.

спасибо за фидбек:)

Тоже в глаза бросилось такое название и ассоциации. 

Выглядит классно

Но статья менее понятна, чем README на ГитХабе 😬

я в статье старался пройтись по истокам возникновения идеи сделать и не вдаваться в детали, обойтись простыми примерами, чтобы читать было проще:)

Самую важную часть, на мой взгляд, упустили

Собственно, на ГитХаб я полез ради того, чтобы понять как сопоставляются env-переменные и db.password

Будет здорово дополнить статью примерами использования

Спасибо, дополнил)

Если нам нужно подгружать опции из альтернативного источника конфигурации, специфичного для нас или просто не поддержанного в текущий момент, мы можем это сделать!

А что с динамическими источниками вроде etcd?

можно добавить и etcd, но не в динамическом режиме, пакет проектирован так что ключи выставляются один раз вызовом метода Parse и больше его вызывать нельзя

А можно добавить возможность динамического перечитывания файла конфигурации каждые ххх секунд? Например, поменяли путь к логам или уровень сбора логов error -> debug и чтобы не стартовать приложение? Вот это мне всегда требуется ((

Спасибо за вопрос,

я не проектировал пакет под live-reload, он привносит немало болейрплейта как по мне при подходе моем подходе к определению конфигурации.

думаю хорошим вариантом будет какраз viper для вас

Выглядит неплохо.

На практике не пробовал, задаю вопрос исходя из статьи и ридми )

Есть ли механизм деприкейта?

Можно ли задавать префикс для энвов?

Как обрабатывается передача несуществующих значений в конфиге или флагах?

деприкейта нет, не очень понимаю как и для чего его стоит реализовавыть?

\\ Depricated на опцией в коде ведь можно указать, или это про рендер конфигурации чтобы там был варн?

префикс для энвов сейчас нельзя, но это вопрос 3 строчек кода, просто руки еще не дошли, поскольку дополировываю код основного пакета

несуществующие знания триггерят ошибку UnknownFields.. (для энв парсера она не триггерится только)

в статье про это есть, можно найти по ключ слову IsUnknown

В кобре есть hide (скрытые флаги), таким образом можно деприкейтить старые флаги, ну или собственно делать скрытые.

тут с точки зрения использования
- кобра - для кли - юзер взаимодействует с --help
- zerocfg - конфигурация приложения, юзер взаимодействует с кодом

мне кажется что для депрекейта достаточно коммента в коде

мне кажется что для депрекейта достаточно коммента в коде

Имеется в виду, оповещение о том, что пользователь использует deprecated-настройку в конфиге. Тут до кода дело не доходит даже - конфиг может даже не разработчик править

Еще через пару лет разработчики го опять выйдут из анабиоза в своей башне из слоновой кости, оглядятся по сторонам, и поймут, что конфиг должен быть частью официального набора инструментов.

Как предполагается менять значения конфига в тест кейсах?

можно иметь yaml \ env + os.SetEnv и использовать встроенные парсеры env и yaml

можно имплементировать mockSource конфигурации и его использовать, либа в этом плане достаточно гибкая

но как мне кажется нужно decoupling делать конфига и тестируемого кода

Я в своё время наговнокодил библиотеку конфигов для go, но которая работает по другому принципу:


type SomeStruct {
  field1 uint64 `env:"UINT" arg:"uint"`
}
type Config struct {
  field1 string `env:"FIELD1" arg:"field1"`
  field2 *SomeStruct `env:"FIELD2" arg:"field2"`
}

cfg, err := getConfig[Config](FromJson("base.json"), FromArg, FromEnv)

И в итоге получаем конфиг, который загружает параметры из json, если параметр задан в переменных среды, то берёт оттуда, а если заданы через аргументы, то из них.

Параметры во вложенных структурах определяются через имя этой структуры, т.е. в моём примере будут аргументы "--field2_field1" и его эквивалент в виде переменной среды "FIELD2_FIELD1".

Массивы пока не поддерживаются.

Пользуясь случаем, покажу свою, в которой у меня мержатся конфиги из переменных окружения, файлов или строк в формате env, ini и json.

https://github.com/Mansiper/GoConfiguration

type Config struct {
    Field_1 int                `env:"f1,required" def:"10"`
    Field_2 []string           `env:"f2,append" def:"a|b" sep:"|"`
    Field_3 [3]time.Time       `env:"f3" def:"now,now"`
    Field_4 map[string]float64 `env:"f4" def:"a?1.1,b?2.2" sep2:"?"`
    Field_5 SubConfig          `env:"f5"`
    Fiend_6 string             `env:"GOPATH"`
    Fiend_7 *float64           `env:"-"`
    Field_8 []*int8            `env:"f8" def:"*nil,1,2,3,4,5"`
    FIell_9 [2]*float32        `env:"f9,required" def:"1.23"`
}
type SubConfig struct {
    Field_1 *uint32            `env:"sf1" def:"*nil"`
}

func main() {
    config := &Config{}
    cr := goconf.NewConfigReader().
        RewriteValues(true).
        AddEnvironment().
        AddFile(".env").
        AddFile("config.ini").
        AddString("f5.sf1 = 10", goconf.FtEnv, "config 1").
        AddFile("config.json").
        EnsureHasNoErrors()
    err := cr.ReadConfig(config)
    if err != nil {
        panic(err)
    }

    ...
}

На мой взгляд, получалось довольно удобно в использовании.

Есть ещё koanf, у которого можно из желаемых источников собирать параметры в структуру

он в апи похож на viper, мне такое апи кажется бойлерплейтным, однако вплане количества источников у него нет равных и либа koanf хороша в этом плане

Я как раз люблю собирать параметры в какие-то структуры и потом ее использовать, не дергая какой-то объект за методы. Вот пример моего config.go текущего пет-проекта. Т.е. весь боилерплэйт умещается в одном файлике, который кладется рядом с main.go в папку cmd. А в main уже параметры читаются из структуры.

package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/knadh/koanf"
	"github.com/knadh/koanf/parsers/json"
	"github.com/knadh/koanf/parsers/toml"
	"github.com/knadh/koanf/parsers/yaml"
	"github.com/knadh/koanf/providers/confmap"
	"github.com/knadh/koanf/providers/env"
	"github.com/knadh/koanf/providers/file"
	"github.com/spf13/pflag"
)

// Config is application config struct
type Config struct {
	Logger struct {
		Level string `koanf:"level"`
	} `koanf:"logger"`
	Sentry struct {
		DSN              string  `koanf:"dsn"`
		TracesSampleRate float64 `koanf:"tracesSampleRate"`
	} `koanf:"sentry"`
	DB struct {
		URL string `koanf:"url"`
	}
	GRPC struct {
		Port int `koanf:"port"`
	}
	Services []ServiceConfig `koanf:"services"`
}

// ServiceConfig is a config for a service
type ServiceConfig struct {
	Type       string            `koanf:"type"`
	Parameters map[string]string `koanf:"parameters"`
}

const (
	ServiceConfigTypeAsset     = "asset"
	ServiceConfigTypeUser      = "user"
	ServiceConfigTypePrice     = "price"
	ServiceConfigTypePortfolio = "portfolio"
	ServiceConfigTypeTrade     = "trade"
)

func getConfig() (*Config, error) {
	var err error
	k := koanf.New(".")

	// Default values

	defaults := map[string]interface{}{
		"sentry.tracesSampleRate": 1.0,
	}
	err = k.Load(confmap.Provider(defaults, "."), nil)
	if err != nil {
		return nil, fmt.Errorf("can't load default config parameters: %w", err)
	}

	// Load command line and configs

	f := pflag.NewFlagSet("config", pflag.ContinueOnError)
	f.Usage = func() {
		fmt.Println(f.FlagUsages())
		os.Exit(0)
	}
	f.String("c", "", "Path to config file")
	err = f.Parse(os.Args[1:])
	if err != nil {
		return nil, fmt.Errorf("can't parse command line arguments: %w", err)
	}

	// Load the config files provided in the commandline.
	cFile, _ := f.GetString("c")
	switch {
	case strings.HasSuffix(cFile, "toml"):
		if err := k.Load(file.Provider(cFile), toml.Parser()); err != nil {
			return nil, fmt.Errorf("error loading file: %w", err)
		}
	case strings.HasSuffix(cFile, "yaml"):
		if err := k.Load(file.Provider(cFile), yaml.Parser()); err != nil {
			return nil, fmt.Errorf("error loading file: %w", err)
		}
	case strings.HasSuffix(cFile, "json"):
		if err := k.Load(file.Provider(cFile), json.Parser()); err != nil {
			return nil, fmt.Errorf("error loading file: %w", err)
		}
	}

	// Load ENV

	err = k.Load(env.Provider(ServiceName+"_", ".", func(s string) string {
		return strings.ReplaceAll(strings.ToLower(
			strings.TrimPrefix(s, ServiceName+"_")), "_", ".")
	}), nil)
	if err != nil {
		return nil, fmt.Errorf("can't load env variables: %w", err)
	}

	// Unmarshal configs to struct
	var config Config
	err = k.Unmarshal("", &config)
	if err != nil {
		return nil, fmt.Errorf("can't unmarshal config: %w", err)
	}

	return &config, nil
}

Тут еще от нехер делать засунуты томл, json. ОБычно все запускаю в ENV так как с компоуза удобнее. Так что можно еще сократить, но накладные расходы на поддержку парсинга конфигов вряд ли фатальны.

раньше использовал вайпер с коброй. Но там вот реально надо было вроде отдельный init файл делать, директории с подкомандами и прочим. Т.е. уже решения для прям сложных гуевых приложений, в которых есть команды приложения и их собственные параметры и т.д.

это стандартный подход достаточно, он удобен и я не пытаюсь с ним бороться, но все проблемы которые я перечислил - применимы к нему, я сам таким всегда пользовался

var (
	loggerLevel            = zfg.Str("logger.level", "info", "Logger level")
	sentryDSN              = zfg.Str("sentry.dsn", "", "Sentry DSN")
	sentryTracesSampleRate = zfg.Float64("sentry.tracesSampleRate", 1.0, "Sentry traces sample rate")
	dbURL                  = zfg.Str("db.url", "", "Database URL")
	port                   = zfg.Int("grpc.port", 8080, "gRPC port")
	services               = zfg.Map("services", nil, "List of service configs (JSON strings)")
)

func main() {
	err := zfg.Parse(env.New())
	if err != nil {
		panic(err)
	}

	fmt.Println(zfg.Show())
}

как то так будет выглядеть подобная логика на зероконф

только в зероконф нужно обявлять опции конфигурации в пакете где она применяться, а не в одном файле

Ага, так может быть удобнее

Слишком сложно всё тут.

Как должно быть:
1. Создаём структуру
2. Заполняем все поля в структуру автоматически из окружения/.env/.yaml
одной строчкой кода (обязательно одной строчкой)
без заполнения никаких настроек,
если совпадает имя поля структуры и имя переменной окружения - то заполняем.
Весь код будет состоять из 1 строчки кода,
будет универсально для любых типов и названий полей.
(я уже так сделал почти, ещё доделаю)

можно пример API ? не очень понял, как должно выглядеть добавление новыого поля или просто как будет выглядеть конфиг с несколькими опциями конфигурации, по тексту не очень понимаю чем это от koanf отличается

Че только из map[string]any не делают)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации