123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- 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
- }
|