From f99526351ba1abffa2607d7011ef695d6c4a1b74 Mon Sep 17 00:00:00 2001 From: wubin1989 <328454505@qq.com> Date: Sun, 7 Jan 2024 22:51:48 +0800 Subject: [PATCH 1/3] add Serve api for supporting tableflip --- framework/banner/banner.go | 31 -- framework/config/config.go | 37 ++- framework/framework.go | 20 +- framework/grpcx/server.go | 64 +++-- framework/rest/gorilla/server.go | 395 -------------------------- framework/rest/gorilla/server_test.go | 23 -- framework/rest/server.go | 187 ++++-------- go.mod | 1 + go.sum | 9 + 9 files changed, 146 insertions(+), 621 deletions(-) delete mode 100644 framework/banner/banner.go delete mode 100644 framework/rest/gorilla/server.go delete mode 100644 framework/rest/gorilla/server_test.go diff --git a/framework/banner/banner.go b/framework/banner/banner.go deleted file mode 100644 index 1a257935..00000000 --- a/framework/banner/banner.go +++ /dev/null @@ -1,31 +0,0 @@ -package banner - -import ( - "github.com/common-nighthawk/go-figure" - "github.com/unionj-cloud/go-doudou/v2/framework" - "github.com/unionj-cloud/go-doudou/v2/framework/config" - "github.com/unionj-cloud/go-doudou/v2/toolkit/cast" - "github.com/unionj-cloud/go-doudou/v2/toolkit/stringutils" - "sync" -) - -var once sync.Once - -func Print() { - once.Do(func() { - if !framework.CheckDev() { - return - } - banner := config.DefaultGddBanner - if b, err := cast.ToBoolE(config.GddBanner.Load()); err == nil { - banner = b - } - if banner { - bannerText := config.DefaultGddBannerText - if stringutils.IsNotEmpty(config.GddBannerText.Load()) { - bannerText = config.GddBannerText.Load() - } - figure.NewColorFigure(bannerText, "doom", "green", true).Print() - } - }) -} diff --git a/framework/config/config.go b/framework/config/config.go index 34e8664c..1e676b6c 100644 --- a/framework/config/config.go +++ b/framework/config/config.go @@ -7,7 +7,6 @@ import ( "os" "strconv" "strings" - "sync" "time" "github.com/apolloconfig/agollo/v4" @@ -19,7 +18,7 @@ import ( "github.com/wubin1989/nacos-sdk-go/v2/vo" _ "go.uber.org/automaxprocs" - "github.com/unionj-cloud/go-doudou/v2/framework" + "github.com/kelseyhightower/envconfig" "github.com/unionj-cloud/go-doudou/v2/framework/configmgr" "github.com/unionj-cloud/go-doudou/v2/toolkit/cast" "github.com/unionj-cloud/go-doudou/v2/toolkit/dotenv" @@ -28,6 +27,8 @@ import ( "github.com/unionj-cloud/go-doudou/v2/toolkit/zlogger" ) +var GddConfig = &gddConfig{} + func LoadConfigFromLocal() { env := os.Getenv("GDD_ENV") if "" == env { @@ -87,12 +88,20 @@ func LoadConfigFromRemote() { } } +func CheckDev() bool { + return stringutils.IsEmpty(os.Getenv("GDD_ENV")) || os.Getenv("GDD_ENV") == "dev" +} + func init() { LoadConfigFromLocal() LoadConfigFromRemote() + err := envconfig.Process("gdd", &GddConfig) + if err != nil { + zlogger.Panic().Msg("Error processing environment variables") + } zl, _ := zerolog.ParseLevel(GddLogLevel.LoadOrDefault(DefaultGddLogLevel)) opts := []zlogger.LoggerConfigOption{ - zlogger.WithDev(framework.CheckDev()), + zlogger.WithDev(CheckDev()), zlogger.WithCaller(cast.ToBoolOrDefault(GddLogCaller.Load(), DefaultGddLogCaller)), zlogger.WithDiscard(cast.ToBoolOrDefault(GddLogDiscard.Load(), DefaultGddLogDiscard)), zlogger.WithZeroLogLevel(zl), @@ -454,8 +463,6 @@ func ServiceDiscoveryMap() map[string]struct{} { return modemap } -var PrintLock sync.Mutex - type Config struct { Db struct { Name string @@ -525,3 +532,23 @@ type Config struct { Name string } } + +type gddConfig struct { + Banner bool + BannerText string `default:"go-doudou" split_words:"true"` + RouteRootPath string `default:"/" split_words:"true"` + WriteTimeout time.Duration `default:"15s" split_words:"true"` + ReadTimeout time.Duration `default:"15s" split_words:"true"` + IdleTimeout time.Duration `default:"60s" split_words:"true"` + GraceTimeout time.Duration `default:"15s" split_words:"true"` + Port string `default:"6060"` + Host string + EnableResponseGzip bool `default:"true" split_words:"true"` + LogReqEnable bool `default:"false" split_words:"true"` + ManageEnable bool `default:"true" split_words:"true"` + FallbackContenttype string `default:"application/json; charset=UTF-8" split_words:"true"` + Grpc struct { + Port string `default:"50051"` + } + Config +} diff --git a/framework/framework.go b/framework/framework.go index e7d4b114..3b27a541 100644 --- a/framework/framework.go +++ b/framework/framework.go @@ -1,8 +1,9 @@ package framework import ( - "github.com/unionj-cloud/go-doudou/v2/toolkit/stringutils" - "os" + "github.com/common-nighthawk/go-figure" + "github.com/unionj-cloud/go-doudou/v2/framework/config" + "sync" ) type Annotation struct { @@ -30,6 +31,17 @@ func (receiver AnnotationStore) GetParams(key string, annotationName string) []s return nil } -func CheckDev() bool { - return stringutils.IsEmpty(os.Getenv("GDD_ENV")) || os.Getenv("GDD_ENV") == "dev" +var PrintLock sync.Mutex + +var once sync.Once + +func PrintBanner() { + once.Do(func() { + if !config.CheckDev() { + return + } + if config.GddConfig.Banner { + figure.NewColorFigure(config.GddConfig.BannerText, "doom", "green", true).Print() + } + }) } diff --git a/framework/grpcx/server.go b/framework/grpcx/server.go index 1fca0715..8c14a716 100644 --- a/framework/grpcx/server.go +++ b/framework/grpcx/server.go @@ -5,10 +5,8 @@ import ( "fmt" "github.com/olekukonko/tablewriter" "github.com/unionj-cloud/go-doudou/v2/framework" - "github.com/unionj-cloud/go-doudou/v2/framework/banner" "github.com/unionj-cloud/go-doudou/v2/framework/config" register "github.com/unionj-cloud/go-doudou/v2/framework/registry" - "github.com/unionj-cloud/go-doudou/v2/toolkit/cast" "github.com/unionj-cloud/go-doudou/v2/toolkit/timeutils" logger "github.com/unionj-cloud/go-doudou/v2/toolkit/zlogger" "google.golang.org/grpc" @@ -52,7 +50,7 @@ func NewGrpcServerWithData(data map[string]interface{}, opt ...grpc.ServerOption } func (srv *GrpcServer) printServices() { - if !framework.CheckDev() { + if !config.CheckDev() { return } logger.Info().Msg("================ Registered Services ================") @@ -88,37 +86,11 @@ func (srv *GrpcServer) Run() { // RunWithPipe runs grpc server func (srv *GrpcServer) RunWithPipe(pipe net.Listener) { - if srv.Server == nil { - return - } - banner.Print() - config.PrintLock.Lock() - register.NewGrpc(srv.data) - port := config.DefaultGddGrpcPort - if p, err := cast.ToIntE(config.GddGrpcPort.Load()); err == nil { - port = p - } - lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + ln, err := net.Listen("tcp", fmt.Sprintf(":%s", config.GddConfig.Grpc.Port)) if err != nil { logger.Panic().Msgf("failed to listen: %v", err) } - reflection.Register(srv) - srv.printServices() - go func() { - if err := srv.Serve(lis); err != nil { - logger.Error().Msgf("failed to serve: %v", err) - } - }() - if pipe != nil { - go func() { - if err := srv.Serve(pipe); err != nil { - logger.Error().Msgf("failed to serve: %v", err) - } - }() - } - logger.Info().Msgf("Grpc server is listening at %v", lis.Addr()) - logger.Info().Msgf("Grpc server started in %s", time.Since(startAt)) - config.PrintLock.Unlock() + srv.ServeWithPipe(ln, pipe) defer func() { register.ShutdownGrpc() @@ -148,3 +120,33 @@ func (srv *GrpcServer) RunWithPipe(pipe net.Listener) { // Block until we receive our signal. <-c } + +func (srv *GrpcServer) Serve(ln net.Listener) { + srv.ServeWithPipe(ln, nil) +} + +func (srv *GrpcServer) ServeWithPipe(ln net.Listener, pipe net.Listener) { + if srv.Server == nil { + return + } + framework.PrintBanner() + framework.PrintLock.Lock() + register.NewGrpc(srv.data) + reflection.Register(srv) + srv.printServices() + go func() { + if err := srv.Server.Serve(ln); err != nil { + logger.Error().Msgf("failed to serve: %v", err) + } + }() + if pipe != nil { + go func() { + if err := srv.Server.Serve(pipe); err != nil { + logger.Error().Msgf("failed to serve: %v", err) + } + }() + } + logger.Info().Msgf("Grpc server is listening at %v", ln.Addr()) + logger.Info().Msgf("Grpc server started in %s", time.Since(startAt)) + framework.PrintLock.Unlock() +} diff --git a/framework/rest/gorilla/server.go b/framework/rest/gorilla/server.go deleted file mode 100644 index 208d0d8a..00000000 --- a/framework/rest/gorilla/server.go +++ /dev/null @@ -1,395 +0,0 @@ -package gorilla - -import ( - "context" - "fmt" - "github.com/arl/statsviz" - "github.com/ascarter/requestid" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/klauspost/compress/gzhttp" - "github.com/olekukonko/tablewriter" - "github.com/rs/cors" - "github.com/unionj-cloud/go-doudou/v2/framework" - "github.com/unionj-cloud/go-doudou/v2/framework/banner" - "github.com/unionj-cloud/go-doudou/v2/framework/config" - register "github.com/unionj-cloud/go-doudou/v2/framework/registry" - "github.com/unionj-cloud/go-doudou/v2/framework/registry/constants" - "github.com/unionj-cloud/go-doudou/v2/framework/rest" - "github.com/unionj-cloud/go-doudou/v2/toolkit/cast" - "github.com/unionj-cloud/go-doudou/v2/toolkit/stringutils" - logger "github.com/unionj-cloud/go-doudou/v2/toolkit/zlogger" - "net/http" - "net/http/pprof" - "os" - "os/signal" - "path" - "strconv" - "strings" - "time" -) - -var startAt time.Time - -func init() { - startAt = time.Now() -} - -type common struct { - gddRoutes []rest.Route - debugRoutes []rest.Route - bizRoutes []rest.Route - Middlewares []mux.MiddlewareFunc - data map[string]interface{} -} - -// RestServer wraps gorilla mux router -type RestServer struct { - *mux.Router - rootRouter *mux.Router - common -} - -const gddPathPrefix = "/go-doudou/" -const debugPathPrefix = "/debug/" - -var contentTypeShouldbeGzip []string - -func init() { - contentTypeShouldbeGzip = []string{ - "text/html", - "text/css", - "text/plain", - "text/xml", - "text/x-component", - "text/javascript", - "application/x-javascript", - "application/javascript", - "application/json", - "application/manifest+json", - "application/vnd.api+json", - "application/xml", - "application/xhtml+xml", - "application/rss+xml", - "application/atom+xml", - "application/vnd.ms-fontobject", - "application/x-font-ttf", - "application/x-font-opentype", - "application/x-font-truetype", - "image/svg+xml", - "image/x-icon", - "image/vnd.microsoft.icon", - "font/ttf", - "font/eot", - "font/otf", - "font/opentype", - } -} - -// mux.MiddlewareFunc is alias for func(http.Handler) http.Handler -func toMiddlewareFunc(m func(http.Handler) http.HandlerFunc) mux.MiddlewareFunc { - return func(handler http.Handler) http.Handler { - return m(handler) - } -} - -// NewRestServer create a RestServer instance -func NewRestServer(data ...map[string]interface{}) *RestServer { - rr := config.DefaultGddRouteRootPath - if stringutils.IsNotEmpty(config.GddRouteRootPath.Load()) { - rr = config.GddRouteRootPath.Load() - } - rootRouter := mux.NewRouter().StrictSlash(true) - srv := &RestServer{ - Router: rootRouter.PathPrefix(rr).Subrouter().StrictSlash(true), - rootRouter: rootRouter, - } - srv.Middlewares = append(srv.Middlewares, - rest.Tracing, - rest.Metrics, - ) - if cast.ToBoolOrDefault(config.GddEnableResponseGzip.Load(), config.DefaultGddEnableResponseGzip) { - gzipMiddleware, err := gzhttp.NewWrapper(gzhttp.ContentTypes(contentTypeShouldbeGzip)) - if err != nil { - panic(err) - } - srv.Middlewares = append(srv.Middlewares, toMiddlewareFunc(gzipMiddleware)) - } - if cast.ToBoolOrDefault(config.GddLogReqEnable.Load(), config.DefaultGddLogReqEnable) { - srv.Middlewares = append(srv.Middlewares, rest.Log) - } - srv.Middlewares = append(srv.Middlewares, - requestid.RequestIDHandler, - handlers.ProxyHeaders, - rest.FallbackContentType(config.GddFallbackContentType.LoadOrDefault(config.DefaultGddFallbackContentType)), - ) - if len(data) > 0 { - srv.data = data[0] - } - return srv -} - -// AddRoute adds routes to router -func (srv *RestServer) AddRoute(route ...rest.Route) { - srv.bizRoutes = append(srv.bizRoutes, route...) -} - -func (srv *common) printRoutes() { - if !framework.CheckDev() { - return - } - logger.Info().Msg("================ Registered Routes ================") - data := [][]string{} - rr := config.DefaultGddRouteRootPath - if stringutils.IsNotEmpty(config.GddRouteRootPath.Load()) { - rr = config.GddRouteRootPath.Load() - } - var all []rest.Route - all = append(all, srv.bizRoutes...) - all = append(all, srv.gddRoutes...) - all = append(all, srv.debugRoutes...) - for _, r := range all { - if strings.HasPrefix(r.Pattern, gddPathPrefix) || strings.HasPrefix(r.Pattern, debugPathPrefix) { - data = append(data, []string{r.Name, r.Method, r.Pattern}) - } else { - data = append(data, []string{r.Name, r.Method, path.Clean(rr + r.Pattern)}) - } - } - - tableString := &strings.Builder{} - table := tablewriter.NewWriter(tableString) - table.SetHeader([]string{"Name", "Method", "Pattern"}) - for _, v := range data { - table.Append(v) - } - table.Render() // Send output - rows := strings.Split(strings.TrimSpace(tableString.String()), "\n") - for _, row := range rows { - logger.Info().Msg(row) - } - logger.Info().Msg("===================================================") -} - -// AddMiddleware adds middlewares to the end of chain -func (srv *RestServer) AddMiddleware(mwf ...func(http.Handler) http.Handler) { - for _, item := range mwf { - srv.Middlewares = append(srv.Middlewares, item) - } -} - -// PreMiddleware adds middlewares to the head of chain -func (srv *RestServer) PreMiddleware(mwf ...func(http.Handler) http.Handler) { - var middlewares []mux.MiddlewareFunc - for _, item := range mwf { - middlewares = append(middlewares, item) - } - srv.Middlewares = append(middlewares, srv.Middlewares...) -} - -func (srv *RestServer) newHttpServer() *http.Server { - write, err := time.ParseDuration(config.GddWriteTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddWriteTimeout), - config.GddWriteTimeout.Load(), err.Error(), config.DefaultGddWriteTimeout) - write, _ = time.ParseDuration(config.DefaultGddWriteTimeout) - } - - read, err := time.ParseDuration(config.GddReadTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddReadTimeout), - config.GddReadTimeout.Load(), err.Error(), config.DefaultGddReadTimeout) - read, _ = time.ParseDuration(config.DefaultGddReadTimeout) - } - - idle, err := time.ParseDuration(config.GddIdleTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddIdleTimeout), - config.GddIdleTimeout.Load(), err.Error(), config.DefaultGddIdleTimeout) - idle, _ = time.ParseDuration(config.DefaultGddIdleTimeout) - } - - httpPort := strconv.Itoa(config.DefaultGddPort) - if _, err = cast.ToIntE(config.GddPort.Load()); err == nil { - httpPort = config.GddPort.Load() - } - httpHost := config.DefaultGddHost - if stringutils.IsNotEmpty(config.GddHost.Load()) { - httpHost = config.GddHost.Load() - } - httpServer := &http.Server{ - Addr: strings.Join([]string{httpHost, httpPort}, ":"), - // Good practice to set timeouts to avoid Slowloris attacks. - WriteTimeout: write, - ReadTimeout: read, - IdleTimeout: idle, - Handler: srv.rootRouter, // Pass our instance of gorilla/mux in. - } - - // Run our server in a goroutine so that it doesn't block. - go func() { - logger.Info().Msgf("Http server is listening at %v", httpServer.Addr) - logger.Info().Msgf("Http server started in %s", time.Since(startAt)) - if err := httpServer.ListenAndServe(); err != nil { - logger.Error().Err(err).Msg("") - } - }() - - return httpServer -} - -// Run runs http server -func (srv *RestServer) Run() { - banner.Print() - register.NewRest(srv.data) - manage := cast.ToBoolOrDefault(config.GddManage.Load(), config.DefaultGddManage) - if manage { - srv.Middlewares = append([]mux.MiddlewareFunc{rest.PrometheusMiddleware}, srv.Middlewares...) - gddRouter := srv.rootRouter.PathPrefix(gddPathPrefix).Subrouter().StrictSlash(true) - corsOpts := cors.New(cors.Options{ - AllowedMethods: []string{ - http.MethodGet, - http.MethodPost, - http.MethodPut, - http.MethodPatch, - http.MethodDelete, - http.MethodOptions, - http.MethodHead, - }, - - AllowedHeaders: []string{ - "*", - }, - - AllowOriginRequestFunc: func(r *http.Request, origin string) bool { - if r.URL.Path == fmt.Sprintf("%sopenapi.json", gddPathPrefix) { - return true - } - return false - }, - }) - gddRouter.Use(rest.Metrics) - gddRouter.Use(corsOpts.Handler) - gddRouter.Use(rest.BasicAuth()) - srv.gddRoutes = append(srv.gddRoutes, rest.DocRoutes()...) - srv.gddRoutes = append(srv.gddRoutes, rest.PromRoutes()...) - srv.gddRoutes = append(srv.gddRoutes, rest.ConfigRoutes()...) - if _, ok := config.ServiceDiscoveryMap()[constants.SD_MEMBERLIST]; ok { - srv.gddRoutes = append(srv.gddRoutes, rest.MemberlistUIRoutes()...) - } - for _, item := range srv.gddRoutes { - gddRouter. - Methods(item.Method, http.MethodOptions). - Path("/" + strings.TrimPrefix(item.Pattern, gddPathPrefix)). - Name(item.Name). - Handler(item.HandlerFunc) - } - freq, err := time.ParseDuration(config.GddStatsFreq.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddStatsFreq), - config.GddStatsFreq.Load(), err.Error(), config.DefaultGddStatsFreq) - freq, _ = time.ParseDuration(config.DefaultGddStatsFreq) - } - srv.gddRoutes = append(srv.gddRoutes, []rest.Route{ - { - Name: "GetStatsvizWs", - Method: "GET", - Pattern: gddPathPrefix + "statsviz/ws", - }, - { - Name: "GetStatsviz", - Method: "GET", - Pattern: gddPathPrefix + "statsviz/", - }, - }...) - gddRouter. - Methods(http.MethodGet). - Path("/statsviz/ws"). - Name("GetStatsvizWs"). - HandlerFunc(statsviz.NewWsHandler(freq)) - gddRouter. - Methods(http.MethodGet). - PathPrefix("/statsviz/"). - Name("GetStatsviz"). - Handler(statsviz.IndexAtRoot(gddPathPrefix + "statsviz/")) - srv.debugRoutes = append(srv.debugRoutes, []rest.Route{ - { - Name: "GetDebugPprofCmdline", - Method: "GET", - Pattern: debugPathPrefix + "pprof/cmdline", - }, - { - Name: "GetDebugPprofProfile", - Method: "GET", - Pattern: debugPathPrefix + "pprof/profile", - }, - { - Name: "GetDebugPprofSymbol", - Method: "GET", - Pattern: debugPathPrefix + "pprof/symbol", - }, - { - Name: "GetDebugPprofTrace", - Method: "GET", - Pattern: debugPathPrefix + "pprof/trace", - }, - { - Name: "GetDebugPprofIndex", - Method: "GET", - Pattern: debugPathPrefix + "pprof/", - }, - }...) - debugRouter := srv.rootRouter.PathPrefix(debugPathPrefix).Subrouter().StrictSlash(true) - debugRouter.Use(rest.Metrics) - debugRouter.Use(corsOpts.Handler) - debugRouter.Use(rest.BasicAuth()) - debugRouter.Methods(http.MethodGet).Path("/pprof/cmdline").Name("GetDebugPprofCmdline").HandlerFunc(pprof.Cmdline) - debugRouter.Methods(http.MethodGet).Path("/pprof/profile").Name("GetDebugPprofProfile").HandlerFunc(pprof.Profile) - debugRouter.Methods(http.MethodGet).Path("/pprof/symbol").Name("GetDebugPprofSymbol").HandlerFunc(pprof.Symbol) - debugRouter.Methods(http.MethodGet).Path("/pprof/trace").Name("GetDebugPprofTrace").HandlerFunc(pprof.Trace) - debugRouter.Methods(http.MethodGet).PathPrefix("/pprof/").Name("GetDebugPprofIndex").HandlerFunc(pprof.Index) - } - srv.Middlewares = append(srv.Middlewares, rest.Recovery) - srv.Use(srv.Middlewares...) - for _, item := range srv.bizRoutes { - srv. - Methods(item.Method, http.MethodOptions). - Path(item.Pattern). - Name(item.Name). - Handler(item.HandlerFunc) - } - srv.rootRouter.NotFoundHandler = srv.rootRouter.NewRoute().BuildOnly().HandlerFunc(http.NotFound).GetHandler() - srv.rootRouter.MethodNotAllowedHandler = srv.rootRouter.NewRoute().BuildOnly().HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusMethodNotAllowed) - w.Write([]byte("405 method not allowed")) - }).GetHandler() - for i := len(srv.Middlewares) - 1; i >= 0; i-- { - srv.rootRouter.NotFoundHandler = srv.Middlewares[i].Middleware(srv.rootRouter.NotFoundHandler) - srv.rootRouter.MethodNotAllowedHandler = srv.Middlewares[i].Middleware(srv.rootRouter.MethodNotAllowedHandler) - } - srv.printRoutes() - httpServer := srv.newHttpServer() - defer func() { - register.ShutdownRest() - grace, err := time.ParseDuration(config.GddGraceTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddGraceTimeout), - config.GddGraceTimeout.Load(), err.Error(), config.DefaultGddGraceTimeout) - grace, _ = time.ParseDuration(config.DefaultGddGraceTimeout) - } - logger.Info().Msgf("Http server is gracefully shutting down in %s", grace) - - ctx, cancel := context.WithTimeout(context.Background(), grace) - defer cancel() - // Doesn't block if no connections, but will otherwise wait - // until the timeout deadline. - httpServer.Shutdown(ctx) - }() - - c := make(chan os.Signal, 1) - // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) - // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. - signal.Notify(c, os.Interrupt) - - // Block until we receive our signal. - <-c -} diff --git a/framework/rest/gorilla/server_test.go b/framework/rest/gorilla/server_test.go deleted file mode 100644 index 23cd212a..00000000 --- a/framework/rest/gorilla/server_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package gorilla - -import ( - "github.com/unionj-cloud/go-doudou/v2/framework/rest" - "testing" -) - -func TestDefaultHttpSrv_printRoutes(t *testing.T) { - srv := NewRestServer() - srv.gddRoutes = append(srv.gddRoutes, []rest.Route{ - { - Name: "GetStatsvizWs", - Method: "GET", - Pattern: gddPathPrefix + "statsviz/ws", - }, - { - Name: "GetStatsviz", - Method: "GET", - Pattern: gddPathPrefix + "statsviz/", - }, - }...) - srv.printRoutes() -} diff --git a/framework/rest/server.go b/framework/rest/server.go index 1e4aa8cd..f481d7b0 100644 --- a/framework/rest/server.go +++ b/framework/rest/server.go @@ -10,20 +10,17 @@ import ( "github.com/olekukonko/tablewriter" "github.com/rs/cors" "github.com/unionj-cloud/go-doudou/v2/framework" - "github.com/unionj-cloud/go-doudou/v2/framework/banner" "github.com/unionj-cloud/go-doudou/v2/framework/config" register "github.com/unionj-cloud/go-doudou/v2/framework/registry" "github.com/unionj-cloud/go-doudou/v2/framework/registry/constants" "github.com/unionj-cloud/go-doudou/v2/framework/rest/httprouter" - "github.com/unionj-cloud/go-doudou/v2/toolkit/cast" - "github.com/unionj-cloud/go-doudou/v2/toolkit/stringutils" logger "github.com/unionj-cloud/go-doudou/v2/toolkit/zlogger" + "net" "net/http" "net/http/pprof" "os" "os/signal" "path" - "strconv" "strings" "time" ) @@ -94,6 +91,7 @@ type RestServer struct { middlewares []MiddlewareFunc data map[string]interface{} panicHandler func(inner http.Handler) http.Handler + *http.Server } func path2key(method, path string) string { @@ -105,15 +103,11 @@ func path2key(method, path string) string { } func (srv *RestServer) printRoutes() { - if !framework.CheckDev() { + if !config.CheckDev() { return } logger.Info().Msg("================ Registered Routes ================") data := [][]string{} - rr := config.DefaultGddRouteRootPath - if stringutils.IsNotEmpty(config.GddRouteRootPath.Load()) { - rr = config.GddRouteRootPath.Load() - } var all []Route all = append(all, srv.bizRoutes...) all = append(all, srv.gddRoutes...) @@ -127,7 +121,7 @@ func (srv *RestServer) printRoutes() { if strings.HasPrefix(r.Pattern, gddPathPrefix) || strings.HasPrefix(r.Pattern, debugPathPrefix) { data = append(data, []string{r.Name, r.Method, r.Pattern}) } else { - data = append(data, []string{r.Name, r.Method, path.Clean(rr + r.Pattern)}) + data = append(data, []string{r.Name, r.Method, path.Clean(config.GddConfig.RouteRootPath + r.Pattern)}) } } } @@ -147,44 +141,11 @@ func (srv *RestServer) printRoutes() { // NewRestServer create a RestServer instance func NewRestServer(data ...map[string]interface{}) *RestServer { - rr := config.DefaultGddRouteRootPath - if stringutils.IsNotEmpty(config.GddRouteRootPath.Load()) { - rr = config.GddRouteRootPath.Load() - } - if stringutils.IsEmpty(rr) { - rr = "/" - } - rootRouter := httprouter.New() - rootRouter.SaveMatchedRoutePath = cast.ToBoolOrDefault(config.GddRouterSaveMatchedRoutePath.Load(), config.DefaultGddRouterSaveMatchedRoutePath) - srv := &RestServer{ - bizRouter: rootRouter.NewGroup(rr), - rootRouter: rootRouter, - panicHandler: recovery, - } - srv.middlewares = append(srv.middlewares, - tracing, - metrics, - gzipBody, - ) - if cast.ToBoolOrDefault(config.GddEnableResponseGzip.Load(), config.DefaultGddEnableResponseGzip) { - gzipMiddleware, err := gzhttp.NewWrapper(gzhttp.ContentTypes(contentTypeShouldbeGzip)) - if err != nil { - panic(err) - } - srv.middlewares = append(srv.middlewares, toMiddlewareFunc(gzipMiddleware)) - } - if cast.ToBoolOrDefault(config.GddLogReqEnable.Load(), config.DefaultGddLogReqEnable) { - srv.middlewares = append(srv.middlewares, log) - } - srv.middlewares = append(srv.middlewares, - requestid.RequestIDHandler, - handlers.ProxyHeaders, - fallbackContentType(config.GddFallbackContentType.LoadOrDefault(config.DefaultGddFallbackContentType)), - ) + var options []ServerOption if len(data) > 0 { - srv.data = data[0] + options = append(options, WithUserData(data[0])) } - return srv + return NewRestServerWithOptions(options...) } type ServerOption func(server *RestServer) @@ -203,19 +164,19 @@ func WithUserData(userData map[string]interface{}) ServerOption { // NewRestServerWithOptions create a RestServer instance with options func NewRestServerWithOptions(options ...ServerOption) *RestServer { - rr := config.DefaultGddRouteRootPath - if stringutils.IsNotEmpty(config.GddRouteRootPath.Load()) { - rr = config.GddRouteRootPath.Load() - } - if stringutils.IsEmpty(rr) { - rr = "/" - } rootRouter := httprouter.New() - rootRouter.SaveMatchedRoutePath = cast.ToBoolOrDefault(config.GddRouterSaveMatchedRoutePath.Load(), config.DefaultGddRouterSaveMatchedRoutePath) + rootRouter.SaveMatchedRoutePath = true srv := &RestServer{ - bizRouter: rootRouter.NewGroup(rr), + bizRouter: rootRouter.NewGroup(config.GddConfig.RouteRootPath), rootRouter: rootRouter, panicHandler: recovery, + Server: &http.Server{ + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: config.GddConfig.WriteTimeout, + ReadTimeout: config.GddConfig.ReadTimeout, + IdleTimeout: config.GddConfig.IdleTimeout, + Handler: rootRouter, // Pass our instance of httprouter.Router in. + }, } for _, fn := range options { fn(srv) @@ -225,20 +186,20 @@ func NewRestServerWithOptions(options ...ServerOption) *RestServer { metrics, gzipBody, ) - if cast.ToBoolOrDefault(config.GddEnableResponseGzip.Load(), config.DefaultGddEnableResponseGzip) { + if config.GddConfig.EnableResponseGzip { gzipMiddleware, err := gzhttp.NewWrapper(gzhttp.ContentTypes(contentTypeShouldbeGzip)) if err != nil { panic(err) } srv.middlewares = append(srv.middlewares, toMiddlewareFunc(gzipMiddleware)) } - if cast.ToBoolOrDefault(config.GddLogReqEnable.Load(), config.DefaultGddLogReqEnable) { + if config.GddConfig.LogReqEnable { srv.middlewares = append(srv.middlewares, log) } srv.middlewares = append(srv.middlewares, requestid.RequestIDHandler, handlers.ProxyHeaders, - fallbackContentType(config.GddFallbackContentType.LoadOrDefault(config.DefaultGddFallbackContentType)), + fallbackContentType(config.GddConfig.FallbackContenttype), ) return srv } @@ -264,62 +225,8 @@ func (srv *RestServer) PreMiddleware(mwf ...func(http.Handler) http.Handler) { srv.middlewares = append(middlewares, srv.middlewares...) } -func (srv *RestServer) newHttpServer() *http.Server { - write, err := time.ParseDuration(config.GddWriteTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddWriteTimeout), - config.GddWriteTimeout.Load(), err.Error(), config.DefaultGddWriteTimeout) - write, _ = time.ParseDuration(config.DefaultGddWriteTimeout) - } - - read, err := time.ParseDuration(config.GddReadTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddReadTimeout), - config.GddReadTimeout.Load(), err.Error(), config.DefaultGddReadTimeout) - read, _ = time.ParseDuration(config.DefaultGddReadTimeout) - } - - idle, err := time.ParseDuration(config.GddIdleTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddIdleTimeout), - config.GddIdleTimeout.Load(), err.Error(), config.DefaultGddIdleTimeout) - idle, _ = time.ParseDuration(config.DefaultGddIdleTimeout) - } - - httpPort := strconv.Itoa(config.DefaultGddPort) - if _, err = cast.ToIntE(config.GddPort.Load()); err == nil { - httpPort = config.GddPort.Load() - } - httpHost := config.DefaultGddHost - if stringutils.IsNotEmpty(config.GddHost.Load()) { - httpHost = config.GddHost.Load() - } - httpServer := &http.Server{ - Addr: strings.Join([]string{httpHost, httpPort}, ":"), - // Good practice to set timeouts to avoid Slowloris attacks. - WriteTimeout: write, - ReadTimeout: read, - IdleTimeout: idle, - Handler: srv.rootRouter, // Pass our instance of httprouter.Router in. - } - - // Run our server in a goroutine so that it doesn't block. - go func() { - if err := httpServer.ListenAndServe(); err != nil { - logger.Error().Err(err).Msg("") - } - }() - - return httpServer -} - -// Run runs http server -func (srv *RestServer) Run() { - banner.Print() - config.PrintLock.Lock() - register.NewRest(srv.data) - manage := cast.ToBoolOrDefault(config.GddManage.Load(), config.DefaultGddManage) - if manage { +func (srv *RestServer) configure() { + if config.GddConfig.ManageEnable { srv.middlewares = append([]MiddlewareFunc{PrometheusMiddleware}, srv.middlewares...) gddRouter := srv.rootRouter.NewGroup(gddPathPrefix) corsOpts := cors.New(cors.Options{ @@ -462,26 +369,25 @@ func (srv *RestServer) Run() { srv.rootRouter.NotFound = srv.middlewares[i].Middleware(srv.rootRouter.NotFound) srv.rootRouter.MethodNotAllowed = srv.middlewares[i].Middleware(srv.rootRouter.MethodNotAllowed) } - srv.printRoutes() - httpServer := srv.newHttpServer() - logger.Info().Msgf("Http server is listening at %v", httpServer.Addr) - logger.Info().Msgf("Http server started in %s", time.Since(startAt)) - config.PrintLock.Unlock() +} + +// Run runs http server +func (srv *RestServer) Run() { + ln, err := net.Listen("tcp", strings.Join([]string{config.GddConfig.Host, config.GddConfig.Port}, ":")) + if err != nil { + logger.Panic().Msg(err.Error()) + } + srv.Serve(ln) defer func() { + // Make sure to set a deadline on exiting the process + // after upg.Exit() is closed. No new upgrades can be + // performed if the parent doesn't exit. + time.AfterFunc(config.GddConfig.GraceTimeout, func() { + logger.Error().Msg("Graceful shutdown timed out") + os.Exit(1) + }) register.ShutdownRest() - grace, err := time.ParseDuration(config.GddGraceTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddGraceTimeout), - config.GddGraceTimeout.Load(), err.Error(), config.DefaultGddGraceTimeout) - grace, _ = time.ParseDuration(config.DefaultGddGraceTimeout) - } - logger.Info().Msgf("Http server is gracefully shutting down in %s", grace) - - ctx, cancel := context.WithTimeout(context.Background(), grace) - defer cancel() - // Doesn't block if no connections, but will otherwise wait - // until the timeout deadline. - httpServer.Shutdown(ctx) + srv.Shutdown(context.Background()) }() c := make(chan os.Signal, 1) @@ -492,3 +398,20 @@ func (srv *RestServer) Run() { // Block until we receive our signal. <-c } + +func (srv *RestServer) Serve(ln net.Listener) { + framework.PrintBanner() + framework.PrintLock.Lock() + register.NewRest(srv.data) + srv.configure() + srv.printRoutes() + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.Server.Serve(ln); err != http.ErrServerClosed { + logger.Error().Msgf("HTTP server: %s", err.Error()) + } + }() + logger.Info().Msgf("Http server is listening at %v", ln.Addr().String()) + logger.Info().Msgf("Http server started in %s", time.Since(startAt)) + framework.PrintLock.Unlock() +} diff --git a/go.mod b/go.mod index fb71045f..bdb5c9a0 100644 --- a/go.mod +++ b/go.mod @@ -67,6 +67,7 @@ require ( require ( github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 // indirect + github.com/cloudflare/tableflip v1.2.3 // indirect github.com/cockroachdb/apd v1.1.1-0.20181017181144-bced77f817b4 // indirect github.com/cockroachdb/errors v1.8.2 // indirect github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect diff --git a/go.sum b/go.sum index bffd7fb4..94d90a5c 100644 --- a/go.sum +++ b/go.sum @@ -155,6 +155,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= @@ -201,6 +202,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cloudflare/tableflip v1.2.3 h1:8I+B99QnnEWPHOY3fWipwVKxS70LGgUsslG7CSfmHMw= +github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= @@ -720,10 +723,16 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= From d3f3a2371ab55fef434b10fa9019edfc22a56a08 Mon Sep 17 00:00:00 2001 From: wubin1989 <328454505@qq.com> Date: Mon, 8 Jan 2024 23:32:17 +0800 Subject: [PATCH 2/3] some optimize --- framework/config/config.go | 4 ++-- framework/grpcx/server.go | 21 +++++++++------------ framework/rest/server.go | 1 + toolkit/dbvendor/ivendor.go | 2 +- toolkit/dbvendor/postgres/template.go | 10 +++++++--- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/framework/config/config.go b/framework/config/config.go index 1e676b6c..770df299 100644 --- a/framework/config/config.go +++ b/framework/config/config.go @@ -95,9 +95,9 @@ func CheckDev() bool { func init() { LoadConfigFromLocal() LoadConfigFromRemote() - err := envconfig.Process("gdd", &GddConfig) + err := envconfig.Process("gdd", GddConfig) if err != nil { - zlogger.Panic().Msg("Error processing environment variables") + zlogger.Panic().Msgf("Error processing environment variables: %s", err.Error()) } zl, _ := zerolog.ParseLevel(GddLogLevel.LoadOrDefault(DefaultGddLogLevel)) opts := []zlogger.LoggerConfigOption{ diff --git a/framework/grpcx/server.go b/framework/grpcx/server.go index 8c14a716..054f3ef4 100644 --- a/framework/grpcx/server.go +++ b/framework/grpcx/server.go @@ -92,19 +92,16 @@ func (srv *GrpcServer) RunWithPipe(pipe net.Listener) { } srv.ServeWithPipe(ln, pipe) defer func() { + logger.Info().Msgf("Grpc server is gracefully shutting down in %s", config.GddConfig.GraceTimeout) + // Make sure to set a deadline on exiting the process + // after upg.Exit() is closed. No new upgrades can be + // performed if the parent doesn't exit. + time.AfterFunc(config.GddConfig.GraceTimeout, func() { + logger.Error().Msg("Graceful shutdown timed out") + os.Exit(1) + }) register.ShutdownGrpc() - - grace, err := time.ParseDuration(config.GddGraceTimeout.Load()) - if err != nil { - logger.Debug().Msgf("Parse %s %s as time.Duration failed: %s, use default %s instead.\n", string(config.GddGraceTimeout), - config.GddGraceTimeout.Load(), err.Error(), config.DefaultGddGraceTimeout) - grace, _ = time.ParseDuration(config.DefaultGddGraceTimeout) - } - logger.Info().Msgf("Grpc server is gracefully shutting down in %s", grace) - - ctx, cancel := context.WithTimeout(context.Background(), grace) - defer cancel() - if err := timeutils.CallWithCtx(ctx, func() struct{} { + if err := timeutils.CallWithCtx(context.Background(), func() struct{} { srv.GracefulStop() return struct{}{} }); err != nil { diff --git a/framework/rest/server.go b/framework/rest/server.go index f481d7b0..019c4008 100644 --- a/framework/rest/server.go +++ b/framework/rest/server.go @@ -379,6 +379,7 @@ func (srv *RestServer) Run() { } srv.Serve(ln) defer func() { + logger.Info().Msgf("Grpc server is gracefully shutting down in %s", config.GddConfig.GraceTimeout) // Make sure to set a deadline on exiting the process // after upg.Exit() is closed. No new upgrades can be // performed if the parent doesn't exit. diff --git a/toolkit/dbvendor/ivendor.go b/toolkit/dbvendor/ivendor.go index 7db9c0c7..0b7a2c9e 100644 --- a/toolkit/dbvendor/ivendor.go +++ b/toolkit/dbvendor/ivendor.go @@ -81,7 +81,7 @@ type Table struct { Name string Columns []Column BizColumns []Column - Pk string + Pk []string Joins []string // 父表 Inherited string diff --git a/toolkit/dbvendor/postgres/template.go b/toolkit/dbvendor/postgres/template.go index 29098a61..31b8bab1 100644 --- a/toolkit/dbvendor/postgres/template.go +++ b/toolkit/dbvendor/postgres/template.go @@ -2,10 +2,14 @@ package postgres var ( createTable = `CREATE TABLE IF NOT EXISTS {{if .TablePrefix }}"{{.TablePrefix}}".{{end}}"{{.Name}}" ( -{{- range $co := .Columns }} -"{{$co.Name}}" {{$co.Type}} {{if $co.Nullable}}NULL{{else}}NOT NULL{{end}}{{if $co.Default}} DEFAULT '{{$co.Default}}'{{end}}, +{{- range $i, $co := .Columns }} +{{- if $i}},{{end}} +"{{$co.Name}}" {{$co.Type}} {{if $co.Nullable}}NULL{{else}}NOT NULL{{end}}{{if $co.Default}} DEFAULT '{{$co.Default}}'{{end}} {{- end }} -PRIMARY KEY ("{{.Pk}}")) +{{- if .Pk }}, +PRIMARY KEY ({{- range $i, $co := .Pk }}{{- if $i}},{{end}}"{{$co}}"{{- end }}) +{{- end }} +) {{- if .Inherited }} INHERITS ({{.Inherited}}) {{- end }}; From b3c87b8d258453512a8ff3878be0f5cdcf4858b7 Mon Sep 17 00:00:00 2001 From: wubin1989 <328454505@qq.com> Date: Mon, 8 Jan 2024 23:33:00 +0800 Subject: [PATCH 3/3] v2.2.5 --- version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version/version.go b/version/version.go index 7ab70f7b..845c1edb 100644 --- a/version/version.go +++ b/version/version.go @@ -1,3 +1,3 @@ package version -const Release = "v2.2.4" +const Release = "v2.2.5"