2022-12-08 13:05:19 -03:00

443 lines
12 KiB
Go

// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package baseserver
import (
"context"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
common_grpc "github.com/gitpod-io/gitpod/common-go/grpc"
"github.com/gitpod-io/gitpod/common-go/pprof"
grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
http_metrics "github.com/slok/go-http-metrics/metrics/prometheus"
"github.com/slok/go-http-metrics/middleware"
"github.com/slok/go-http-metrics/middleware/std"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
)
func New(name string, opts ...Option) (*Server, error) {
options, err := evaluateOptions(defaultOptions(), opts...)
if err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
server := &Server{
Name: name,
options: options,
}
server.builtinServices = newBuiltinServices(server)
server.httpMux = http.NewServeMux()
server.http = &http.Server{Handler: std.Handler("", middleware.New(middleware.Config{
Recorder: http_metrics.NewRecorder(http_metrics.Config{
Prefix: "gitpod",
Registry: server.MetricsRegistry(),
}),
}), server.httpMux)}
err = server.initializeMetrics()
if err != nil {
return nil, fmt.Errorf("failed to initialize metrics: %w", err)
}
err = server.initializeGRPC()
if err != nil {
return nil, fmt.Errorf("failed to initialize gRPC server: %w", err)
}
return server, nil
}
// Server is a packaged server with batteries included. It is designed to be standard across components where it makes sense.
// Server implements graceful shutdown making it suitable for usage in integration tests. See server_test.go.
//
// Server is composed of the following:
// - Debug server which serves observability and debug endpoints
// - /metrics for Prometheus metrics
// - /pprof for Golang profiler
// - /ready for kubernetes readiness check
// - /live for kubernetes liveness check
// - (optional) gRPC server with standard interceptors and configuration
// - Started when baseserver is configured WithGRPCPort (port is non-negative)
// - Use Server.GRPC() to get access to the underlying grpc.Server and register services
// - (optional) HTTP server
// - Currently does not come with any standard HTTP middlewares
// - Started when baseserver is configured WithHTTPPort (port is non-negative)
// - Use Server.HTTPMux() to get access to the root handler and register your endpoints
type Server struct {
// Name is the name of this server, used for logging context
Name string
options *options
builtinServices *builtinServices
// http is an http Server, only used when port is specified in cfg
http *http.Server
httpMux *http.ServeMux
httpListener net.Listener
// grpc is a grpc Server, only used when port is specified in cfg
grpc *grpc.Server
grpcListener net.Listener
// listening indicates the server is serving. When closed, the server is in the process of graceful termination.
listening chan struct{}
closeOnce sync.Once
}
func serveHTTP(cfg *ServerConfiguration, srv *http.Server, l net.Listener) (err error) {
if cfg.TLS == nil {
err = srv.Serve(l)
} else {
err = srv.ServeTLS(l, cfg.TLS.CertPath, cfg.TLS.KeyPath)
}
return
}
func (s *Server) ListenAndServe() error {
var err error
s.listening = make(chan struct{})
defer func() {
err := s.Close()
if err != nil {
s.Logger().WithError(err).Errorf("cannot close gracefully")
}
}()
go func() {
err := s.builtinServices.ListenAndServe()
if err != nil {
s.Logger().WithError(err).Errorf("builtin services encountered an error - closing remaining servers.")
s.Close()
}
}()
if srv := s.options.config.Services.HTTP; srv != nil {
s.httpListener, err = net.Listen("tcp", srv.Address)
if err != nil {
return fmt.Errorf("failed to start HTTP server: %w", err)
}
s.http.Addr = srv.Address
go func() {
err := serveHTTP(srv, s.http, s.httpListener)
if err != nil {
s.Logger().WithError(err).Errorf("HTTP server encountered an error - closing remaining servers.")
s.Close()
}
}()
}
if srv := s.options.config.Services.GRPC; srv != nil {
s.grpcListener, err = net.Listen("tcp", srv.Address)
if err != nil {
return fmt.Errorf("failed to start gRPC server: %w", err)
}
go func() {
err := s.grpc.Serve(s.grpcListener)
if err != nil {
s.Logger().WithError(err).Errorf("gRPC server encountered an error - closing remaining servers.")
s.Close()
}
}()
}
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
// Await operating system signals, or server errors.
sig := <-signals
s.Logger().Infof("Received system signal %s, closing server.", sig.String())
return nil
}
func (s *Server) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), s.options.closeTimeout)
defer cancel()
var err error
s.closeOnce.Do(func() {
err = s.close(ctx)
})
return err
}
func (s *Server) Logger() *logrus.Entry {
return s.options.logger
}
func (s *Server) HTTPMux() *http.ServeMux {
return s.httpMux
}
func (s *Server) GRPC() *grpc.Server {
return s.grpc
}
func (s *Server) MetricsRegistry() *prometheus.Registry {
return s.options.metricsRegistry
}
func (s *Server) close(ctx context.Context) error {
if s.listening == nil {
return fmt.Errorf("server is not running, invalid close operation")
}
if s.isClosing() {
s.Logger().Debug("Server is already closing.")
return nil
}
s.Logger().Info("Received graceful shutdown request.")
close(s.listening)
if s.grpc != nil {
s.grpc.GracefulStop()
// s.grpc.GracefulStop() also closes the underlying net.Listener, we just release the reference.
s.grpcListener = nil
s.Logger().Info("GRPC server terminated.")
}
if s.http != nil {
err := s.http.Shutdown(ctx)
if err != nil {
return fmt.Errorf("failed to close http server: %w", err)
}
// s.http.Shutdown() also closes the underlying net.Listener, we just release the reference.
s.httpListener = nil
s.Logger().Info("HTTP server terminated.")
}
// Always terminate builtin server last, we want to keep it running for as long as possible
err := s.builtinServices.Close()
if err != nil {
return fmt.Errorf("failed to close debug server: %w", err)
}
s.Logger().Info("Debug server terminated.")
return nil
}
func (s *Server) isClosing() bool {
select {
case <-s.listening:
// listening channel is closed, we're in graceful shutdown mode
return true
default:
return false
}
}
func (s *Server) healthEndpoint() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/ready", s.options.healthHandler.ReadyEndpoint)
mux.HandleFunc("/live", s.options.healthHandler.LiveEndpoint)
return mux
}
func (s *Server) metricsEndpoint() http.Handler {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.InstrumentMetricHandler(
s.options.metricsRegistry, promhttp.HandlerFor(s.options.metricsRegistry, promhttp.HandlerOpts{}),
))
return mux
}
func (s *Server) initializeGRPC() error {
common_grpc.SetupLogging()
grpcMetrics := grpc_prometheus.NewServerMetrics()
grpcMetrics.EnableHandlingTimeHistogram(
grpc_prometheus.WithHistogramBuckets([]float64{.005, .025, .05, .1, .5, 1, 2.5, 5, 30, 60, 120, 240, 600}),
)
if err := s.MetricsRegistry().Register(grpcMetrics); err != nil {
return fmt.Errorf("failed to register grpc metrics: %w", err)
}
unary := []grpc.UnaryServerInterceptor{
grpc_logrus.UnaryServerInterceptor(s.Logger(),
grpc_logrus.WithDecider(func(fullMethodName string, err error) bool {
// Skip logs for anything that does not contain an error.
if err == nil {
return false
}
// Skip gRPC healthcheck logs, they are frequent and pollute our logging infra
return fullMethodName != "/grpc.health.v1.Health/Check"
}),
),
grpcMetrics.UnaryServerInterceptor(),
}
stream := []grpc.StreamServerInterceptor{
grpc_logrus.StreamServerInterceptor(s.Logger()),
grpcMetrics.StreamServerInterceptor(),
}
opts := common_grpc.ServerOptionsWithInterceptors(stream, unary)
if cfg := s.options.config.Services.GRPC; cfg != nil && cfg.TLS != nil {
tlsConfig, err := common_grpc.ClientAuthTLSConfig(
cfg.TLS.CAPath, cfg.TLS.CertPath, cfg.TLS.KeyPath,
common_grpc.WithSetClientCAs(true),
common_grpc.WithServerName(s.Name),
)
if err != nil {
return fmt.Errorf("failed to load certificates: %w", err)
}
opts = append(opts, grpc.Creds(credentials.NewTLS(tlsConfig)))
}
opts = append(opts, grpc.MaxRecvMsgSize(100*1024*1024))
s.grpc = grpc.NewServer(opts...)
reflection.Register(s.grpc)
// Register health service by default
grpc_health_v1.RegisterHealthServer(s.grpc, s.options.grpcHealthCheck)
return nil
}
func (s *Server) initializeMetrics() error {
err := s.MetricsRegistry().Register(collectors.NewGoCollector())
if err != nil {
return fmt.Errorf("faile to register go collectors: %w", err)
}
err = s.MetricsRegistry().Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
if err != nil {
return fmt.Errorf("failed to register process collectors: %w", err)
}
err = registerMetrics(s.MetricsRegistry())
if err != nil {
return fmt.Errorf("failed to register baseserver metrics: %w", err)
}
reportServerVersion(s.options.version)
return nil
}
func (s *Server) DebugAddress() string {
if s.builtinServices == nil {
return ""
}
return "http://" + s.builtinServices.Debug.Addr
}
func (s *Server) HealthAddr() string {
if s.builtinServices == nil {
return ""
}
return "http://" + s.builtinServices.Health.Addr
}
func (s *Server) HTTPAddress() string {
return httpAddress(s.options.config.Services.HTTP, s.httpListener)
}
func (s *Server) GRPCAddress() string { return s.options.config.Services.GRPC.GetAddress() }
const (
BuiltinDebugPort = 6060
BuiltinMetricsPort = 9500
BuiltinHealthPort = 9501
BuiltinMetricsPortName = "metrics"
)
type builtinServices struct {
underTest bool
Debug *http.Server
Health *http.Server
Metrics *http.Server
}
func newBuiltinServices(server *Server) *builtinServices {
healthAddr := fmt.Sprintf(":%d", BuiltinHealthPort)
if server.options.underTest {
healthAddr = ":0"
}
return &builtinServices{
underTest: server.options.underTest,
Debug: &http.Server{
Addr: fmt.Sprintf(":%d", BuiltinDebugPort),
Handler: pprof.Handler(),
},
Health: &http.Server{
Addr: healthAddr,
Handler: server.healthEndpoint(),
},
Metrics: &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", BuiltinMetricsPort),
Handler: server.metricsEndpoint(),
},
}
}
func (s *builtinServices) ListenAndServe() error {
if s == nil {
return nil
}
var eg errgroup.Group
if !s.underTest {
eg.Go(func() error { return s.Debug.ListenAndServe() })
eg.Go(func() error { return s.Metrics.ListenAndServe() })
}
eg.Go(func() error {
// health is the only service which has a variable address,
// because we need the health service to figure out if the
// server started at all
l, err := net.Listen("tcp", s.Health.Addr)
if err != nil {
return err
}
s.Health.Addr = l.Addr().String()
err = s.Health.Serve(l)
if err == http.ErrServerClosed {
return nil
}
return err
})
return eg.Wait()
}
func (s *builtinServices) Close() error {
var eg errgroup.Group
eg.Go(func() error { return s.Debug.Close() })
eg.Go(func() error { return s.Metrics.Close() })
eg.Go(func() error { return s.Health.Close() })
return eg.Wait()
}
func httpAddress(cfg *ServerConfiguration, l net.Listener) string {
if l == nil {
return ""
}
protocol := "http"
if cfg != nil && cfg.TLS != nil {
protocol = "https"
}
return fmt.Sprintf("%s://%s", protocol, l.Addr().String())
}