由网络副手--寻路人于2022.05.18 00:19:00发布在Brave随笔,Go语言,企业技术支持 四、整合go logrus 记录request/response 日志 阅读2020 评论0 喜欢0 ## 日志组件Logurs 整合gin 框架 背景: 之前的业务并没有日志跟踪记录,想解决日志追踪问题,实现两个便捷 1. 让日志落地存储 2. 让前后访问日志能串联起来 ## 安装logurs 1. 安装执行命令 go get github.com/sirupsen/logrus ## 代码整合 ``` |——config |——config.go |——middleware |——logger |——logger.go |——recover |——recover.go main.go |——api/router.go ``` ### 配置文件 config/config.go ``` func ConfInstance() *Config { confOnce.Do(func() { Conf.initLogRus() } } func (c *Config) initLogRus() { logger.Setup() } ``` ### 日志组件代码 ``` package logger import ( "bytes" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "io/ioutil" "musenetwork.org/muse-utils" "os" "path" "strconv" "strings" "time" ) var AppLog *logrus.Logger var WebLog *logrus.Logger func Setup() { initAppLog() initWebLog() } /** 初始化AppLog */ func initAppLog() { logFileName := "project-app.log" AppLog = initLog(logFileName) } /** 初始化WebLog */ func initWebLog() { logFileName := "project-web.log" WebLog = initLog(logFileName) } /** 初始化日志句柄 */ func initLog(logFileName string) *logrus.Logger { logPath := utils.GetEnvDefault("logs_path", "logs/") utils.CheckDirAndCreate(logPath) // 日志文件 logName := path.Join(logPath, logFileName) var f *os.File var err error //判断日志文件是否存在,不存在则创建,否则就直接打开 if _, err := os.Stat(logName); os.IsNotExist(err) { f, err = os.Create(logName) } else { f, err = os.OpenFile(logName, os.O_APPEND|os.O_WRONLY, os.ModeAppend) } if err != nil { fmt.Println("open log file failed") } log := logrus.New() log.Formatter = &logrus.JSONFormatter{ TimestampFormat: "2006-01-02 15:04:05", } log.Out = f //// 设置日志级别 log.SetLevel(logrus.InfoLevel) // 设置 rotatelogs /* logWriter, _ := rotatelogs.New( // 分割后的文件名称 logName+".%Y%m%d.log", // 生成软链,指向最新日志文件 rotatelogs.WithLinkName(logName), // 设置最大保存时间(7天) rotatelogs.WithMaxAge(7*24*time.Hour), // 设置日志切割时间间隔(1天) rotatelogs.WithRotationTime(24*time.Hour), ) writeMap := lfshook.WriterMap{ logrus.InfoLevel: logWriter, logrus.FatalLevel: logWriter, logrus.DebugLevel: logWriter, logrus.WarnLevel: logWriter, logrus.ErrorLevel: logWriter, logrus.PanicLevel: logWriter, } log.SetReportCaller(true) lfHook := lfshook.NewHook(writeMap, &logrus.JSONFormatter{ //格式精确到毫秒 TimestampFormat: "2006-01-02 15:04:05.000", }) // 新增钩子 log.AddHook(lfHook) */ return log } type Result struct { Code int `json:"code"` Error string `json:"error"` Data interface{} `json:"data"` } type bodyLogWriter struct { gin.ResponseWriter body *bytes.Buffer } func (w bodyLogWriter) Write(b []byte) (int, error) { w.body.Write(b) return w.ResponseWriter.Write(b) } func (w bodyLogWriter) WriteString(s string) (int, error) { w.body.WriteString(s) return w.ResponseWriter.WriteString(s) } /** Gin中间件函数,记录请求日志 */ func LoggerToFile() gin.HandlerFunc { return func(c *gin.Context) { if c.Request.Method == "OPTIONS" { c.Next() return } traceId := fmt.Sprintf("%s-%v", utils.RandStr(20), time.Now().UnixMilli()) WebLog.AddHook(NewTraceIdHook(traceId)) AppLog.AddHook(NewTraceIdHook(traceId)) bodyLogWriter := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} c.Writer = bodyLogWriter // 开始时间 startTime := time.Now().UnixNano() postJsonData := "" if c.Request.Method == "POST" && c.Request.Header.Get("Content-Type") != "" && strings.ToLower(c.Request.Header.Get("Content-Type")) == "application/json" { body, err := c.GetRawData() if err != nil { fmt.Println(err.Error()) } c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) postJsonData = string(body) } // 处理请求 c.Next() responseBody := bodyLogWriter.body.String() var responseCode string var responseMsg string //var responseData interface{} if responseBody != "" { res := Result{} err := json.Unmarshal([]byte(responseBody), &res) if err == nil { responseCode = strconv.Itoa(res.Code) responseMsg = res.Error //responseData = res.Data } } // 结束时间 endTime := time.Now().UnixNano() if c.Request.Method == "POST" && postJsonData == "" { c.Request.ParseForm() postJsonData = c.Request.PostForm.Encode() if postJsonData == "" { postJsonData = "got nothing form data, pls check params type and contentType" } } costTime, _ := strconv.ParseFloat(fmt.Sprintf("%.4f", float64(endTime-startTime)/1e6), 64) // 日志格式 WebLog.WithFields(logrus.Fields{ "request_method": c.Request.Method, "request_uri": c.Request.RequestURI, "request_proto": c.Request.Proto, "request_useragent": c.Request.UserAgent(), "request_referer": c.Request.Referer(), "request_post_data": postJsonData, "request_auth_data": c.GetInt("uid"), "request_client_ip": c.ClientIP(), "response_status_code": c.Writer.Status(), "response_code": responseCode, "response_msg": responseMsg, //"response_data": responseData, "cost_time": costTime, }).Info("access") } } type TraceIdHook struct { TraceId string } func NewTraceIdHook(traceId string) logrus.Hook { hook := TraceIdHook{ TraceId: traceId, } return &hook } func (hook *TraceIdHook) Fire(entry *logrus.Entry) error { entry.Data["Trace-Id"] = hook.TraceId return nil } func (hook *TraceIdHook) Levels() []logrus.Level { return logrus.AllLevels } ``` ### utils 文件 ``` package utils import ( "fmt" "math/rand" "net" "os" "strconv" "strings" "time" ) const ( FormatTimeScore = "2006-01-02 15:04:05" FormatTimeScoreNoStr = "20060102150405" ) func GetEnvDefault(key, defVal string) string { val, ex := os.LookupEnv(key) if !ex { return defVal } return val } // CheckDirAndCreate 判断文件夹是否存在并创建 func CheckDirAndCreate(path string) error { if _, err := os.Stat(path); err == nil { //存在 return nil } else { //不存在 err := os.MkdirAll(path, 0751) if err != nil { return err } return nil } } func Find(slice []string, val string) bool { for _, item := range slice { if item == val { return true } } return false } // LocalIP 获取机器的IP func LocalIP() string { info, _ := net.InterfaceAddrs() for _, addr := range info { ipNet, ok := addr.(*net.IPNet) if !ok { continue } if !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil { return ipNet.IP.String() } } return "" } func SplitParam(parameterStr string) map[string]interface{} { parameterDict := make(map[string]interface{}) //参数数组 parameterArray := strings.Split(parameterStr, "&") //生成参数字典 for i := 0; i < len(parameterArray); i++ { str := parameterArray[i] if len(str) > 0 { tem := strings.Split(str, "=") if len(tem) > 0 && len(tem) == 1 { parameterDict[tem[0]] = "" } else if len(tem) > 1 { parameterDict[tem[0]] = tem[1] } } } return parameterDict } // 手机号中间4位替换为*号 func FormatMobileStar(mobile string) string { if len(mobile) <= 10 { return mobile } return mobile[:3] + "****" + mobile[7:] } type PrizeInfo struct { PrizeId int // id Weight int // 概率 } func GetRandom(list []PrizeInfo) PrizeInfo { if len(list) == 0 { return PrizeInfo{} } total := 0 for _, v := range list { total += v.Weight } rand.Seed(time.Now().UnixNano()) index := rand.Intn(total) //fmt.Println("total:"+strconv.Itoa(total)) //fmt.Println("index:"+strconv.Itoa(index)) current := 0 for _, v := range list { current += v.Weight if index < current { return v } } return PrizeInfo{} } func FormatMoney64(money int) float64 { if money%100 == 0 { return float64(money) / float64(100) } else { //保留两位小数 tmpMoney := float64(money) / float64(100) value, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", tmpMoney), 64) return value } } //float64 取2位小数 func Format642f(money float64) float64 { value, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", money), 64) return value } ``` ### 在业务路由上整合组件方法--main.go ``` func main() { r := api.InitRouter() //_ = r.Run(":9000") s := &http.Server{ Addr: ":9000", Handler: r, MaxHeaderBytes: 1 << 20, } go func() { if err := s.ListenAndServe(); err != nil { log.Printf("Listen: %s\n", err) } }() quit := make(chan os.Signal) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <- quit ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second) defer cancel() if err := s.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } } ``` ### 在业务路由上整合组件方法--api/router.go ``` package api import ( ginSwagger "github.com/swaggo/gin-swagger" "github.com/swaggo/gin-swagger/swaggerFiles" "log" "muse-api/middleware/logger" "muse-api/middleware/recover" "net/http" "net/url" "os" "strconv" "strings" "muse-api/config" "muse-api/controller" "muse-api/service" "musenetwork.org/muse-utils" "github.com/gin-gonic/gin" ) var router = gin.New() func InitRouter() *gin.Engine { //router := gin.Default() 此方法内有recover,可能不会输出异常,慎用! router.Use(logger.LoggerToFile(), recover.Recover()) router.Use(gin.Logger()) if gin.Mode() != gin.ReleaseMode { router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) } router.Use(Cors()) registerRoute() return router } func both() gin.HandlerFunc { return func(c *gin.Context) { v := c.Request.Header.Get("Authorization") if s := strings.Split(v, " "); len(s) == 2 && s[0] == "Bearer" { token := s[1] j := utils.NewJWT(config.Conf.Yaml.Jwt.JwtSecret) claims, err := j.ParseToken(token) if err == nil && claims != nil && claims.UID > 0 { uid := claims.UID c.Set("uid", uid) } } c.Next() } } func jwtAuth() gin.HandlerFunc { return func(c *gin.Context) { if (os.Getenv("APP_ENV") == "dev" || os.Getenv("APP_ENV") == "test") && c.Request.Header.Get("dev_uid") != "" { uid, _ := strconv.Atoi(c.Request.Header.Get("dev_uid")) c.Set("uid", uid) c.Next() return } v := c.Request.Header.Get("Authorization") if s := strings.Split(v, " "); len(s) == 2 && s[0] == "Bearer" { token := s[1] auth(token, c) } else { c.JSON(http.StatusUnauthorized, controller.WriteResponse(controller.RequestTokenFail, nil)) c.Abort() } } } func mustBindPhone() gin.HandlerFunc { return func(c *gin.Context) { //判断是否绑定手机号 uid := c.GetInt("uid") b, err := service.IsBindPhone(uid) if err != nil { c.JSON(http.StatusBadRequest, controller.WriteResponse(controller.ServerError, err)) c.Abort() return } if !b { c.JSON(http.StatusBadRequest, controller.WriteResponse(controller.NoBindPhone, nil)) c.Abort() return } } } func Cors() gin.HandlerFunc { return func(c *gin.Context) { method := c.Request.Method //请求方法 c.Writer.Header().Set("Access-Control-Allow-Origin", "*") //https://h5.pengyin.vip c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求 // header的类型 c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma") // 允许跨域设置 可以返回其他子段 c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析 //c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒 c.Header("Access-Control-Allow-Credentials", "true") // 跨域请求是否需要带cookie信息 默认设置为true c.Set("content-type", "application/json") // 设置返回格式是json var err error c.Request.RequestURI, err = url.QueryUnescape(c.Request.RequestURI) if err != nil { log.Println(err) } //放行所有OPTIONS方法 if method == "OPTIONS" { c.JSON(http.StatusOK, "Options Request!") } c.Next() } } func auth(token string, c *gin.Context) { j := utils.NewJWT(config.Conf.Yaml.Jwt.JwtSecret) claims, err := j.ParseToken(token) if err != nil { if strings.Contains(err.Error(), "token is expired") { //用旧token换新token;超过刷新token有效期,要求前端重新登录 c.JSON(http.StatusUnauthorized, controller.WriteResponse(controller.TokenIsExpired, nil)) c.Abort() } else { c.JSON(http.StatusInternalServerError, controller.WriteResponse(http.StatusInternalServerError, err)) c.Abort() } } else { uid := claims.UID c.Set("uid", uid) c.Next() } } ``` ### recover/recover.go ``` package recover import ( "fmt" "github.com/gin-gonic/gin" "muse-api/common/alarm" "net/http" ) type Result struct { Code int `json:"code"` Data interface{} `json:"data"` Error string `json:"error"` } func Recover() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if r := recover(); r != nil { alarm.Panic(fmt.Sprintf("%s", r)) c.JSON(http.StatusOK, &Result{ Code: 10004, Data: nil, Error: "服务业务异常请联系技术伙伴", }) return } }() c.Next() } } ``` ### 日志效果 ``` ├── api_log1.out ├── api_log2.out ├── config │ └── api.pengyin.vip_beta@@beta@@beta_id ├── nacos-sdk.log ├── project-app.log #系统错误日志 └── project-web.log #系统请求日志 {"Trace-Id":"beteO4GhZaHQCyXWWujP-1659439216070","cost_time":"175.3026 ms","level":"info","msg":"access","request_auth_data":34744,"request_client_ip":"124.127.114.130","request_method":"POST","request_post_data":"got nothing form data, pls check params type and contentType","request_proto":"HTTP/1.0","request_referer":"https://test-h5.pengyin.vip/","request_uri":"/v2/user/status","request_useragent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CriOS/47.0.2526.70 Safari/601.1.46 bsl/1.0","response_code":"0","response_msg":"成功","response_status_code":200,"time":"2022-08-02 19:20:16"} {"Trace-Id":"6qxAx5SiphcVCV89ITbd-1659431319273","level":"error","msg":"查询市集买入超时订单失败Error 1146: Table 'pengyin_new_beta.py_sale_order' doesn't exist","time":"2022-08-02 17:26:59"} ``` 赞 0 分享 赏 您可以选择一种方式赞助本站 支付宝扫码赞助 BraveDu 署名: 网络副手~寻路人