Update go-app
This commit is contained in:
parent
bf3fa2c54c
commit
e8e4af8051
14
go.mod
14
go.mod
@ -3,6 +3,7 @@ module gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
gitea.libretechconsulting.com/rmcguire/go-app v0.3.0
|
||||
github.com/caarlos0/env/v9 v9.0.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
|
||||
@ -15,11 +16,12 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.33.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0
|
||||
go.opentelemetry.io/otel/trace v1.33.0
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/sys v0.29.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v11 v11.3.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
)
|
||||
@ -42,11 +44,11 @@ require (
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
|
||||
google.golang.org/grpc v1.69.2 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
google.golang.org/protobuf v1.36.2 // indirect
|
||||
)
|
||||
|
16
go.sum
16
go.sum
@ -1,5 +1,9 @@
|
||||
gitea.libretechconsulting.com/rmcguire/go-app v0.3.0 h1:TSR6oEDBX+83975gmgGgU/cTFgfG999+9N/1h4RAXq0=
|
||||
gitea.libretechconsulting.com/rmcguire/go-app v0.3.0/go.mod h1:wHOWh4O4AMDATQ3WEUYjq5a5bnICPBpu5G6BsNxqN38=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
@ -87,25 +91,37 @@ go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qq
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
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=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
|
||||
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
6
main.go
6
main.go
@ -19,9 +19,9 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/app"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/srv"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/app"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -1,93 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/otel"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/srv"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
AppContext context.Context
|
||||
HTTP *AppHTTP
|
||||
cfg *config.AppConfig
|
||||
l *zerolog.Logger
|
||||
tracer trace.Tracer
|
||||
shutdownFuncs []shutdownFunc
|
||||
appDone chan interface{}
|
||||
}
|
||||
|
||||
type AppHTTP struct {
|
||||
Funcs []srv.HTTPFunc
|
||||
HealthChecks []srv.HealthCheckFunc
|
||||
httpDone <-chan interface{}
|
||||
}
|
||||
|
||||
type (
|
||||
healthCheckFunc func(context.Context) error
|
||||
shutdownFunc func(context.Context) error
|
||||
)
|
||||
|
||||
func (a *App) Done() <-chan interface{} {
|
||||
return a.appDone
|
||||
}
|
||||
|
||||
func (a *App) MustRun() {
|
||||
if a.cfg != nil {
|
||||
panic(errors.New("already ran app trying to run"))
|
||||
}
|
||||
|
||||
// Set up app
|
||||
a.cfg = config.MustFromCtx(a.AppContext)
|
||||
a.l = zerolog.Ctx(a.AppContext)
|
||||
a.shutdownFuncs = make([]shutdownFunc, 0)
|
||||
a.appDone = make(chan interface{})
|
||||
a.HTTP.httpDone = make(chan interface{})
|
||||
|
||||
if len(a.HTTP.Funcs) < 1 {
|
||||
a.l.Warn().Msg("no http funcs provided, only serving health and metrics")
|
||||
}
|
||||
|
||||
// Start OTEL
|
||||
a.initOTEL()
|
||||
var initSpan trace.Span
|
||||
_, initSpan = a.tracer.Start(a.AppContext, "init")
|
||||
|
||||
// Start HTTP
|
||||
a.initHTTP()
|
||||
|
||||
// Monitor app lifecycle
|
||||
go a.run()
|
||||
|
||||
// Startup Complete
|
||||
a.l.Info().
|
||||
Str("name", a.cfg.Name).
|
||||
Str("version", a.cfg.Version).
|
||||
Str("logLevel", a.cfg.Logging.Level).
|
||||
Msg("app initialized")
|
||||
initSpan.SetStatus(codes.Ok, "")
|
||||
initSpan.End()
|
||||
}
|
||||
|
||||
func (a *App) initHTTP() {
|
||||
var httpShutdown shutdownFunc
|
||||
httpShutdown, a.HTTP.httpDone = srv.MustInitHTTPServer(
|
||||
a.AppContext,
|
||||
a.HTTP.Funcs,
|
||||
a.HTTP.HealthChecks...,
|
||||
)
|
||||
a.shutdownFuncs = append(a.shutdownFuncs, httpShutdown)
|
||||
}
|
||||
|
||||
func (a *App) initOTEL() {
|
||||
var otelShutdown shutdownFunc
|
||||
a.AppContext, otelShutdown = otel.Init(a.AppContext)
|
||||
a.shutdownFuncs = append(a.shutdownFuncs, otelShutdown)
|
||||
a.tracer = otel.MustTracerFromCtx(a.AppContext)
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
// Watches contexts and channels for the
|
||||
// app to be finished and calls shutdown once
|
||||
// the app is done
|
||||
func (a *App) run() {
|
||||
select {
|
||||
case <-a.AppContext.Done():
|
||||
a.l.Warn().Str("reason", a.AppContext.Err().Error()).
|
||||
Msg("shutting down on context done")
|
||||
case <-a.HTTP.httpDone:
|
||||
a.l.Warn().Msg("shutting down early on http server done")
|
||||
}
|
||||
a.Shutdown()
|
||||
|
||||
a.appDone <- nil
|
||||
}
|
||||
|
||||
// Typically invoked when AppContext is done
|
||||
// or Server has exited. Not intended to be called
|
||||
// manually
|
||||
func (a *App) Shutdown() {
|
||||
now := time.Now()
|
||||
|
||||
doneCtx, cncl := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer func() {
|
||||
if doneCtx.Err() == context.DeadlineExceeded {
|
||||
a.l.Err(doneCtx.Err()).
|
||||
Dur("shutdownTime", time.Since(now)).
|
||||
Msg("app shutdown aborted")
|
||||
} else {
|
||||
a.l.Info().
|
||||
Int("shutdownFuncsCalled", len(a.shutdownFuncs)).
|
||||
Dur("shutdownTime", time.Since(now)).
|
||||
Msg("app shutdown normally")
|
||||
}
|
||||
cncl()
|
||||
}()
|
||||
|
||||
doneCtx, span := a.tracer.Start(doneCtx, "shutdown")
|
||||
defer span.End()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(a.shutdownFuncs))
|
||||
|
||||
for _, f := range a.shutdownFuncs {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := f(doneCtx)
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, "shutdown failed")
|
||||
span.RecordError(err)
|
||||
a.l.Err(err).Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/logging"
|
||||
)
|
||||
|
||||
// Helper function to return a context loaded up with
|
||||
// config.AppConfig and a logger
|
||||
func MustSetupConfigAndLogging(ctx context.Context) context.Context {
|
||||
ctx, err := config.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
ctx = logging.MustInitLogging(ctx)
|
||||
|
||||
zerolog.Ctx(ctx).Trace().Any("config", *cfg).Send()
|
||||
return ctx
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/caarlos0/env/v9"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 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 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()
|
||||
|
||||
// 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 := newAppConfig()
|
||||
|
||||
if configPath != "" {
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
decoder := yaml.NewDecoder(file)
|
||||
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 {
|
||||
return nil, fmt.Errorf("could not parse environment variables: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getVersion() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
|
||||
return info.Main.Version
|
||||
}
|
||||
return Version
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type appConfigKey uint8
|
||||
|
||||
const appConfigCtxKey appConfigKey = iota
|
||||
|
||||
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 {
|
||||
return nil, errors.New("no config found in context")
|
||||
}
|
||||
|
||||
cfg, ok := ctxData.(*AppConfig)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid config stored in context")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
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:"requestTimeout" 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:"prometheusEnabled" env:"APP_OTEL_PROMETHEUS_ENABLED" envDefault:"true"`
|
||||
PrometheusPath string `yaml:"prometheusPath" env:"APP_OTEL_PROMETHEUS_PATH" envDefault:"/metrics"`
|
||||
StdoutEnabled bool `yaml:"stdoutEnabled" env:"APP_OTEL_STDOUT_ENABLED" envDefault:"false"`
|
||||
MetricIntervalSecs int `yaml:"metricIntervalSecs" env:"APP_OTEL_METRIC_INTERVAL_SECS" envDefault:"15"`
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
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 = ""
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type otelCtxKey uint8
|
||||
|
||||
const (
|
||||
ctxKeyTracer otelCtxKey = iota
|
||||
ctxKeyMeter
|
||||
)
|
||||
|
||||
func MustTracerFromCtx(ctx context.Context) trace.Tracer {
|
||||
ctxData := ctx.Value(ctxKeyTracer)
|
||||
if ctxData == nil {
|
||||
panic(errors.New("no tracer found in context"))
|
||||
}
|
||||
|
||||
tracer, ok := ctxData.(trace.Tracer)
|
||||
if !ok {
|
||||
panic(errors.New("invalid tracer found in context"))
|
||||
}
|
||||
|
||||
return tracer
|
||||
}
|
||||
|
||||
func AddTracerToCtx(ctx context.Context, tracer trace.Tracer) context.Context {
|
||||
ctx = context.WithValue(ctx, ctxKeyTracer, tracer)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func MustMeterFromCtx(ctx context.Context) metric.Meter {
|
||||
ctxData := ctx.Value(ctxKeyMeter)
|
||||
if ctxData == nil {
|
||||
panic(errors.New("no meter found in context"))
|
||||
}
|
||||
|
||||
meter, ok := ctxData.(metric.Meter)
|
||||
if !ok {
|
||||
panic(errors.New("invalid meter found in context"))
|
||||
}
|
||||
|
||||
return meter
|
||||
}
|
||||
|
||||
func AddMeterToCtx(ctx context.Context, meter metric.Meter) context.Context {
|
||||
ctx = context.WithValue(ctx, ctxKeyMeter, meter)
|
||||
return ctx
|
||||
}
|
227
pkg/otel/otel.go
227
pkg/otel/otel.go
@ -1,227 +0,0 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
opentelemetry "go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
|
||||
|
||||
noopMetric "go.opentelemetry.io/otel/metric/noop"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
traceSDK "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
||||
trace "go.opentelemetry.io/otel/trace"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
// OTEL Options
|
||||
var (
|
||||
EnableStdoutExporter Option = enableStdoutExporter{}
|
||||
EnablePrometheusExporter Option = enablePrometheusExporter{}
|
||||
// Overide the default metric export interval
|
||||
WithMetricExportInterval = func(interval time.Duration) Option {
|
||||
return exportInterval{interval: interval}
|
||||
}
|
||||
)
|
||||
|
||||
const defMetricInterval = 15 * time.Second
|
||||
|
||||
// Context must carry config.AppConfig
|
||||
func Init(ctx context.Context) (context.Context, func(context.Context) error) {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
// Nothing to do here if not enabled
|
||||
if !cfg.OTEL.Enabled {
|
||||
opentelemetry.SetMeterProvider(noopMetric.NewMeterProvider())
|
||||
opentelemetry.SetTracerProvider(noop.NewTracerProvider())
|
||||
return ctx, func(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
metricInterval = defMetricInterval
|
||||
options = make([]Option, 0)
|
||||
s = &settings{}
|
||||
shutdownFuncs []func(context.Context) error
|
||||
)
|
||||
|
||||
// Prepare settings for OTEL from configuration
|
||||
if cfg.OTEL.StdoutEnabled {
|
||||
options = append(options, EnableStdoutExporter)
|
||||
}
|
||||
if cfg.OTEL.MetricIntervalSecs > 0 {
|
||||
metricInterval = time.Duration(cfg.OTEL.MetricIntervalSecs) * time.Second
|
||||
}
|
||||
if cfg.OTEL.PrometheusEnabled {
|
||||
options = append(options, EnablePrometheusExporter)
|
||||
}
|
||||
options = append(options,
|
||||
WithMetricExportInterval(metricInterval))
|
||||
|
||||
// Apply settings
|
||||
for _, opt := range options {
|
||||
opt.apply(s)
|
||||
}
|
||||
|
||||
// shutdown calls cleanup functions registered via shutdownFuncs.
|
||||
// The errors from the calls are joined.
|
||||
// Each registered cleanup will be invoked once.
|
||||
shutdown := func(ctx context.Context) error {
|
||||
var err error
|
||||
for _, fn := range shutdownFuncs {
|
||||
err = errors.Join(err, fn(ctx))
|
||||
}
|
||||
shutdownFuncs = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
|
||||
handleErr := func(inErr error) {
|
||||
if err := errors.Join(inErr, shutdown(ctx)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "OTEL Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up meter provider.
|
||||
meterProvider, err := s.newMeterProvider(ctx)
|
||||
if err != nil {
|
||||
handleErr(err)
|
||||
return ctx, shutdown
|
||||
}
|
||||
|
||||
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
|
||||
opentelemetry.SetMeterProvider(meterProvider)
|
||||
|
||||
meter := opentelemetry.Meter(cfg.Name)
|
||||
ctx = AddMeterToCtx(ctx, meter)
|
||||
|
||||
// Set up tracing
|
||||
opentelemetry.SetTextMapPropagator(newPropagator())
|
||||
var tracerProvider *traceSDK.TracerProvider
|
||||
tracerProvider, err = s.newTracerProvider(ctx)
|
||||
if err != nil {
|
||||
handleErr(err)
|
||||
return ctx, shutdown
|
||||
}
|
||||
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
||||
opentelemetry.SetTracerProvider(tracerProvider)
|
||||
|
||||
tracer := opentelemetry.Tracer(cfg.Name)
|
||||
ctx = AddTracerToCtx(ctx, tracer)
|
||||
|
||||
return ctx, shutdown
|
||||
}
|
||||
|
||||
func newPropagator() propagation.TextMapPropagator {
|
||||
return propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *settings) newTracerProvider(ctx context.Context) (traceProvider *traceSDK.TracerProvider, err error) {
|
||||
traceOpts := []traceSDK.TracerProviderOption{
|
||||
traceSDK.WithResource(newResource()),
|
||||
}
|
||||
|
||||
host, set := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
if set && host != "" {
|
||||
exporter, err := otlptracegrpc.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceOpts = append(traceOpts, traceSDK.WithBatcher(exporter))
|
||||
}
|
||||
|
||||
if s.EnableStdoutExporter {
|
||||
stdoutExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceOpts = append(traceOpts, traceSDK.WithBatcher(stdoutExporter))
|
||||
}
|
||||
|
||||
traceProvider = traceSDK.NewTracerProvider(traceOpts...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newResource() *resource.Resource {
|
||||
return resource.NewWithAttributes(semconv.SchemaURL)
|
||||
}
|
||||
|
||||
// Configures meter provider
|
||||
// Always provides a prometheus metrics exporter
|
||||
// Conditionally provides an OTLP metrics exporter
|
||||
func (s *settings) newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {
|
||||
// OTEL Prometheus Exporter
|
||||
exporter, err := prometheus.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricOptions := make([]metric.Option, 0, 5)
|
||||
if s.EnableStdoutExporter {
|
||||
stdoutMetricExporter, err := stdoutmetric.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(metric.NewPeriodicReader(stdoutMetricExporter)),
|
||||
)
|
||||
}
|
||||
|
||||
host, set := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
var otlpExporter *otlpmetricgrpc.Exporter
|
||||
if set && host != "" {
|
||||
if exp, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithInsecure()); err != nil {
|
||||
return nil, fmt.Errorf("otlpmetricgrpc.New: %w", err)
|
||||
} else {
|
||||
otlpExporter = exp
|
||||
}
|
||||
}
|
||||
|
||||
var meterProvider *metric.MeterProvider
|
||||
if otlpExporter != nil {
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(exporter),
|
||||
metric.WithReader(
|
||||
metric.NewPeriodicReader(
|
||||
otlpExporter,
|
||||
metric.WithInterval(s.MetricExportInterval),
|
||||
),
|
||||
),
|
||||
metric.WithResource(newResource()),
|
||||
)
|
||||
} else {
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(exporter),
|
||||
metric.WithResource(newResource()),
|
||||
)
|
||||
}
|
||||
|
||||
meterProvider = metric.NewMeterProvider(metricOptions...)
|
||||
|
||||
return meterProvider, nil
|
||||
}
|
||||
|
||||
// Creates a new tracer from the global opentelemetry provider
|
||||
func NewTracer(options ...trace.TracerOption) trace.Tracer {
|
||||
return opentelemetry.GetTracerProvider().Tracer(
|
||||
os.Getenv("OTEL_SERVICE_NAME"),
|
||||
options...,
|
||||
)
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package otel
|
||||
|
||||
import "time"
|
||||
|
||||
type settings struct {
|
||||
EnableStdoutExporter bool
|
||||
EnablePrometheusExporter bool
|
||||
MetricExportInterval time.Duration
|
||||
}
|
||||
|
||||
type Option interface {
|
||||
apply(*settings)
|
||||
}
|
||||
|
||||
type enableStdoutExporter struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (setting enableStdoutExporter) apply(o *settings) {
|
||||
o.EnableStdoutExporter = true
|
||||
}
|
||||
|
||||
type enablePrometheusExporter struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (setting enablePrometheusExporter) apply(o *settings) {
|
||||
o.EnablePrometheusExporter = true
|
||||
}
|
||||
|
||||
type exportInterval struct {
|
||||
Option
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func (setting exportInterval) apply(o *settings) {
|
||||
o.MetricExportInterval = setting.interval
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
|
||||
)
|
||||
|
||||
func GetTracer(ctx context.Context, components ...string) trace.Tracer {
|
||||
return otel.Tracer(getName(ctx, components...))
|
||||
}
|
||||
|
||||
func GetMeter(ctx context.Context, components ...string) metric.Meter {
|
||||
return otel.Meter(getName(ctx, components...))
|
||||
}
|
||||
|
||||
func getName(ctx context.Context, components ...string) string {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
path := make([]string, 0, len(components)+1)
|
||||
path = append(path, cfg.Name)
|
||||
path = append(path, components...)
|
||||
|
||||
return strings.Join(path, ".")
|
||||
}
|
131
pkg/srv/http.go
131
pkg/srv/http.go
@ -1,131 +0,0 @@
|
||||
package srv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
|
||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/otel"
|
||||
)
|
||||
|
||||
var (
|
||||
httpMeter metric.Meter
|
||||
httpTracer trace.Tracer
|
||||
readTimeout = 10 * time.Second
|
||||
writeTimeout = 10 * time.Second
|
||||
idleTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type HTTPFunc struct {
|
||||
Path string
|
||||
HandlerFunc http.HandlerFunc
|
||||
}
|
||||
|
||||
func prepHTTPServer(ctx context.Context, handleFuncs []HTTPFunc, hcFuncs ...HealthCheckFunc) *http.Server {
|
||||
var (
|
||||
cfg = config.MustFromCtx(ctx)
|
||||
l = zerolog.Ctx(ctx)
|
||||
mux = &http.ServeMux{}
|
||||
)
|
||||
|
||||
// NOTE: Wraps handle func with otelhttp handler and
|
||||
// inserts route tag
|
||||
otelHandleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
|
||||
handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
|
||||
mux.Handle(pattern, handler) // Associate pattern with handler
|
||||
}
|
||||
|
||||
healthChecks := handleHealthCheckFunc(ctx, hcFuncs...)
|
||||
otelHandleFunc("/health", healthChecks)
|
||||
otelHandleFunc("/", healthChecks)
|
||||
|
||||
for _, f := range handleFuncs {
|
||||
otelHandleFunc(f.Path, f.HandlerFunc)
|
||||
}
|
||||
|
||||
// Prometheus metrics endpoint
|
||||
if cfg.OTEL.PrometheusEnabled {
|
||||
mux.Handle(cfg.OTEL.PrometheusPath, promhttp.Handler())
|
||||
l.Info().Str("prometheusPath", cfg.OTEL.PrometheusPath).
|
||||
Msg("mounted prometheus metrics endpoint")
|
||||
}
|
||||
|
||||
// Add OTEL, skip health-check spans
|
||||
// NOTE: Add any other span exclusions here
|
||||
handler := otelhttp.NewHandler(mux, "/",
|
||||
otelhttp.WithFilter(func(r *http.Request) bool {
|
||||
switch r.URL.Path {
|
||||
case "/health":
|
||||
return false
|
||||
case cfg.OTEL.PrometheusPath:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}))
|
||||
|
||||
return &http.Server{
|
||||
Addr: cfg.HTTP.Listen,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
IdleTimeout: idleTimeout,
|
||||
Handler: handler,
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a shutdown func and a done channel if the
|
||||
// server aborts abnormally. Panics on error.
|
||||
func MustInitHTTPServer(ctx context.Context, funcs []HTTPFunc, hcFuncs ...HealthCheckFunc) (
|
||||
func(context.Context) error, <-chan interface{},
|
||||
) {
|
||||
shutdownFunc, doneChan, err := InitHTTPServer(ctx, funcs, hcFuncs...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return shutdownFunc, doneChan
|
||||
}
|
||||
|
||||
// Returns a shutdown func and a done channel if the
|
||||
// server aborts abnormally. Returns error on failure to start
|
||||
func InitHTTPServer(ctx context.Context, funcs []HTTPFunc, hcFuncs ...HealthCheckFunc) (
|
||||
func(context.Context) error, <-chan interface{}, error,
|
||||
) {
|
||||
l := zerolog.Ctx(ctx)
|
||||
doneChan := make(chan interface{})
|
||||
|
||||
var server *http.Server
|
||||
|
||||
httpMeter = otel.GetMeter(ctx, "http")
|
||||
httpTracer = otel.GetTracer(ctx, "http")
|
||||
|
||||
server = prepHTTPServer(ctx, funcs, hcFuncs...)
|
||||
|
||||
go func() {
|
||||
l.Debug().Msg("HTTP Server Started")
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
l.Err(err).Msg("HTTP server error")
|
||||
} else {
|
||||
l.Info().Msg("HTTP server shut down")
|
||||
}
|
||||
doneChan <- nil
|
||||
}()
|
||||
|
||||
// Shut down http server with a deadline
|
||||
return func(shutdownCtx context.Context) error {
|
||||
l.Debug().Msg("stopping http server")
|
||||
server.Shutdown(shutdownCtx)
|
||||
return nil
|
||||
}, doneChan, nil
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package srv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type HealthCheckFunc func(context.Context) error
|
||||
|
||||
func handleHealthCheckFunc(ctx context.Context, hcFuncs ...HealthCheckFunc) func(w http.ResponseWriter, r *http.Request) {
|
||||
// Return http handle func
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
healthChecksFailed bool
|
||||
errs error
|
||||
hcWg sync.WaitGroup
|
||||
)
|
||||
|
||||
if len(hcFuncs) < 1 {
|
||||
zerolog.Ctx(ctx).Warn().Msg("no health checks given responding with dummy 200")
|
||||
hcFuncs = append(hcFuncs, dummyHealthCheck)
|
||||
}
|
||||
|
||||
// Run all health check funcs concurrently
|
||||
// log all errors
|
||||
hcWg.Add(len(hcFuncs))
|
||||
for _, hc := range hcFuncs {
|
||||
go func() {
|
||||
defer hcWg.Done()
|
||||
errs = errors.Join(errs, hc(ctx))
|
||||
}()
|
||||
}
|
||||
hcWg.Wait()
|
||||
|
||||
if errs != nil {
|
||||
healthChecksFailed = true
|
||||
}
|
||||
|
||||
if healthChecksFailed {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if errs != nil {
|
||||
w.Write([]byte(errs.Error()))
|
||||
} else {
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dummyHealthCheck(ctx context.Context) error {
|
||||
workFor := rand.Intn(750)
|
||||
ticker := time.NewTicker(time.Duration(time.Duration(workFor) * time.Millisecond))
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user