Config and logging
This commit is contained in:
parent
3d3b44bfc7
commit
b93f8fb6c8
3
go.mod
3
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
|
||||
)
|
||||
|
14
go.sum
14
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=
|
||||
|
27
main.go
27
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() {}
|
||||
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user