go-app/pkg/srv/http.go

171 lines
4.1 KiB
Go
Raw Normal View History

2025-01-04 12:24:42 -05:00
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-app/pkg/config"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
)
var (
2025-01-05 15:35:27 -05:00
httpMeter metric.Meter
httpTracer trace.Tracer
defReadTimeout = 10 * time.Second
defWriteTimeout = 10 * time.Second
defIdleTimeout = 15 * time.Second
2025-01-04 12:24:42 -05:00
)
type HTTPFunc struct {
Path string
HandlerFunc http.HandlerFunc
}
2025-01-28 14:32:27 -05:00
type HTTPServerOpts struct {
Ctx context.Context
HandleFuncs []HTTPFunc
Middleware []http.Handler
HealthCheckFuncs []HealthCheckFunc
}
func prepHTTPServer(opts *HTTPServerOpts) *http.Server {
2025-01-04 12:24:42 -05:00
var (
2025-01-28 14:32:27 -05:00
cfg = config.MustFromCtx(opts.Ctx)
l = zerolog.Ctx(opts.Ctx)
2025-01-04 12:24:42 -05:00
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
}
2025-01-28 14:32:27 -05:00
healthChecks := handleHealthCheckFunc(opts.Ctx, opts.HealthCheckFuncs...)
2025-01-04 12:24:42 -05:00
otelHandleFunc("/health", healthChecks)
otelHandleFunc("/", healthChecks)
2025-01-28 14:32:27 -05:00
for _, f := range opts.HandleFuncs {
2025-01-04 12:24:42 -05:00
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
}
}))
2025-01-05 15:35:27 -05:00
// 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
}
2025-01-28 14:32:27 -05:00
// 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)
})
}
2025-01-05 16:22:15 -05:00
// Inject logging middleware
if cfg.HTTP.LogRequests {
2025-01-28 14:32:27 -05:00
handler = loggingMiddleware(opts.Ctx, handler)
2025-01-05 16:22:15 -05:00
}
2025-01-04 12:24:42 -05:00
return &http.Server{
Addr: cfg.HTTP.Listen,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
Handler: handler,
BaseContext: func(_ net.Listener) context.Context {
2025-01-28 14:32:27 -05:00
return opts.Ctx
2025-01-04 12:24:42 -05:00
},
}
}
// Returns a shutdown func and a done channel if the
// server aborts abnormally. Panics on error.
2025-01-28 14:32:27 -05:00
func MustInitHTTPServer(opts *HTTPServerOpts) (
2025-01-04 12:24:42 -05:00
func(context.Context) error, <-chan interface{},
) {
2025-01-28 14:32:27 -05:00
shutdownFunc, doneChan, err := InitHTTPServer(opts)
2025-01-04 12:24:42 -05:00
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
2025-01-28 14:32:27 -05:00
func InitHTTPServer(opts *HTTPServerOpts) (
2025-01-04 12:24:42 -05:00
func(context.Context) error, <-chan interface{}, error,
) {
2025-01-28 14:32:27 -05:00
l := zerolog.Ctx(opts.Ctx)
2025-01-04 12:24:42 -05:00
doneChan := make(chan interface{})
var server *http.Server
2025-01-28 14:32:27 -05:00
httpMeter = otel.GetMeter(opts.Ctx, "http")
httpTracer = otel.GetTracer(opts.Ctx, "http")
2025-01-04 12:24:42 -05:00
2025-01-28 14:32:27 -05:00
server = prepHTTPServer(opts)
2025-01-04 12:24:42 -05:00
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
}