From e01a0148b6cb8afd0a24ee02e8b037beeb3dded0 Mon Sep 17 00:00:00 2001 From: xhe Date: Tue, 17 Jan 2023 15:18:36 +0800 Subject: [PATCH] api: add /api/debug/health (#193) --- lib/cli/health.go | 44 ++++++++++++++++ lib/cli/main.go | 1 + pkg/server/api/api.go | 2 +- pkg/server/api/debug.go | 5 ++ pkg/server/api/http.go | 112 ++++++++++++++++++++++++++++++++++++++++ pkg/server/server.go | 68 +++--------------------- 6 files changed, 170 insertions(+), 62 deletions(-) create mode 100644 lib/cli/health.go create mode 100644 pkg/server/api/http.go diff --git a/lib/cli/health.go b/lib/cli/health.go new file mode 100644 index 00000000..be848f24 --- /dev/null +++ b/lib/cli/health.go @@ -0,0 +1,44 @@ +// Copyright 2023 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "net/http" + + "github.com/spf13/cobra" +) + +const ( + healthPrefix = "/api/debug/health" +) + +func GetHealthCmd(ctx *Context) *cobra.Command { + rootCmd := &cobra.Command{ + Use: "health", + Short: "", + } + + rootCmd.RunE = func(cmd *cobra.Command, args []string) error { + resp, err := doRequest(cmd.Context(), ctx, http.MethodGet, healthPrefix, nil) + if err != nil { + return err + } + + cmd.Println(resp) + return nil + } + + return rootCmd +} diff --git a/lib/cli/main.go b/lib/cli/main.go index c40e6183..143adb32 100644 --- a/lib/cli/main.go +++ b/lib/cli/main.go @@ -94,5 +94,6 @@ func GetRootCmd(tlsConfig *tls.Config) *cobra.Command { rootCmd.AddCommand(GetNamespaceCmd(ctx)) rootCmd.AddCommand(GetConfigCmd(ctx)) + rootCmd.AddCommand(GetHealthCmd(ctx)) return rootCmd } diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index 963cda03..c34c2aed 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -22,7 +22,7 @@ import ( "go.uber.org/zap" ) -func Register(group *gin.RouterGroup, cfg config.API, logger *zap.Logger, nsmgr *mgrns.NamespaceManager, cfgmgr *mgrcfg.ConfigManager) { +func register(group *gin.RouterGroup, cfg config.API, logger *zap.Logger, nsmgr *mgrns.NamespaceManager, cfgmgr *mgrcfg.ConfigManager) { { adminGroup := group.Group("admin") if cfg.EnableBasicAuth { diff --git a/pkg/server/api/debug.go b/pkg/server/api/debug.go index d0a66d07..50a467e9 100644 --- a/pkg/server/api/debug.go +++ b/pkg/server/api/debug.go @@ -28,6 +28,10 @@ type debugHttpHandler struct { nsmgr *mgrns.NamespaceManager } +func (h *debugHttpHandler) Health(c *gin.Context) { + c.JSON(http.StatusOK, "") +} + func (h *debugHttpHandler) Redirect(c *gin.Context) { errs := h.nsmgr.RedirectConnections() if len(errs) != 0 { @@ -48,5 +52,6 @@ func (h *debugHttpHandler) Redirect(c *gin.Context) { func registerDebug(group *gin.RouterGroup, logger *zap.Logger, nsmgr *mgrns.NamespaceManager) { handler := &debugHttpHandler{logger, nsmgr} group.POST("/redirect", handler.Redirect) + group.GET("/health", handler.Health) pprof.RouteRegister(group, "/pprof") } diff --git a/pkg/server/api/http.go b/pkg/server/api/http.go new file mode 100644 index 00000000..0f7bd3b4 --- /dev/null +++ b/pkg/server/api/http.go @@ -0,0 +1,112 @@ +// Copyright 2023 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + ginzap "github.com/gin-contrib/zap" + "github.com/gin-gonic/gin" + "github.com/pingcap/TiProxy/lib/config" + "github.com/pingcap/TiProxy/lib/util/errors" + "github.com/pingcap/TiProxy/lib/util/waitgroup" + mgrcrt "github.com/pingcap/TiProxy/pkg/manager/cert" + mgrcfg "github.com/pingcap/TiProxy/pkg/manager/config" + mgrns "github.com/pingcap/TiProxy/pkg/manager/namespace" + "go.uber.org/atomic" + "go.uber.org/ratelimit" + "go.uber.org/zap" +) + +const ( + // DefAPILimit is the global API limit per second. + DefAPILimit = 100 + // DefConnTimeout is used as timeout duration in the HTTP server. + DefConnTimeout = 30 * time.Second +) + +type HTTPHandler interface { + RegisterHTTP(c *gin.Engine) error +} + +type HTTPServer struct { + listener net.Listener + wg waitgroup.WaitGroup +} + +func NewHTTPServer(cfg config.API, lg *zap.Logger, + nsmgr *mgrns.NamespaceManager, cfgmgr *mgrcfg.ConfigManager, + crtmgr *mgrcrt.CertManager, handler HTTPHandler, + ready *atomic.Bool) (*HTTPServer, error) { + h := &HTTPServer{} + + var err error + h.listener, err = net.Listen("tcp", cfg.Addr) + if err != nil { + return nil, err + } + + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + limit := ratelimit.New(DefAPILimit) + engine.Use( + gin.Recovery(), + ginzap.GinzapWithConfig(lg.Named("gin"), &ginzap.Config{ + UTC: true, + SkipPaths: []string{ + "/api/debug/health", + }, + }), + func(c *gin.Context) { + _ = limit.Take() + if !ready.Load() { + c.Abort() + c.JSON(http.StatusInternalServerError, "service not ready") + } + }, + ) + + register(engine.Group("/api"), cfg, lg, nsmgr, cfgmgr) + + if tlscfg := crtmgr.ServerTLS(); tlscfg != nil { + h.listener = tls.NewListener(h.listener, tlscfg) + } + + if handler != nil { + if err := handler.RegisterHTTP(engine); err != nil { + return nil, errors.WithStack(err) + } + } + + h.wg.Run(func() { + hsrv := http.Server{ + Handler: engine.Handler(), + ReadHeaderTimeout: DefConnTimeout, + IdleTimeout: DefConnTimeout, + } + lg.Info("HTTP closed", zap.Error(hsrv.Serve(h.listener))) + }) + + return h, nil +} + +func (h *HTTPServer) Close() error { + err := h.listener.Close() + h.wg.Wait() + return err +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 4a093f7b..112c8f26 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -16,13 +16,8 @@ package server import ( "context" - "crypto/tls" - "net" "net/http" - "time" - ginzap "github.com/gin-contrib/zap" - "github.com/gin-gonic/gin" "github.com/pingcap/TiProxy/lib/config" "github.com/pingcap/TiProxy/lib/util/errors" "github.com/pingcap/TiProxy/lib/util/waitgroup" @@ -38,17 +33,9 @@ import ( "github.com/pingcap/TiProxy/pkg/server/api" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/atomic" - "go.uber.org/ratelimit" "go.uber.org/zap" ) -const ( - // DefAPILimit is the global API limit per second. - DefAPILimit = 100 - // DefConnTimeout is used as timeout duration in the HTTP server. - DefConnTimeout = 30 * time.Second -) - type Server struct { wg waitgroup.WaitGroup // managers @@ -61,7 +48,7 @@ type Server struct { // HTTP client Http *http.Client // HTTP server - HTTPListener net.Listener + HTTPServer *api.HTTPServer // L7 proxy Proxy *proxy.SQLServer } @@ -83,6 +70,7 @@ func NewServer(ctx context.Context, sctx *sctx.Context) (srv *Server, err error) if srv.LoggerManager, lg, err = logger.NewLoggerManager(&sctx.Overlay.Log); err != nil { return } + srv.LoggerManager.Init(srv.ConfigManager.WatchConfig()) // setup config manager if err = srv.ConfigManager.Init(ctx, lg.Named("config"), sctx.ConfigFile, &sctx.Overlay); err != nil { @@ -91,9 +79,6 @@ func NewServer(ctx context.Context, sctx *sctx.Context) (srv *Server, err error) } cfg := srv.ConfigManager.GetConfig() - // also hook logger - srv.LoggerManager.Init(srv.ConfigManager.WatchConfig()) - // setup metrics srv.MetricsManager.Init(ctx, lg.Named("metrics"), cfg.Metrics.MetricsAddr, cfg.Metrics.MetricsInterval, cfg.Proxy.Addr) @@ -102,48 +87,9 @@ func NewServer(ctx context.Context, sctx *sctx.Context) (srv *Server, err error) return } - // setup gin - { - slogger := lg.Named("gin") - gin.SetMode(gin.ReleaseMode) - engine := gin.New() - limit := ratelimit.New(DefAPILimit) - engine.Use( - gin.Recovery(), - ginzap.Ginzap(slogger, "", true), - func(c *gin.Context) { - _ = limit.Take() - if !ready.Load() { - c.Abort() - c.JSON(http.StatusInternalServerError, "service not ready") - } - }, - ) - - api.Register(engine.Group("/api"), cfg.API, lg.Named("api"), srv.NamespaceManager, srv.ConfigManager) - - srv.HTTPListener, err = net.Listen("tcp", cfg.API.Addr) - if err != nil { - return nil, err - } - if tlscfg := srv.CertManager.ServerTLS(); tlscfg != nil { - srv.HTTPListener = tls.NewListener(srv.HTTPListener, tlscfg) - } - - if handler != nil { - if err := handler.RegisterHTTP(engine); err != nil { - return nil, errors.WithStack(err) - } - } - - srv.wg.Run(func() { - hsrv := http.Server{ - Handler: engine.Handler(), - ReadHeaderTimeout: DefConnTimeout, - IdleTimeout: DefConnTimeout, - } - slogger.Info("HTTP closed", zap.Error(hsrv.Serve(srv.HTTPListener))) - }) + // setup http + if srv.HTTPServer, err = api.NewHTTPServer(cfg.API, lg.Named("api"), srv.NamespaceManager, srv.ConfigManager, srv.CertManager, handler, ready); err != nil { + return } // general cluster HTTP client @@ -217,8 +163,8 @@ func (s *Server) Close() error { if s.Proxy != nil { errs = append(errs, s.Proxy.Close()) } - if s.HTTPListener != nil { - s.HTTPListener.Close() + if s.HTTPServer != nil { + s.HTTPServer.Close() } if s.NamespaceManager != nil { errs = append(errs, s.NamespaceManager.Close())