core.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. package core
  2. import (
  3. "fmt"
  4. "html/template"
  5. "net/http"
  6. "net/url"
  7. "runtime/debug"
  8. "time"
  9. "github.com/xinliangnote/go-gin-api/assets"
  10. "github.com/xinliangnote/go-gin-api/configs"
  11. _ "github.com/xinliangnote/go-gin-api/docs"
  12. "github.com/xinliangnote/go-gin-api/internal/code"
  13. "github.com/xinliangnote/go-gin-api/internal/proposal"
  14. "github.com/xinliangnote/go-gin-api/pkg/browser"
  15. "github.com/xinliangnote/go-gin-api/pkg/color"
  16. "github.com/xinliangnote/go-gin-api/pkg/env"
  17. "github.com/xinliangnote/go-gin-api/pkg/errors"
  18. "github.com/xinliangnote/go-gin-api/pkg/trace"
  19. "github.com/gin-contrib/pprof"
  20. "github.com/gin-gonic/gin"
  21. "github.com/prometheus/client_golang/prometheus/promhttp"
  22. cors "github.com/rs/cors/wrapper/gin"
  23. ginSwagger "github.com/swaggo/gin-swagger"
  24. "github.com/swaggo/gin-swagger/swaggerFiles"
  25. "go.uber.org/multierr"
  26. "go.uber.org/zap"
  27. "golang.org/x/time/rate"
  28. )
  29. // see https://patorjk.com/software/taag/#p=testall&f=Graffiti&t=go-gin-api
  30. const _UI = `
  31. ██████╗ ██████╗ ██████╗ ██╗███╗ ██╗ █████╗ ██████╗ ██╗
  32. ██╔════╝ ██╔═══██╗ ██╔════╝ ██║████╗ ██║ ██╔══██╗██╔══██╗██║
  33. ██║ ███╗██║ ██║█████╗██║ ███╗██║██╔██╗ ██║█████╗███████║██████╔╝██║
  34. ██║ ██║██║ ██║╚════╝██║ ██║██║██║╚██╗██║╚════╝██╔══██║██╔═══╝ ██║
  35. ╚██████╔╝╚██████╔╝ ╚██████╔╝██║██║ ╚████║ ██║ ██║██║ ██║
  36. ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝
  37. `
  38. type Option func(*option)
  39. type option struct {
  40. disablePProf bool
  41. disableSwagger bool
  42. disablePrometheus bool
  43. enableCors bool
  44. enableRate bool
  45. enableOpenBrowser string
  46. alertNotify proposal.NotifyHandler
  47. recordHandler proposal.RecordHandler
  48. }
  49. // WithDisablePProf 禁用 pprof
  50. func WithDisablePProf() Option {
  51. return func(opt *option) {
  52. opt.disablePProf = true
  53. }
  54. }
  55. // WithDisableSwagger 禁用 swagger
  56. func WithDisableSwagger() Option {
  57. return func(opt *option) {
  58. opt.disableSwagger = true
  59. }
  60. }
  61. // WithDisablePrometheus 禁用prometheus
  62. func WithDisablePrometheus() Option {
  63. return func(opt *option) {
  64. opt.disablePrometheus = true
  65. }
  66. }
  67. // WithAlertNotify 设置告警通知
  68. func WithAlertNotify(notifyHandler proposal.NotifyHandler) Option {
  69. return func(opt *option) {
  70. opt.alertNotify = notifyHandler
  71. }
  72. }
  73. // WithRecordMetrics 设置记录接口指标
  74. func WithRecordMetrics(recordHandler proposal.RecordHandler) Option {
  75. return func(opt *option) {
  76. opt.recordHandler = recordHandler
  77. }
  78. }
  79. // WithEnableOpenBrowser 启动后在浏览器中打开 uri
  80. func WithEnableOpenBrowser(uri string) Option {
  81. return func(opt *option) {
  82. opt.enableOpenBrowser = uri
  83. }
  84. }
  85. // WithEnableCors 设置支持跨域
  86. func WithEnableCors() Option {
  87. return func(opt *option) {
  88. opt.enableCors = true
  89. }
  90. }
  91. // WithEnableRate 设置支持限流
  92. func WithEnableRate() Option {
  93. return func(opt *option) {
  94. opt.enableRate = true
  95. }
  96. }
  97. // DisableTraceLog 禁止记录日志
  98. func DisableTraceLog(ctx Context) {
  99. ctx.disableTrace()
  100. }
  101. // DisableRecordMetrics 禁止记录指标
  102. func DisableRecordMetrics(ctx Context) {
  103. ctx.disableRecordMetrics()
  104. }
  105. // AliasForRecordMetrics 对请求路径起个别名,用于记录指标。
  106. // 如:Get /user/:username 这样的路径,因为 username 会有非常多的情况,这样记录指标非常不友好。
  107. func AliasForRecordMetrics(path string) HandlerFunc {
  108. return func(ctx Context) {
  109. ctx.setAlias(path)
  110. }
  111. }
  112. // WrapAuthHandler 用来处理 Auth 的入口
  113. func WrapAuthHandler(handler func(Context) (sessionUserInfo proposal.SessionUserInfo, err BusinessError)) HandlerFunc {
  114. return func(ctx Context) {
  115. sessionUserInfo, err := handler(ctx)
  116. if err != nil {
  117. ctx.AbortWithError(err)
  118. return
  119. }
  120. ctx.setSessionUserInfo(sessionUserInfo)
  121. }
  122. }
  123. // RouterGroup 包装gin的RouterGroup
  124. type RouterGroup interface {
  125. Group(string, ...HandlerFunc) RouterGroup
  126. IRoutes
  127. }
  128. var _ IRoutes = (*router)(nil)
  129. // IRoutes 包装gin的IRoutes
  130. type IRoutes interface {
  131. Any(string, ...HandlerFunc)
  132. GET(string, ...HandlerFunc)
  133. POST(string, ...HandlerFunc)
  134. DELETE(string, ...HandlerFunc)
  135. PATCH(string, ...HandlerFunc)
  136. PUT(string, ...HandlerFunc)
  137. OPTIONS(string, ...HandlerFunc)
  138. HEAD(string, ...HandlerFunc)
  139. }
  140. type router struct {
  141. group *gin.RouterGroup
  142. }
  143. func (r *router) Group(relativePath string, handlers ...HandlerFunc) RouterGroup {
  144. group := r.group.Group(relativePath, wrapHandlers(handlers...)...)
  145. return &router{group: group}
  146. }
  147. func (r *router) Any(relativePath string, handlers ...HandlerFunc) {
  148. r.group.Any(relativePath, wrapHandlers(handlers...)...)
  149. }
  150. func (r *router) GET(relativePath string, handlers ...HandlerFunc) {
  151. r.group.GET(relativePath, wrapHandlers(handlers...)...)
  152. }
  153. func (r *router) POST(relativePath string, handlers ...HandlerFunc) {
  154. r.group.POST(relativePath, wrapHandlers(handlers...)...)
  155. }
  156. func (r *router) DELETE(relativePath string, handlers ...HandlerFunc) {
  157. r.group.DELETE(relativePath, wrapHandlers(handlers...)...)
  158. }
  159. func (r *router) PATCH(relativePath string, handlers ...HandlerFunc) {
  160. r.group.PATCH(relativePath, wrapHandlers(handlers...)...)
  161. }
  162. func (r *router) PUT(relativePath string, handlers ...HandlerFunc) {
  163. r.group.PUT(relativePath, wrapHandlers(handlers...)...)
  164. }
  165. func (r *router) OPTIONS(relativePath string, handlers ...HandlerFunc) {
  166. r.group.OPTIONS(relativePath, wrapHandlers(handlers...)...)
  167. }
  168. func (r *router) HEAD(relativePath string, handlers ...HandlerFunc) {
  169. r.group.HEAD(relativePath, wrapHandlers(handlers...)...)
  170. }
  171. func wrapHandlers(handlers ...HandlerFunc) []gin.HandlerFunc {
  172. funcs := make([]gin.HandlerFunc, len(handlers))
  173. for i, handler := range handlers {
  174. handler := handler
  175. funcs[i] = func(c *gin.Context) {
  176. ctx := newContext(c)
  177. defer releaseContext(ctx)
  178. handler(ctx)
  179. }
  180. }
  181. return funcs
  182. }
  183. var _ Mux = (*mux)(nil)
  184. // Mux http mux
  185. type Mux interface {
  186. http.Handler
  187. Group(relativePath string, handlers ...HandlerFunc) RouterGroup
  188. }
  189. type mux struct {
  190. engine *gin.Engine
  191. }
  192. func (m *mux) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  193. m.engine.ServeHTTP(w, req)
  194. }
  195. func (m *mux) Group(relativePath string, handlers ...HandlerFunc) RouterGroup {
  196. return &router{
  197. group: m.engine.Group(relativePath, wrapHandlers(handlers...)...),
  198. }
  199. }
  200. func New(logger *zap.Logger, options ...Option) (Mux, error) {
  201. if logger == nil {
  202. return nil, errors.New("logger required")
  203. }
  204. gin.SetMode(gin.ReleaseMode)
  205. mux := &mux{
  206. engine: gin.New(),
  207. }
  208. fmt.Println(color.Blue(_UI))
  209. mux.engine.StaticFS("assets", http.FS(assets.Bootstrap))
  210. mux.engine.SetHTMLTemplate(template.Must(template.New("").ParseFS(assets.Templates, "templates/**/*")))
  211. // withoutTracePaths 这些请求,默认不记录日志
  212. withoutTracePaths := map[string]bool{
  213. "/metrics": true,
  214. "/debug/pprof/": true,
  215. "/debug/pprof/cmdline": true,
  216. "/debug/pprof/profile": true,
  217. "/debug/pprof/symbol": true,
  218. "/debug/pprof/trace": true,
  219. "/debug/pprof/allocs": true,
  220. "/debug/pprof/block": true,
  221. "/debug/pprof/goroutine": true,
  222. "/debug/pprof/heap": true,
  223. "/debug/pprof/mutex": true,
  224. "/debug/pprof/threadcreate": true,
  225. "/favicon.ico": true,
  226. "/system/health": true,
  227. }
  228. opt := new(option)
  229. for _, f := range options {
  230. f(opt)
  231. }
  232. if !opt.disablePProf {
  233. if !env.Active().IsPro() {
  234. pprof.Register(mux.engine) // register pprof to gin
  235. }
  236. }
  237. if !opt.disableSwagger {
  238. if !env.Active().IsPro() {
  239. mux.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // register swagger
  240. }
  241. }
  242. if !opt.disablePrometheus {
  243. mux.engine.GET("/metrics", gin.WrapH(promhttp.Handler())) // register prometheus
  244. }
  245. if opt.enableCors {
  246. mux.engine.Use(cors.New(cors.Options{
  247. AllowedOrigins: []string{"*"},
  248. AllowedMethods: []string{
  249. http.MethodHead,
  250. http.MethodGet,
  251. http.MethodPost,
  252. http.MethodPut,
  253. http.MethodPatch,
  254. http.MethodDelete,
  255. },
  256. AllowedHeaders: []string{"*"},
  257. AllowCredentials: true,
  258. OptionsPassthrough: true,
  259. }))
  260. }
  261. if opt.enableOpenBrowser != "" {
  262. _ = browser.Open(opt.enableOpenBrowser)
  263. }
  264. // recover两次,防止处理时发生panic,尤其是在OnPanicNotify中。
  265. mux.engine.Use(func(ctx *gin.Context) {
  266. defer func() {
  267. if err := recover(); err != nil {
  268. logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", string(debug.Stack())))
  269. }
  270. }()
  271. ctx.Next()
  272. })
  273. mux.engine.Use(func(ctx *gin.Context) {
  274. if ctx.Writer.Status() == http.StatusNotFound {
  275. return
  276. }
  277. ts := time.Now()
  278. context := newContext(ctx)
  279. defer releaseContext(context)
  280. context.init()
  281. context.setLogger(logger)
  282. context.ableRecordMetrics()
  283. if !withoutTracePaths[ctx.Request.URL.Path] {
  284. if traceId := context.GetHeader(trace.Header); traceId != "" {
  285. context.setTrace(trace.New(traceId))
  286. } else {
  287. context.setTrace(trace.New(""))
  288. }
  289. }
  290. defer func() {
  291. var (
  292. response interface{}
  293. businessCode int
  294. businessCodeMsg string
  295. abortErr error
  296. traceId string
  297. graphResponse interface{}
  298. )
  299. if ct := context.Trace(); ct != nil {
  300. context.SetHeader(trace.Header, ct.ID())
  301. traceId = ct.ID()
  302. }
  303. // region 发生 Panic 异常发送告警提醒
  304. if err := recover(); err != nil {
  305. stackInfo := string(debug.Stack())
  306. logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", stackInfo))
  307. context.AbortWithError(Error(
  308. http.StatusInternalServerError,
  309. code.ServerError,
  310. code.Text(code.ServerError)),
  311. )
  312. if notifyHandler := opt.alertNotify; notifyHandler != nil {
  313. notifyHandler(&proposal.AlertMessage{
  314. ProjectName: configs.ProjectName,
  315. Env: env.Active().Value(),
  316. TraceID: traceId,
  317. HOST: context.Host(),
  318. URI: context.URI(),
  319. Method: context.Method(),
  320. ErrorMessage: err,
  321. ErrorStack: stackInfo,
  322. Timestamp: time.Now(),
  323. })
  324. }
  325. }
  326. // endregion
  327. // region 发生错误,进行返回
  328. if ctx.IsAborted() {
  329. for i := range ctx.Errors {
  330. multierr.AppendInto(&abortErr, ctx.Errors[i])
  331. }
  332. if err := context.abortError(); err != nil { // customer err
  333. // 判断是否需要发送告警通知
  334. if err.IsAlert() {
  335. if notifyHandler := opt.alertNotify; notifyHandler != nil {
  336. notifyHandler(&proposal.AlertMessage{
  337. ProjectName: configs.ProjectName,
  338. Env: env.Active().Value(),
  339. TraceID: traceId,
  340. HOST: context.Host(),
  341. URI: context.URI(),
  342. Method: context.Method(),
  343. ErrorMessage: err.Message(),
  344. ErrorStack: fmt.Sprintf("%+v", err.StackError()),
  345. Timestamp: time.Now(),
  346. })
  347. }
  348. }
  349. multierr.AppendInto(&abortErr, err.StackError())
  350. businessCode = err.BusinessCode()
  351. businessCodeMsg = err.Message()
  352. response = &code.Failure{
  353. Code: businessCode,
  354. Message: businessCodeMsg,
  355. }
  356. ctx.JSON(err.HTTPCode(), response)
  357. }
  358. }
  359. // endregion
  360. // region 正确返回
  361. response = context.getPayload()
  362. if response != nil {
  363. ctx.JSON(http.StatusOK, response)
  364. }
  365. // endregion
  366. // region 记录指标
  367. if opt.recordHandler != nil && context.isRecordMetrics() {
  368. path := context.Path()
  369. if alias := context.Alias(); alias != "" {
  370. path = alias
  371. }
  372. opt.recordHandler(&proposal.MetricsMessage{
  373. ProjectName: configs.ProjectName,
  374. Env: env.Active().Value(),
  375. TraceID: traceId,
  376. HOST: context.Host(),
  377. Path: path,
  378. Method: context.Method(),
  379. HTTPCode: ctx.Writer.Status(),
  380. BusinessCode: businessCode,
  381. CostSeconds: time.Since(ts).Seconds(),
  382. IsSuccess: !ctx.IsAborted() && (ctx.Writer.Status() == http.StatusOK),
  383. })
  384. }
  385. // endregion
  386. // region 记录日志
  387. var t *trace.Trace
  388. if x := context.Trace(); x != nil {
  389. t = x.(*trace.Trace)
  390. } else {
  391. return
  392. }
  393. decodedURL, _ := url.QueryUnescape(ctx.Request.URL.RequestURI())
  394. // ctx.Request.Header,精简 Header 参数
  395. traceHeader := map[string]string{
  396. "Content-Type": ctx.GetHeader("Content-Type"),
  397. configs.HeaderLoginToken: ctx.GetHeader(configs.HeaderLoginToken),
  398. configs.HeaderSignToken: ctx.GetHeader(configs.HeaderSignToken),
  399. configs.HeaderSignTokenDate: ctx.GetHeader(configs.HeaderSignTokenDate),
  400. }
  401. t.WithRequest(&trace.Request{
  402. TTL: "un-limit",
  403. Method: ctx.Request.Method,
  404. DecodedURL: decodedURL,
  405. Header: traceHeader,
  406. Body: string(context.RawData()),
  407. })
  408. var responseBody interface{}
  409. if response != nil {
  410. responseBody = response
  411. }
  412. graphResponse = context.getGraphPayload()
  413. if graphResponse != nil {
  414. responseBody = graphResponse
  415. }
  416. t.WithResponse(&trace.Response{
  417. Header: ctx.Writer.Header(),
  418. HttpCode: ctx.Writer.Status(),
  419. HttpCodeMsg: http.StatusText(ctx.Writer.Status()),
  420. BusinessCode: businessCode,
  421. BusinessCodeMsg: businessCodeMsg,
  422. Body: responseBody,
  423. CostSeconds: time.Since(ts).Seconds(),
  424. })
  425. t.Success = !ctx.IsAborted() && (ctx.Writer.Status() == http.StatusOK)
  426. t.CostSeconds = time.Since(ts).Seconds()
  427. logger.Info("trace-log",
  428. zap.Any("method", ctx.Request.Method),
  429. zap.Any("path", decodedURL),
  430. zap.Any("http_code", ctx.Writer.Status()),
  431. zap.Any("business_code", businessCode),
  432. zap.Any("success", t.Success),
  433. zap.Any("cost_seconds", t.CostSeconds),
  434. zap.Any("trace_id", t.Identifier),
  435. zap.Any("trace_info", t),
  436. zap.Error(abortErr),
  437. )
  438. // endregion
  439. }()
  440. ctx.Next()
  441. })
  442. if opt.enableRate {
  443. limiter := rate.NewLimiter(rate.Every(time.Second*1), configs.MaxRequestsPerSecond)
  444. mux.engine.Use(func(ctx *gin.Context) {
  445. context := newContext(ctx)
  446. defer releaseContext(context)
  447. if !limiter.Allow() {
  448. context.AbortWithError(Error(
  449. http.StatusTooManyRequests,
  450. code.TooManyRequests,
  451. code.Text(code.TooManyRequests)),
  452. )
  453. return
  454. }
  455. ctx.Next()
  456. })
  457. }
  458. mux.engine.NoMethod(wrapHandlers(DisableTraceLog)...)
  459. mux.engine.NoRoute(wrapHandlers(DisableTraceLog)...)
  460. system := mux.Group("/system")
  461. {
  462. // 健康检查
  463. system.GET("/health", func(ctx Context) {
  464. resp := &struct {
  465. Timestamp time.Time `json:"timestamp"`
  466. Environment string `json:"environment"`
  467. Host string `json:"host"`
  468. Status string `json:"status"`
  469. }{
  470. Timestamp: time.Now(),
  471. Environment: env.Active().Value(),
  472. Host: ctx.Host(),
  473. Status: "ok",
  474. }
  475. ctx.Payload(resp)
  476. })
  477. }
  478. return mux, nil
  479. }