package core import ( "fmt" "html/template" "net/http" "net/url" "runtime/debug" "time" "github.com/xinliangnote/go-gin-api/assets" "github.com/xinliangnote/go-gin-api/configs" _ "github.com/xinliangnote/go-gin-api/docs" "github.com/xinliangnote/go-gin-api/internal/code" "github.com/xinliangnote/go-gin-api/internal/proposal" "github.com/xinliangnote/go-gin-api/pkg/browser" "github.com/xinliangnote/go-gin-api/pkg/color" "github.com/xinliangnote/go-gin-api/pkg/env" "github.com/xinliangnote/go-gin-api/pkg/errors" "github.com/xinliangnote/go-gin-api/pkg/trace" "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" cors "github.com/rs/cors/wrapper/gin" ginSwagger "github.com/swaggo/gin-swagger" "github.com/swaggo/gin-swagger/swaggerFiles" "go.uber.org/multierr" "go.uber.org/zap" "golang.org/x/time/rate" ) // see https://patorjk.com/software/taag/#p=testall&f=Graffiti&t=go-gin-api const _UI = ` ██████╗ ██████╗ ██████╗ ██╗███╗ ██╗ █████╗ ██████╗ ██╗ ██╔════╝ ██╔═══██╗ ██╔════╝ ██║████╗ ██║ ██╔══██╗██╔══██╗██║ ██║ ███╗██║ ██║█████╗██║ ███╗██║██╔██╗ ██║█████╗███████║██████╔╝██║ ██║ ██║██║ ██║╚════╝██║ ██║██║██║╚██╗██║╚════╝██╔══██║██╔═══╝ ██║ ╚██████╔╝╚██████╔╝ ╚██████╔╝██║██║ ╚████║ ██║ ██║██║ ██║ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ` type Option func(*option) type option struct { disablePProf bool disableSwagger bool disablePrometheus bool enableCors bool enableRate bool enableOpenBrowser string alertNotify proposal.NotifyHandler recordHandler proposal.RecordHandler } // WithDisablePProf 禁用 pprof func WithDisablePProf() Option { return func(opt *option) { opt.disablePProf = true } } // WithDisableSwagger 禁用 swagger func WithDisableSwagger() Option { return func(opt *option) { opt.disableSwagger = true } } // WithDisablePrometheus 禁用prometheus func WithDisablePrometheus() Option { return func(opt *option) { opt.disablePrometheus = true } } // WithAlertNotify 设置告警通知 func WithAlertNotify(notifyHandler proposal.NotifyHandler) Option { return func(opt *option) { opt.alertNotify = notifyHandler } } // WithRecordMetrics 设置记录接口指标 func WithRecordMetrics(recordHandler proposal.RecordHandler) Option { return func(opt *option) { opt.recordHandler = recordHandler } } // WithEnableOpenBrowser 启动后在浏览器中打开 uri func WithEnableOpenBrowser(uri string) Option { return func(opt *option) { opt.enableOpenBrowser = uri } } // WithEnableCors 设置支持跨域 func WithEnableCors() Option { return func(opt *option) { opt.enableCors = true } } // WithEnableRate 设置支持限流 func WithEnableRate() Option { return func(opt *option) { opt.enableRate = true } } // DisableTraceLog 禁止记录日志 func DisableTraceLog(ctx Context) { ctx.disableTrace() } // DisableRecordMetrics 禁止记录指标 func DisableRecordMetrics(ctx Context) { ctx.disableRecordMetrics() } // AliasForRecordMetrics 对请求路径起个别名,用于记录指标。 // 如:Get /user/:username 这样的路径,因为 username 会有非常多的情况,这样记录指标非常不友好。 func AliasForRecordMetrics(path string) HandlerFunc { return func(ctx Context) { ctx.setAlias(path) } } // WrapAuthHandler 用来处理 Auth 的入口 func WrapAuthHandler(handler func(Context) (sessionUserInfo proposal.SessionUserInfo, err BusinessError)) HandlerFunc { return func(ctx Context) { sessionUserInfo, err := handler(ctx) if err != nil { ctx.AbortWithError(err) return } ctx.setSessionUserInfo(sessionUserInfo) } } // RouterGroup 包装gin的RouterGroup type RouterGroup interface { Group(string, ...HandlerFunc) RouterGroup IRoutes } var _ IRoutes = (*router)(nil) // IRoutes 包装gin的IRoutes type IRoutes interface { Any(string, ...HandlerFunc) GET(string, ...HandlerFunc) POST(string, ...HandlerFunc) DELETE(string, ...HandlerFunc) PATCH(string, ...HandlerFunc) PUT(string, ...HandlerFunc) OPTIONS(string, ...HandlerFunc) HEAD(string, ...HandlerFunc) } type router struct { group *gin.RouterGroup } func (r *router) Group(relativePath string, handlers ...HandlerFunc) RouterGroup { group := r.group.Group(relativePath, wrapHandlers(handlers...)...) return &router{group: group} } func (r *router) Any(relativePath string, handlers ...HandlerFunc) { r.group.Any(relativePath, wrapHandlers(handlers...)...) } func (r *router) GET(relativePath string, handlers ...HandlerFunc) { r.group.GET(relativePath, wrapHandlers(handlers...)...) } func (r *router) POST(relativePath string, handlers ...HandlerFunc) { r.group.POST(relativePath, wrapHandlers(handlers...)...) } func (r *router) DELETE(relativePath string, handlers ...HandlerFunc) { r.group.DELETE(relativePath, wrapHandlers(handlers...)...) } func (r *router) PATCH(relativePath string, handlers ...HandlerFunc) { r.group.PATCH(relativePath, wrapHandlers(handlers...)...) } func (r *router) PUT(relativePath string, handlers ...HandlerFunc) { r.group.PUT(relativePath, wrapHandlers(handlers...)...) } func (r *router) OPTIONS(relativePath string, handlers ...HandlerFunc) { r.group.OPTIONS(relativePath, wrapHandlers(handlers...)...) } func (r *router) HEAD(relativePath string, handlers ...HandlerFunc) { r.group.HEAD(relativePath, wrapHandlers(handlers...)...) } func wrapHandlers(handlers ...HandlerFunc) []gin.HandlerFunc { funcs := make([]gin.HandlerFunc, len(handlers)) for i, handler := range handlers { handler := handler funcs[i] = func(c *gin.Context) { ctx := newContext(c) defer releaseContext(ctx) handler(ctx) } } return funcs } var _ Mux = (*mux)(nil) // Mux http mux type Mux interface { http.Handler Group(relativePath string, handlers ...HandlerFunc) RouterGroup } type mux struct { engine *gin.Engine } func (m *mux) ServeHTTP(w http.ResponseWriter, req *http.Request) { m.engine.ServeHTTP(w, req) } func (m *mux) Group(relativePath string, handlers ...HandlerFunc) RouterGroup { return &router{ group: m.engine.Group(relativePath, wrapHandlers(handlers...)...), } } func New(logger *zap.Logger, options ...Option) (Mux, error) { if logger == nil { return nil, errors.New("logger required") } gin.SetMode(gin.ReleaseMode) mux := &mux{ engine: gin.New(), } fmt.Println(color.Blue(_UI)) mux.engine.StaticFS("assets", http.FS(assets.Bootstrap)) mux.engine.SetHTMLTemplate(template.Must(template.New("").ParseFS(assets.Templates, "templates/**/*"))) // withoutTracePaths 这些请求,默认不记录日志 withoutTracePaths := map[string]bool{ "/metrics": true, "/debug/pprof/": true, "/debug/pprof/cmdline": true, "/debug/pprof/profile": true, "/debug/pprof/symbol": true, "/debug/pprof/trace": true, "/debug/pprof/allocs": true, "/debug/pprof/block": true, "/debug/pprof/goroutine": true, "/debug/pprof/heap": true, "/debug/pprof/mutex": true, "/debug/pprof/threadcreate": true, "/favicon.ico": true, "/system/health": true, } opt := new(option) for _, f := range options { f(opt) } if !opt.disablePProf { if !env.Active().IsPro() { pprof.Register(mux.engine) // register pprof to gin } } if !opt.disableSwagger { if !env.Active().IsPro() { mux.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // register swagger } } if !opt.disablePrometheus { mux.engine.GET("/metrics", gin.WrapH(promhttp.Handler())) // register prometheus } if opt.enableCors { mux.engine.Use(cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{ http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, }, AllowedHeaders: []string{"*"}, AllowCredentials: true, OptionsPassthrough: true, })) } if opt.enableOpenBrowser != "" { _ = browser.Open(opt.enableOpenBrowser) } // recover两次,防止处理时发生panic,尤其是在OnPanicNotify中。 mux.engine.Use(func(ctx *gin.Context) { defer func() { if err := recover(); err != nil { logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", string(debug.Stack()))) } }() ctx.Next() }) mux.engine.Use(func(ctx *gin.Context) { if ctx.Writer.Status() == http.StatusNotFound { return } ts := time.Now() context := newContext(ctx) defer releaseContext(context) context.init() context.setLogger(logger) context.ableRecordMetrics() if !withoutTracePaths[ctx.Request.URL.Path] { if traceId := context.GetHeader(trace.Header); traceId != "" { context.setTrace(trace.New(traceId)) } else { context.setTrace(trace.New("")) } } defer func() { var ( response interface{} businessCode int businessCodeMsg string abortErr error traceId string graphResponse interface{} ) if ct := context.Trace(); ct != nil { context.SetHeader(trace.Header, ct.ID()) traceId = ct.ID() } // region 发生 Panic 异常发送告警提醒 if err := recover(); err != nil { stackInfo := string(debug.Stack()) logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", stackInfo)) context.AbortWithError(Error( http.StatusInternalServerError, code.ServerError, code.Text(code.ServerError)), ) if notifyHandler := opt.alertNotify; notifyHandler != nil { notifyHandler(&proposal.AlertMessage{ ProjectName: configs.ProjectName, Env: env.Active().Value(), TraceID: traceId, HOST: context.Host(), URI: context.URI(), Method: context.Method(), ErrorMessage: err, ErrorStack: stackInfo, Timestamp: time.Now(), }) } } // endregion // region 发生错误,进行返回 if ctx.IsAborted() { for i := range ctx.Errors { multierr.AppendInto(&abortErr, ctx.Errors[i]) } if err := context.abortError(); err != nil { // customer err // 判断是否需要发送告警通知 if err.IsAlert() { if notifyHandler := opt.alertNotify; notifyHandler != nil { notifyHandler(&proposal.AlertMessage{ ProjectName: configs.ProjectName, Env: env.Active().Value(), TraceID: traceId, HOST: context.Host(), URI: context.URI(), Method: context.Method(), ErrorMessage: err.Message(), ErrorStack: fmt.Sprintf("%+v", err.StackError()), Timestamp: time.Now(), }) } } multierr.AppendInto(&abortErr, err.StackError()) businessCode = err.BusinessCode() businessCodeMsg = err.Message() response = &code.Failure{ Code: businessCode, Message: businessCodeMsg, } ctx.JSON(err.HTTPCode(), response) } } // endregion // region 正确返回 response = context.getPayload() if response != nil { ctx.JSON(http.StatusOK, response) } // endregion // region 记录指标 if opt.recordHandler != nil && context.isRecordMetrics() { path := context.Path() if alias := context.Alias(); alias != "" { path = alias } opt.recordHandler(&proposal.MetricsMessage{ ProjectName: configs.ProjectName, Env: env.Active().Value(), TraceID: traceId, HOST: context.Host(), Path: path, Method: context.Method(), HTTPCode: ctx.Writer.Status(), BusinessCode: businessCode, CostSeconds: time.Since(ts).Seconds(), IsSuccess: !ctx.IsAborted() && (ctx.Writer.Status() == http.StatusOK), }) } // endregion // region 记录日志 var t *trace.Trace if x := context.Trace(); x != nil { t = x.(*trace.Trace) } else { return } decodedURL, _ := url.QueryUnescape(ctx.Request.URL.RequestURI()) // ctx.Request.Header,精简 Header 参数 traceHeader := map[string]string{ "Content-Type": ctx.GetHeader("Content-Type"), configs.HeaderLoginToken: ctx.GetHeader(configs.HeaderLoginToken), configs.HeaderSignToken: ctx.GetHeader(configs.HeaderSignToken), configs.HeaderSignTokenDate: ctx.GetHeader(configs.HeaderSignTokenDate), } t.WithRequest(&trace.Request{ TTL: "un-limit", Method: ctx.Request.Method, DecodedURL: decodedURL, Header: traceHeader, Body: string(context.RawData()), }) var responseBody interface{} if response != nil { responseBody = response } graphResponse = context.getGraphPayload() if graphResponse != nil { responseBody = graphResponse } t.WithResponse(&trace.Response{ Header: ctx.Writer.Header(), HttpCode: ctx.Writer.Status(), HttpCodeMsg: http.StatusText(ctx.Writer.Status()), BusinessCode: businessCode, BusinessCodeMsg: businessCodeMsg, Body: responseBody, CostSeconds: time.Since(ts).Seconds(), }) t.Success = !ctx.IsAborted() && (ctx.Writer.Status() == http.StatusOK) t.CostSeconds = time.Since(ts).Seconds() logger.Info("trace-log", zap.Any("method", ctx.Request.Method), zap.Any("path", decodedURL), zap.Any("http_code", ctx.Writer.Status()), zap.Any("business_code", businessCode), zap.Any("success", t.Success), zap.Any("cost_seconds", t.CostSeconds), zap.Any("trace_id", t.Identifier), zap.Any("trace_info", t), zap.Error(abortErr), ) // endregion }() ctx.Next() }) if opt.enableRate { limiter := rate.NewLimiter(rate.Every(time.Second*1), configs.MaxRequestsPerSecond) mux.engine.Use(func(ctx *gin.Context) { context := newContext(ctx) defer releaseContext(context) if !limiter.Allow() { context.AbortWithError(Error( http.StatusTooManyRequests, code.TooManyRequests, code.Text(code.TooManyRequests)), ) return } ctx.Next() }) } mux.engine.NoMethod(wrapHandlers(DisableTraceLog)...) mux.engine.NoRoute(wrapHandlers(DisableTraceLog)...) system := mux.Group("/system") { // 健康检查 system.GET("/health", func(ctx Context) { resp := &struct { Timestamp time.Time `json:"timestamp"` Environment string `json:"environment"` Host string `json:"host"` Status string `json:"status"` }{ Timestamp: time.Now(), Environment: env.Active().Value(), Host: ctx.Host(), Status: "ok", } ctx.Payload(resp) }) } return mux, nil }