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 }