Config and logging

This commit is contained in:
Ryan McGuire 2025-01-03 21:09:40 -05:00
parent 3d3b44bfc7
commit b93f8fb6c8
7 changed files with 215 additions and 42 deletions

3
go.mod
View File

@ -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
)

14
go.sum
View File

@ -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=

27
main.go
View File

@ -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() {}

View File

@ -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
}

View File

@ -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
View 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"`
}

View File

@ -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 = ""
}
}