Config and logging
This commit is contained in:
@ -1,52 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/caarlos0/env/v9"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `yaml:"name" env:"APP_NAME" envDefault:"go-http-server-with-otel"`
|
||||
Environment string `yaml:"environment" env:"APP_ENVIRONMENT" envDefault:"development"`
|
||||
Version string `yaml:"version" env:"APP_VERSION"`
|
||||
Logging LogConfig `yaml:"logging"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
OTEL OTELConfig `yaml:"otel"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Enabled bool `yaml:"enabled" env:"APP_LOGGING_ENABLED" envDefault:"true"`
|
||||
Level string `yaml:"level" env:"APP_LOGGING_LEVEL" envDefault:"info"`
|
||||
Format string `yaml:"format" env:"APP_LOGGING_FORMAT" envDefault:"json"`
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Listen string `yaml:"listen" env:"APP_HTTP_LISTEN" envDefault:"127.0.0.1:8080"`
|
||||
RequestTimeout int `yaml:"request_timeout" env:"APP_HTTP_REQUEST_TIMEOUT" envDefault:"30"`
|
||||
}
|
||||
|
||||
type OTELConfig struct {
|
||||
Enabled bool `yaml:"enabled" env:"APP_OTEL_ENABLED" envDefault:"true"`
|
||||
PrometheusEnabled bool `yaml:"prometheus_enabled" env:"APP_OTEL_PROMETHEUS_ENABLED" envDefault:"true"`
|
||||
PrometheusPath string `yaml:"prometheus_path" env:"APP_OTEL_PROMETHEUS_PATH" envDefault:"/metrics"`
|
||||
}
|
||||
// To be set by ldflags in go build command or
|
||||
// retrieved from build meta below
|
||||
var Version = "(devel)"
|
||||
|
||||
// Calling this will try to load from config if -config is
|
||||
// provided, otherwise will return *AppConfig with defaults,
|
||||
// performing any environment substitutions
|
||||
func LoadConfig() (*AppConfig, error) {
|
||||
// provided as a file, and will apply any environment overrides
|
||||
// on-top of configuration defaults.
|
||||
// Config is stored in returned context, and can be retrieved
|
||||
// using config.FromCtx(ctx)
|
||||
func LoadConfig(ctx context.Context) (context.Context, error) {
|
||||
configPath := flag.String("config", "", "Path to the configuration file")
|
||||
flag.Parse()
|
||||
|
||||
return loadConfig(*configPath)
|
||||
// Start with defaults
|
||||
// Load from config if provided
|
||||
// Layer on environment
|
||||
cfg, err := loadConfig(*configPath)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Add config to context, and return
|
||||
// an updated context
|
||||
ctx = cfg.AddToCtx(ctx)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func loadConfig(configPath string) (*AppConfig, error) {
|
||||
cfg := AppConfig{}
|
||||
cfg := newAppConfig()
|
||||
|
||||
if configPath != "" {
|
||||
file, err := os.Open(configPath)
|
||||
@ -56,14 +49,21 @@ func loadConfig(configPath string) (*AppConfig, error) {
|
||||
defer file.Close()
|
||||
|
||||
decoder := yaml.NewDecoder(file)
|
||||
if err := decoder.Decode(&cfg); err != nil {
|
||||
if err := decoder.Decode(cfg); err != nil {
|
||||
return nil, fmt.Errorf("could not decode config file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
if err := env.Parse(cfg); err != nil {
|
||||
return nil, fmt.Errorf("could not parse environment variables: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getVersion() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
|
||||
return info.Main.Version
|
||||
}
|
||||
return Version
|
||||
}
|
||||
|
@ -13,6 +13,14 @@ func (a *AppConfig) AddToCtx(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, appConfigCtxKey, a)
|
||||
}
|
||||
|
||||
func MustFromCtx(ctx context.Context) *AppConfig {
|
||||
cfg, err := FromCtx(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func FromCtx(ctx context.Context) (*AppConfig, error) {
|
||||
ctxData := ctx.Value(appConfigCtxKey)
|
||||
if ctxData == nil {
|
||||
|
68
pkg/config/types.go
Normal file
68
pkg/config/types.go
Normal file
@ -0,0 +1,68 @@
|
||||
package config
|
||||
|
||||
func newAppConfig() *AppConfig {
|
||||
return &AppConfig{
|
||||
Version: getVersion(),
|
||||
Logging: &LogConfig{},
|
||||
HTTP: &HTTPConfig{},
|
||||
OTEL: &OTELConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `yaml:"name" env:"APP_NAME" envDefault:"go-http-server-with-otel"`
|
||||
Environment string `yaml:"environment" env:"APP_ENVIRONMENT" envDefault:"development"`
|
||||
// This should either be set by ldflags, such as with
|
||||
// go build -ldflags "-X gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config.Version=$(VERSION)"
|
||||
// or allow this to use build meta. Will default to (devel)
|
||||
Version string `yaml:"version" env:"APP_VERSION"`
|
||||
Logging *LogConfig `yaml:"logging"`
|
||||
HTTP *HTTPConfig `yaml:"http"`
|
||||
OTEL *OTELConfig `yaml:"otel"`
|
||||
}
|
||||
|
||||
// Logging Configuration
|
||||
type LogConfig struct {
|
||||
Enabled bool `yaml:"enabled" env:"APP_LOG_ENABLED" envDefault:"true"`
|
||||
Level string `yaml:"level" env:"APP_LOG_LEVEL" envDefault:"info"`
|
||||
Format LogFormat `yaml:"format" env:"APP_LOG_FORMAT" envDefault:"json"`
|
||||
Output LogOutput `yaml:"output" env:"APP_LOG_OUTPUT" envDefault:"stderr"`
|
||||
TimeFormat TimeFormat `yaml:"timeFormat" env:"APP_LOG_TIME_FORMAT" envDefault:"short"`
|
||||
}
|
||||
|
||||
type LogFormat string
|
||||
|
||||
const (
|
||||
LogFormatConsole LogFormat = "console"
|
||||
LogFormatJSON LogFormat = "json"
|
||||
)
|
||||
|
||||
type TimeFormat string
|
||||
|
||||
const (
|
||||
TimeFormatShort TimeFormat = "short"
|
||||
TimeFormatLong TimeFormat = "long"
|
||||
TimeFormatUnix TimeFormat = "unix"
|
||||
TimeFormatRFC3339 TimeFormat = "rfc3339"
|
||||
TimeFormatOff TimeFormat = "off"
|
||||
)
|
||||
|
||||
type LogOutput string
|
||||
|
||||
const (
|
||||
LogOutputStdout LogOutput = "stdout"
|
||||
LogOutputStderr LogOutput = "stderr"
|
||||
)
|
||||
|
||||
// HTTP Configuration
|
||||
type HTTPConfig struct {
|
||||
Listen string `yaml:"listen" env:"APP_HTTP_LISTEN" envDefault:"127.0.0.1:8080"`
|
||||
RequestTimeout int `yaml:"request_timeout" env:"APP_HTTP_REQUEST_TIMEOUT" envDefault:"30"`
|
||||
}
|
||||
|
||||
// OTEL Configuration
|
||||
type OTELConfig struct {
|
||||
Enabled bool `yaml:"enabled" env:"APP_OTEL_ENABLED" envDefault:"true"`
|
||||
PrometheusEnabled bool `yaml:"prometheus_enabled" env:"APP_OTEL_PROMETHEUS_ENABLED" envDefault:"true"`
|
||||
PrometheusPath string `yaml:"prometheus_path" env:"APP_OTEL_PROMETHEUS_PATH" envDefault:"/metrics"`
|
||||
}
|
@ -1 +1,72 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
|
||||
)
|
||||
|
||||
func MustInitLogging(ctx context.Context) context.Context {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
logger, err := configureLogger(cfg.Logging)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return logger.WithContext(ctx)
|
||||
}
|
||||
|
||||
func configureLogger(cfg *config.LogConfig) (*zerolog.Logger, error) {
|
||||
setTimeFormat(cfg.TimeFormat)
|
||||
|
||||
// Default JSON logger
|
||||
logger := zerolog.New(os.Stderr)
|
||||
if cfg.TimeFormat != config.TimeFormatOff {
|
||||
logger = logger.With().Timestamp().Logger()
|
||||
}
|
||||
|
||||
// Pretty console logger
|
||||
if cfg.Format == config.LogFormatConsole {
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: zerolog.TimeFieldFormat,
|
||||
}
|
||||
if cfg.TimeFormat == config.TimeFormatOff {
|
||||
consoleWriter.FormatTimestamp = func(_ interface{}) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
logger = log.Output(consoleWriter)
|
||||
}
|
||||
|
||||
level, err := zerolog.ParseLevel(cfg.Level)
|
||||
if err != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
logger = logger.Level(level)
|
||||
zerolog.SetGlobalLevel(level)
|
||||
|
||||
return &logger, err
|
||||
}
|
||||
|
||||
func setTimeFormat(format config.TimeFormat) {
|
||||
switch format {
|
||||
case config.TimeFormatShort:
|
||||
zerolog.TimeFieldFormat = time.Kitchen
|
||||
case config.TimeFormatUnix:
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
case config.TimeFormatLong:
|
||||
zerolog.TimeFieldFormat = time.DateTime
|
||||
case config.TimeFormatRFC3339:
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
case config.TimeFormatOff:
|
||||
zerolog.TimeFieldFormat = ""
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user