Reference implementation
This commit is contained in:
126
pkg/srv/http.go
Normal file
126
pkg/srv/http.go
Normal file
@ -0,0 +1,126 @@
|
||||
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) *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
|
||||
}
|
||||
|
||||
otelHandleFunc("/health", handleHealthCheckFunc(ctx))
|
||||
otelHandleFunc("/", handleHealthCheckFunc(ctx))
|
||||
|
||||
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) (func(context.Context) error, <-chan interface{}) {
|
||||
shutdownFunc, doneChan, err := InitHTTPServer(ctx, funcs...)
|
||||
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) (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...)
|
||||
|
||||
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
|
||||
}
|
59
pkg/srv/http_health.go
Normal file
59
pkg/srv/http_health.go
Normal file
@ -0,0 +1,59 @@
|
||||
package srv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func handleHealthCheckFunc(_ context.Context) func(w http.ResponseWriter, r *http.Request) {
|
||||
// Return http handle func
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var healthChecksFailed bool
|
||||
|
||||
// TODO: Insert useful health checks here
|
||||
// For multiple checks, perform concurrently
|
||||
// Consider using errors.Join() for multiple checks
|
||||
var hcWg sync.WaitGroup
|
||||
for range 5 {
|
||||
hcWg.Add(1)
|
||||
go func() {
|
||||
defer hcWg.Done()
|
||||
err = errors.Join(err, dummyHealthCheck(r.Context()))
|
||||
}()
|
||||
}
|
||||
hcWg.Wait()
|
||||
if err != nil {
|
||||
healthChecksFailed = true
|
||||
}
|
||||
|
||||
// TODO: Friendly reminder...
|
||||
err = errors.New("WARNING: Unimplemented health-check")
|
||||
|
||||
if healthChecksFailed {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.Write([]byte(err.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()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user