go-app/pkg/srv/http.go

175 lines
4.2 KiB
Go

package srv
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"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-app/pkg/config"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
)
var (
httpMeter metric.Meter
httpTracer trace.Tracer
defReadTimeout = 10 * time.Second
defWriteTimeout = 10 * time.Second
defIdleTimeout = 15 * time.Second
)
type HTTPFunc struct {
Path string
HandlerFunc http.HandlerFunc
}
type HTTPServerOpts struct {
Ctx context.Context
HandleFuncs []HTTPFunc
Middleware []http.Handler
HealthCheckFuncs []HealthCheckFunc
}
func prepHTTPServer(opts *HTTPServerOpts) *http.Server {
var (
cfg = config.MustFromCtx(opts.Ctx)
l = zerolog.Ctx(opts.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(opts.Ctx, opts.HealthCheckFuncs...)
otelHandleFunc("/health", healthChecks)
otelHandleFunc("/", healthChecks)
for _, f := range opts.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
}
}))
// Set timeouts from defaults, override
// with config timeouts if set
readTimeout := defReadTimeout
writeTimeout := defWriteTimeout
idleTimeout := defIdleTimeout
rT, wT, iT := cfg.HTTP.Timeouts()
if rT != nil {
readTimeout = *rT
}
if wT != nil {
writeTimeout = *wT
}
if iT != nil {
idleTimeout = *iT
}
// Inject any supplied middleware
for i := len(opts.Middleware) - 1; i >= 0; i-- {
mw := opts.Middleware[i]
next := handler
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mw.ServeHTTP(w, r)
next.ServeHTTP(w, r)
})
}
// Inject logging middleware
if cfg.HTTP.LogRequests {
handler = loggingMiddleware(opts.Ctx, handler)
}
return &http.Server{
Addr: cfg.HTTP.Listen,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
Handler: handler,
ErrorLog: log.New(os.Stderr, fmt.Sprintf("Go-HTTP[%s]", cfg.Name), log.Flags()),
BaseContext: func(_ net.Listener) context.Context {
return opts.Ctx
},
}
}
// Returns a shutdown func and a done channel if the
// server aborts abnormally. Panics on error.
func MustInitHTTPServer(opts *HTTPServerOpts) (
func(context.Context) error, <-chan interface{},
) {
shutdownFunc, doneChan, err := InitHTTPServer(opts)
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(opts *HTTPServerOpts) (
func(context.Context) error, <-chan interface{}, error,
) {
l := zerolog.Ctx(opts.Ctx)
doneChan := make(chan interface{})
var server *http.Server
httpMeter = otel.GetMeter(opts.Ctx, "http")
httpTracer = otel.GetTracer(opts.Ctx, "http")
server = prepHTTPServer(opts)
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
}