github.com/wfusion/gofusion@v1.1.14/http/construct.go (about)

     1  package http
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"reflect"
     9  	"sync"
    10  	"syscall"
    11  	"time"
    12  
    13  	"github.com/gin-contrib/pprof"
    14  	"github.com/gin-gonic/gin"
    15  	"github.com/go-resty/resty/v2"
    16  	"github.com/pkg/errors"
    17  	"golang.org/x/text/language"
    18  
    19  	"github.com/wfusion/gofusion/common/di"
    20  	"github.com/wfusion/gofusion/common/utils"
    21  	"github.com/wfusion/gofusion/common/utils/clone"
    22  	"github.com/wfusion/gofusion/common/utils/inspect"
    23  	"github.com/wfusion/gofusion/config"
    24  	"github.com/wfusion/gofusion/http/middleware"
    25  	"github.com/wfusion/gofusion/i18n"
    26  
    27  	fusLog "github.com/wfusion/gofusion/log"
    28  
    29  	_ "github.com/wfusion/gofusion/log/customlogger"
    30  )
    31  
    32  var (
    33  	Router IRouter
    34  
    35  	locker          sync.RWMutex
    36  	routers         = map[string]IRouter{}
    37  	appClientMap    = map[string]map[string]*resty.Client{}
    38  	appClientCfgMap = map[string]map[string]*cfg{}
    39  )
    40  
    41  func Construct(ctx context.Context, conf Conf, opts ...utils.OptionExtender) func() {
    42  	opt := utils.ApplyOptions[config.InitOption](opts...)
    43  	optU := utils.ApplyOptions[useOption](opts...)
    44  	if opt.AppName == "" {
    45  		opt.AppName = optU.appName
    46  	}
    47  
    48  	var logger resty.Logger
    49  	if utils.IsStrNotBlank(conf.Logger) {
    50  		logger = reflect.New(inspect.TypeOf(conf.Logger)).Interface().(resty.Logger)
    51  		if custom, ok := logger.(customLogger); ok {
    52  			l := fusLog.Use(conf.LogInstance, fusLog.AppName(opt.AppName))
    53  			custom.Init(l, opt.AppName)
    54  		}
    55  	}
    56  
    57  	exitRouterFn := addRouter(ctx, conf, logger, opt)
    58  	exitI18nFn := addI18n(conf, opt)
    59  	exitClientFn := addClient(ctx, conf, logger, opt)
    60  
    61  	// gracefully exit outside gofusion
    62  	return func() {
    63  		exitClientFn()
    64  		exitRouterFn()
    65  		exitI18nFn()
    66  	}
    67  }
    68  
    69  func addRouter(ctx context.Context, conf Conf, logger resty.Logger, opt *config.InitOption) func() {
    70  	if config.Use(opt.AppName).Debug() {
    71  		gin.SetMode(gin.DebugMode)
    72  	} else {
    73  		gin.SetMode(gin.ReleaseMode)
    74  	}
    75  	if !conf.ColorfulConsole {
    76  		gin.DisableConsoleColor()
    77  	} else {
    78  		gin.ForceConsoleColor()
    79  	}
    80  
    81  	engine := gin.New()
    82  	engine.Use(
    83  		gin.Recovery(),
    84  		middleware.Gateway,
    85  		middleware.Trace(),
    86  		middleware.Logging(ctx, opt.AppName, conf.Metrics.HeaderLabels, logger),
    87  		middleware.Cors(),
    88  		middleware.XSS(conf.XSSWhiteURLList),
    89  		middleware.Recover(opt.AppName, logger, map[string]any{
    90  			"code":    Errcode(conf.ErrorCode),
    91  			"message": "service internal error",
    92  		}),
    93  	)
    94  
    95  	tag := i18n.DefaultLang(i18n.AppName(opt.AppName))
    96  	engine.NoMethod(func(c *gin.Context) {
    97  		c.Status(http.StatusMethodNotAllowed)
    98  		msg := fmt.Sprintf("找不到该方法, Method: %s", c.Request.Method)
    99  		if tag != language.Chinese {
   100  			msg = fmt.Sprintf("Cannot find method: %s", c.Request.Method)
   101  		}
   102  
   103  		rspError(c, opt.AppName, Errcode(conf.ErrorCode), nil, 0, 0, msg)
   104  	})
   105  	engine.NoRoute(func(c *gin.Context) {
   106  		c.Status(http.StatusNotFound)
   107  		msg := fmt.Sprintf("找不到该内容, URL: %s", c.Request.URL.String())
   108  		if tag != language.Chinese {
   109  			msg = fmt.Sprintf("Cannot find URL content: %s", c.Request.URL.String())
   110  		}
   111  		rspError(c, opt.AppName, Errcode(conf.ErrorCode), nil, 0, 0, msg)
   112  	})
   113  
   114  	if conf.Pprof {
   115  		pprof.Register(engine)
   116  	}
   117  	instance := newRouter(ctx, engine, opt.AppName, conf.SuccessCode, conf.ErrorCode)
   118  	instance.(*router).metricsConf = conf.Metrics
   119  
   120  	locker.Lock()
   121  	defer locker.Unlock()
   122  	if len(conf.Asynq) > 0 {
   123  		initAsynq(ctx, opt.AppName, instance, conf.Asynq)
   124  	}
   125  	if _, ok := routers[opt.AppName]; ok {
   126  		panic(errors.Errorf("duplicated http name: %s", opt.AppName))
   127  	}
   128  	routers[opt.AppName] = instance
   129  	if opt.AppName == "" {
   130  		Router = instance
   131  	}
   132  
   133  	if opt.DI != nil {
   134  		opt.DI.MustProvide(func() IRouter { return Use(AppName(opt.AppName)) })
   135  	}
   136  
   137  	return func() {
   138  		locker.Lock()
   139  		defer locker.Unlock()
   140  		if routers != nil {
   141  			if router, ok := routers[opt.AppName]; ok {
   142  				router.shutdown()
   143  				wg := new(sync.WaitGroup)
   144  				wg.Add(1)
   145  				go func() { defer wg.Done(); <-router.Closing() }()
   146  				if utils.Timeout(15*time.Second, utils.TimeoutWg(wg)) {
   147  					pid := syscall.Getpid()
   148  					app := config.Use(opt.AppName).AppName()
   149  					log.Printf("%v [Gofusion] %s %s close http server timeout", pid, app, config.ComponentHttp)
   150  				}
   151  			}
   152  
   153  			delete(routers, opt.AppName)
   154  		}
   155  		if opt.AppName == "" {
   156  			Router = nil
   157  		}
   158  	}
   159  }
   160  
   161  func addI18n(conf Conf, opt *config.InitOption) func() {
   162  	bundle := i18n.NewBundle[Errcode](i18n.DefaultLang(i18n.AppName(opt.AppName)))
   163  	if I18n == nil {
   164  		I18n = bundle
   165  	}
   166  	if i18ns == nil {
   167  		i18ns = make(map[string]i18n.Localizable[Errcode])
   168  	}
   169  
   170  	i18ns[opt.AppName] = bundle
   171  
   172  	// initialize http internal error
   173  	bundle.AddMessages(Errcode(conf.ErrorCode), map[language.Tag]*i18n.Message{
   174  		language.English: {Other: "Invalid request parameters{{.err}}"},
   175  		language.Chinese: {Other: "请求参数错误{{.err}}"},
   176  	}, i18n.Var("err"))
   177  
   178  	if opt.DI != nil {
   179  		opt.DI.MustProvide(func() i18n.Localizable[Errcode] { return bundle })
   180  	}
   181  
   182  	errParam = Errcode(conf.ErrorCode)
   183  
   184  	ginBindingValidatorI18n(opt.AppName)
   185  
   186  	return func() {
   187  		locker.Lock()
   188  		defer locker.Unlock()
   189  		if i18ns != nil {
   190  			delete(i18ns, opt.AppName)
   191  		}
   192  		if opt.AppName == "" {
   193  			I18n = nil
   194  		}
   195  	}
   196  }
   197  
   198  func addClient(ctx context.Context, conf Conf, logger resty.Logger, opt *config.InitOption) func() {
   199  	if _, ok := appClientCfgMap[opt.AppName]; !ok {
   200  		defaultCfg := &cfg{
   201  			c:       clone.Clone(defaultClientConf),
   202  			appName: opt.AppName,
   203  			logger:  logger,
   204  		}
   205  		appClientCfgMap[opt.AppName] = map[string]*cfg{
   206  			"":        defaultCfg,
   207  			"default": defaultCfg,
   208  		}
   209  	}
   210  	for name, cliConf := range conf.Clients {
   211  		cliCfg := &cfg{
   212  			c:       cliConf,
   213  			appName: opt.AppName,
   214  			logger:  logger,
   215  		}
   216  		appClientCfgMap[opt.AppName][name] = cliCfg
   217  		if name == config.DefaultInstanceKey {
   218  			appClientCfgMap[opt.AppName][""] = cliCfg
   219  		}
   220  
   221  		if opt.AppName == "" && name == config.DefaultInstanceKey {
   222  			Client = New(AppName(opt.AppName), CName(name))
   223  		}
   224  	}
   225  
   226  	if opt.DI != nil {
   227  		for name := range conf.Clients {
   228  			opt.DI.MustProvide(func() *resty.Client { return New(AppName(opt.AppName), CName(name)) }, di.Name(name))
   229  		}
   230  	}
   231  
   232  	return func() {
   233  		locker.Lock()
   234  		defer locker.Unlock()
   235  		if appClientMap != nil {
   236  			delete(appClientMap, opt.AppName)
   237  		}
   238  		if appClientCfgMap != nil {
   239  			delete(appClientCfgMap, opt.AppName)
   240  		}
   241  		if opt.AppName == "" {
   242  			Client = nil
   243  		}
   244  	}
   245  }
   246  
   247  type useOption struct {
   248  	appName string
   249  }
   250  
   251  func AppName(name string) utils.OptionFunc[useOption] {
   252  	return func(o *useOption) {
   253  		o.appName = name
   254  	}
   255  }
   256  
   257  func Use(opts ...utils.OptionExtender) IRouter {
   258  	opt := utils.ApplyOptions[useOption](opts...)
   259  	locker.RLock()
   260  	defer locker.RUnlock()
   261  
   262  	router, ok := routers[opt.appName]
   263  	if !ok {
   264  		panic(errors.Errorf("router not found"))
   265  	}
   266  	return router
   267  }
   268  
   269  func init() {
   270  	config.AddComponent(config.ComponentHttp, Construct, config.WithFlag(&flagString))
   271  }