Skip to content
Snippets Groups Projects
http.go 4.59 KiB
Newer Older
ale's avatar
ale committed
package serverutil

import (
	"context"
	"crypto/tls"
	"io"
ale's avatar
ale committed
	"log"
	"net"
ale's avatar
ale committed
	"net/http"
	"net/http/pprof"
ale's avatar
ale committed
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/coreos/go-systemd/daemon"
ale's avatar
ale committed
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var gracefulShutdownTimeout = 3 * time.Second

// ServerConfig stores common HTTP/HTTPS server configuration parameters.
type ServerConfig struct {
	TLS                 *TLSServerConfig `yaml:"tls"`
	MaxInflightRequests int              `yaml:"max_inflight_requests"`
	TrustedForwarders   []string         `yaml:"trusted_forwarders"`
ale's avatar
ale committed
}

func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error) {
ale's avatar
ale committed
	var tlsConfig *tls.Config
	var err error
	if config != nil {
		if config.TLS != nil {
			tlsConfig, err = config.TLS.TLSConfig()
ale's avatar
ale committed
			if err != nil {
				return nil, err
ale's avatar
ale committed
			}
			h, err = config.TLS.TLSAuthWrapper(h)
ale's avatar
ale committed
			if err != nil {
				return nil, err
ale's avatar
ale committed
			}
		}

		// If TrustedForwarders is defined, rewrite the request
		// headers using X-Forwarded-Proto and X-Real-IP.
		if len(config.TrustedForwarders) > 0 {
			h, err = newProxyHeaders(h, config.TrustedForwarders)
			if err != nil {
				return nil, err
			}
		}

		// If MaxInflightRequests is set, enable the load
		// shedding wrapper.
		if config.MaxInflightRequests > 0 {
			h = newLoadSheddingWrapper(config.MaxInflightRequests, h)
ale's avatar
ale committed
		}
	}

	// These are not meant to be external-facing servers, so we
	// can be generous with the timeouts to keep the number of
	// reconnections low.
	return &http.Server{
		Handler:      defaultHandler(h),
ale's avatar
ale committed
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 30 * time.Second,
		IdleTimeout:  600 * time.Second,
		TLSConfig:    tlsConfig,
	}, nil
}

// Serve HTTP(S) content on the specified address. If config.TLS is
// not nil, enable HTTPS and TLS authentication.
//
// This function will return an error if there are problems creating
// the listener, otherwise it will handle graceful termination on
// SIGINT or SIGTERM and return nil.
func Serve(h http.Handler, config *ServerConfig, addr string) error {
	// Create the HTTP server.
	srv, err := config.buildHTTPServer(h)
	if err != nil {
		return err
	}

	// Create the net.Listener first, so we can detect
	// initialization-time errors safely.
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	if srv.TLSConfig != nil {
		l = tls.NewListener(l, srv.TLSConfig)
ale's avatar
ale committed
	}

	// Install a signal handler for gentle process termination.
	done := make(chan struct{})
	sigCh := make(chan os.Signal, 1)
	go func() {
		<-sigCh
		log.Printf("exiting")

		// Gracefully terminate for 3 seconds max, then shut
		// down remaining clients.
		ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
		defer cancel()
ale's avatar
ale committed
		if err = srv.Shutdown(ctx); err == context.Canceled {
			if err = srv.Close(); err != nil {
ale's avatar
ale committed
				log.Printf("error terminating server: %v", err)
			}
		}

		close(done)
	}()
ale's avatar
ale committed
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

	// Notify systemd that we are ready to serve.
	daemon.SdNotify(false, "READY=1")

	err = srv.Serve(l)
ale's avatar
ale committed
	if err != http.ErrServerClosed {
ale's avatar
ale committed
		return err
	}

	<-done
	return nil
}

func defaultHandler(h http.Handler) http.Handler {
ale's avatar
ale committed
	root := http.NewServeMux()

	// Add an endpoint for HTTP health checking probes.
	root.Handle("/health", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		io.WriteString(w, "OK")
	}))

	// Add an endpoint to serve Prometheus metrics.
ale's avatar
ale committed
	root.Handle("/metrics", promhttp.Handler())

	// Add the net/http/pprof debug handlers.
	root.Handle("/debug/pprof/", pprof.Handler(""))

	// Forward everything else to the main handler, adding
	// Prometheus instrumentation (requests to /metrics and
	// /health are not included).
	root.Handle("/", promhttp.InstrumentHandlerInFlight(inFlightRequests,
		promhttp.InstrumentHandlerCounter(totalRequests, h)))

	return root
ale's avatar
ale committed
}

// HTTP-related metrics.
var (
ale's avatar
ale committed
	// Since we instrument the root HTTP handler, we don't really
	// have a good way to set the 'handler' label based on the
	// request URL - but still, we'd like to set the label to
	// match what the other Prometheus jobs do. So we just set it
	// to 'all'.
ale's avatar
ale committed
	totalRequests = prometheus.NewCounterVec(
		prometheus.CounterOpts{
ale's avatar
ale committed
			Name: "http_requests_total",
ale's avatar
ale committed
			Help: "Total number of requests.",
ale's avatar
ale committed
			ConstLabels: prometheus.Labels{
				"handler": "all",
			},
ale's avatar
ale committed
		},
ale's avatar
ale committed
		[]string{"code", "method"},
ale's avatar
ale committed
	)
	inFlightRequests = prometheus.NewGauge(
		prometheus.GaugeOpts{
ale's avatar
ale committed
			Name: "http_requests_inflight",
ale's avatar
ale committed
			Help: "Number of in-flight requests.",
		},
	)
)

func init() {
	prometheus.MustRegister(totalRequests, inFlightRequests)
}