Added app package and health check funcs
This commit is contained in:
parent
ce2a7ce221
commit
bf3fa2c54c
14
env-sample
Normal file
14
env-sample
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# App Config
|
||||||
|
APP_NAME="go-http-server-with-otel"
|
||||||
|
APP_LOG_LEVEL=trace ## For testing only
|
||||||
|
APP_LOG_FORMAT=console ## console, json
|
||||||
|
APP_LOG_TIME_FORMAT=long ## long, short, unix, rfc3339, off
|
||||||
|
|
||||||
|
# App OTEL Config
|
||||||
|
APP_OTEL_STDOUT_ENABLED=true ## For testing only
|
||||||
|
APP_OTEL_METRIC_INTERVAL_SECS=15
|
||||||
|
|
||||||
|
# OTEL SDK Config
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT="otel-collector.otel.svc.cluster.local" # Set to your otel collector
|
||||||
|
OTEL_SERVICE_NAME="go-http-server-with-otel"
|
||||||
|
OTEL_RESOURCE_ATTRIBUTES="env=development,service.version=(devel)"
|
113
main.go
113
main.go
@ -14,17 +14,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"go.opentelemetry.io/otel/codes"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"golang.org/x/sys/unix"
|
"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/config"
|
||||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/logging"
|
|
||||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/otel"
|
|
||||||
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/srv"
|
"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/srv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,100 +34,29 @@ func main() {
|
|||||||
ctx, cncl := signal.NotifyContext(context.Background(), os.Interrupt, unix.SIGTERM)
|
ctx, cncl := signal.NotifyContext(context.Background(), os.Interrupt, unix.SIGTERM)
|
||||||
defer cncl()
|
defer cncl()
|
||||||
|
|
||||||
shutdownFuncs := make([]func(context.Context) error, 0)
|
|
||||||
|
|
||||||
// Load configuration and setup logging
|
// Load configuration and setup logging
|
||||||
ctx = setupConfigAndLogging(ctx)
|
ctx = app.MustSetupConfigAndLogging(ctx)
|
||||||
|
|
||||||
// Set up OTEL
|
// Prepare app
|
||||||
ctx, otelShutdown := otel.Init(ctx)
|
app := &app.App{
|
||||||
shutdownFuncs = append(shutdownFuncs, otelShutdown)
|
AppContext: ctx,
|
||||||
tracer = otel.MustTracerFromCtx(ctx)
|
HTTP: &app.AppHTTP{
|
||||||
|
Funcs: []srv.HTTPFunc{
|
||||||
// Start App
|
{
|
||||||
ctx, initSpan := tracer.Start(ctx, "init")
|
Path: "/test",
|
||||||
|
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Start HTTP Server
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
dummyFuncs := []srv.HTTPFunc{
|
w.Write([]byte("http test route"))
|
||||||
{
|
},
|
||||||
Path: "/dummy",
|
},
|
||||||
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("hello world"))
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
httpShutdown, httpDone := srv.MustInitHTTPServer(ctx, dummyFuncs...)
|
|
||||||
shutdownFuncs = append(shutdownFuncs, httpShutdown)
|
|
||||||
|
|
||||||
// Startup Complete
|
// Launch app
|
||||||
l.Info().
|
app.MustRun()
|
||||||
Str("name", cfg.Name).
|
|
||||||
Str("version", cfg.Version).
|
|
||||||
Str("logLevel", cfg.Logging.Level).
|
|
||||||
Msg("app initialized")
|
|
||||||
initSpan.SetStatus(codes.Ok, "")
|
|
||||||
initSpan.End()
|
|
||||||
|
|
||||||
// Wait for signal
|
// Wait for app to complete
|
||||||
select {
|
// Perform any extra shutdown here
|
||||||
case <-httpDone:
|
<-app.Done()
|
||||||
l.Warn().Msg("shutting down early on http server done")
|
|
||||||
case <-ctx.Done():
|
|
||||||
l.Warn().Str("reason", ctx.Err().Error()).
|
|
||||||
Msg("shutting down on context done")
|
|
||||||
}
|
|
||||||
shutdown(shutdownFuncs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shutdown(shutdownFuncs ...func(context.Context) error) {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
doneCtx, cncl := context.WithTimeout(context.Background(), 15*time.Second)
|
|
||||||
defer func() {
|
|
||||||
if doneCtx.Err() == context.DeadlineExceeded {
|
|
||||||
l.Err(doneCtx.Err()).
|
|
||||||
Dur("shutdownTime", time.Since(now)).
|
|
||||||
Msg("app shutdown aborted")
|
|
||||||
} else {
|
|
||||||
l.Info().
|
|
||||||
Int("shutdownFuncsCalled", len(shutdownFuncs)).
|
|
||||||
Dur("shutdownTime", time.Since(now)).
|
|
||||||
Msg("app shutdown normally")
|
|
||||||
}
|
|
||||||
cncl()
|
|
||||||
}()
|
|
||||||
|
|
||||||
doneCtx, span := tracer.Start(doneCtx, "shutdown")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(shutdownFuncs))
|
|
||||||
|
|
||||||
for _, f := range shutdownFuncs {
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
err := f(doneCtx)
|
|
||||||
if err != nil {
|
|
||||||
span.SetStatus(codes.Error, "shutdown failed")
|
|
||||||
span.RecordError(err)
|
|
||||||
l.Err(err).Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupConfigAndLogging(ctx context.Context) context.Context {
|
|
||||||
ctx, err := config.LoadConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = config.MustFromCtx(ctx)
|
|
||||||
ctx = logging.MustInitLogging(ctx)
|
|
||||||
l = zerolog.Ctx(ctx)
|
|
||||||
|
|
||||||
l.Trace().Any("config", *cfg).Send()
|
|
||||||
return ctx
|
|
||||||
}
|
}
|
||||||
|
93
pkg/app/app.go
Normal file
93
pkg/app/app.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
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)
|
||||||
|
}
|
67
pkg/app/run.go
Normal file
67
pkg/app/run.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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()
|
||||||
|
}
|
25
pkg/app/setup.go
Normal file
25
pkg/app/setup.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -29,7 +29,7 @@ type HTTPFunc struct {
|
|||||||
HandlerFunc http.HandlerFunc
|
HandlerFunc http.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepHTTPServer(ctx context.Context, handleFuncs ...HTTPFunc) *http.Server {
|
func prepHTTPServer(ctx context.Context, handleFuncs []HTTPFunc, hcFuncs ...HealthCheckFunc) *http.Server {
|
||||||
var (
|
var (
|
||||||
cfg = config.MustFromCtx(ctx)
|
cfg = config.MustFromCtx(ctx)
|
||||||
l = zerolog.Ctx(ctx)
|
l = zerolog.Ctx(ctx)
|
||||||
@ -43,8 +43,9 @@ func prepHTTPServer(ctx context.Context, handleFuncs ...HTTPFunc) *http.Server {
|
|||||||
mux.Handle(pattern, handler) // Associate pattern with handler
|
mux.Handle(pattern, handler) // Associate pattern with handler
|
||||||
}
|
}
|
||||||
|
|
||||||
otelHandleFunc("/health", handleHealthCheckFunc(ctx))
|
healthChecks := handleHealthCheckFunc(ctx, hcFuncs...)
|
||||||
otelHandleFunc("/", handleHealthCheckFunc(ctx))
|
otelHandleFunc("/health", healthChecks)
|
||||||
|
otelHandleFunc("/", healthChecks)
|
||||||
|
|
||||||
for _, f := range handleFuncs {
|
for _, f := range handleFuncs {
|
||||||
otelHandleFunc(f.Path, f.HandlerFunc)
|
otelHandleFunc(f.Path, f.HandlerFunc)
|
||||||
@ -85,8 +86,10 @@ func prepHTTPServer(ctx context.Context, handleFuncs ...HTTPFunc) *http.Server {
|
|||||||
|
|
||||||
// Returns a shutdown func and a done channel if the
|
// Returns a shutdown func and a done channel if the
|
||||||
// server aborts abnormally. Panics on error.
|
// server aborts abnormally. Panics on error.
|
||||||
func MustInitHTTPServer(ctx context.Context, funcs ...HTTPFunc) (func(context.Context) error, <-chan interface{}) {
|
func MustInitHTTPServer(ctx context.Context, funcs []HTTPFunc, hcFuncs ...HealthCheckFunc) (
|
||||||
shutdownFunc, doneChan, err := InitHTTPServer(ctx, funcs...)
|
func(context.Context) error, <-chan interface{},
|
||||||
|
) {
|
||||||
|
shutdownFunc, doneChan, err := InitHTTPServer(ctx, funcs, hcFuncs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -95,7 +98,9 @@ func MustInitHTTPServer(ctx context.Context, funcs ...HTTPFunc) (func(context.Co
|
|||||||
|
|
||||||
// Returns a shutdown func and a done channel if the
|
// Returns a shutdown func and a done channel if the
|
||||||
// server aborts abnormally. Returns error on failure to start
|
// server aborts abnormally. Returns error on failure to start
|
||||||
func InitHTTPServer(ctx context.Context, funcs ...HTTPFunc) (func(context.Context) error, <-chan interface{}, error) {
|
func InitHTTPServer(ctx context.Context, funcs []HTTPFunc, hcFuncs ...HealthCheckFunc) (
|
||||||
|
func(context.Context) error, <-chan interface{}, error,
|
||||||
|
) {
|
||||||
l := zerolog.Ctx(ctx)
|
l := zerolog.Ctx(ctx)
|
||||||
doneChan := make(chan interface{})
|
doneChan := make(chan interface{})
|
||||||
|
|
||||||
@ -104,7 +109,7 @@ func InitHTTPServer(ctx context.Context, funcs ...HTTPFunc) (func(context.Contex
|
|||||||
httpMeter = otel.GetMeter(ctx, "http")
|
httpMeter = otel.GetMeter(ctx, "http")
|
||||||
httpTracer = otel.GetTracer(ctx, "http")
|
httpTracer = otel.GetTracer(ctx, "http")
|
||||||
|
|
||||||
server = prepHTTPServer(ctx, funcs...)
|
server = prepHTTPServer(ctx, funcs, hcFuncs...)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
l.Debug().Msg("HTTP Server Started")
|
l.Debug().Msg("HTTP Server Started")
|
||||||
|
@ -7,39 +7,47 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleHealthCheckFunc(_ context.Context) func(w http.ResponseWriter, r *http.Request) {
|
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 http handle func
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var err error
|
var (
|
||||||
var healthChecksFailed bool
|
healthChecksFailed bool
|
||||||
|
errs error
|
||||||
|
hcWg sync.WaitGroup
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: Insert useful health checks here
|
if len(hcFuncs) < 1 {
|
||||||
// For multiple checks, perform concurrently
|
zerolog.Ctx(ctx).Warn().Msg("no health checks given responding with dummy 200")
|
||||||
// Consider using errors.Join() for multiple checks
|
hcFuncs = append(hcFuncs, dummyHealthCheck)
|
||||||
var hcWg sync.WaitGroup
|
}
|
||||||
for range 5 {
|
|
||||||
hcWg.Add(1)
|
// Run all health check funcs concurrently
|
||||||
|
// log all errors
|
||||||
|
hcWg.Add(len(hcFuncs))
|
||||||
|
for _, hc := range hcFuncs {
|
||||||
go func() {
|
go func() {
|
||||||
defer hcWg.Done()
|
defer hcWg.Done()
|
||||||
err = errors.Join(err, dummyHealthCheck(r.Context()))
|
errs = errors.Join(errs, hc(ctx))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
hcWg.Wait()
|
hcWg.Wait()
|
||||||
if err != nil {
|
|
||||||
|
if errs != nil {
|
||||||
healthChecksFailed = true
|
healthChecksFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Friendly reminder...
|
|
||||||
err = errors.New("WARNING: Unimplemented health-check")
|
|
||||||
|
|
||||||
if healthChecksFailed {
|
if healthChecksFailed {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if errs != nil {
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(errs.Error()))
|
||||||
} else {
|
} else {
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user