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 }