From b93f8fb6c87657fc7aa4c5bd93250ac37d96b3c1 Mon Sep 17 00:00:00 2001 From: Ryan D McGuire Date: Fri, 3 Jan 2025 21:09:40 -0500 Subject: [PATCH] Config and logging --- go.mod | 3 ++ go.sum | 14 +++++++++ main.go | 27 ++++++++++------ pkg/config/config.go | 66 +++++++++++++++++++-------------------- pkg/config/ctx.go | 8 +++++ pkg/config/types.go | 68 ++++++++++++++++++++++++++++++++++++++++ pkg/logging/logging.go | 71 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 pkg/config/types.go diff --git a/go.mod b/go.mod index 4078b22..393ebf5 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ go 1.23.4 require ( github.com/caarlos0/env/v9 v9.0.0 + github.com/rs/zerolog v1.33.0 golang.org/x/sys v0.28.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/kr/pretty v0.3.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index bdeffec..5cca5f2 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,27 @@ github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 9a16fc0..e6e3202 100644 --- a/main.go +++ b/main.go @@ -4,35 +4,44 @@ // spans and metrics to an OTEL collector if configured // to do so. Will also stand up a prometheus metrics // endpoint. +// +// Configuration and logger stored in context package main import ( "context" - "fmt" "os" "os/signal" + "github.com/rs/zerolog" "golang.org/x/sys/unix" "gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/logging" ) func main() { ctx, cncl := signal.NotifyContext(context.Background(), os.Interrupt, unix.SIGTERM) defer cncl() - conf, err := config.LoadConfig() - if err != nil { - panic(err) - } - ctx = conf.AddToCtx(ctx) - - conf, err = config.FromCtx(ctx) + // Set up app config and logging + ctx, err := config.LoadConfig(ctx) if err != nil { panic(err) } - fmt.Printf("%#v\n", conf) + cfg := config.MustFromCtx(ctx) + ctx = logging.MustInitLogging(ctx) + + l := zerolog.Ctx(ctx) + l.Trace().Any("config", *cfg).Send() + l.Info(). + Str("name", cfg.Name). + Str("version", cfg.Version). + Str("logLevel", cfg.Logging.Level). + Msg("app initialized") + + <-ctx.Done() } func init() {} diff --git a/pkg/config/config.go b/pkg/config/config.go index 492695d..e919b1b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 } diff --git a/pkg/config/ctx.go b/pkg/config/ctx.go index b5dc76c..b451469 100644 --- a/pkg/config/ctx.go +++ b/pkg/config/ctx.go @@ -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 { diff --git a/pkg/config/types.go b/pkg/config/types.go new file mode 100644 index 0000000..87cb31e --- /dev/null +++ b/pkg/config/types.go @@ -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"` +} diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 2b43acc..58c86e8 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -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 = "" + } +}