• 【Gin-API系列】Gin中间件之日志模块(四)


    日志是程序开发中必不可少的模块,同时也是日常运维定位故障的最重要环节之一。一般日志类的操作包括日志采集,日志查询,日志监控、日志统计等等。本文,我们将介绍日志模块在Gin中的使用。

    Golang如何打印日志

    • 日志打印需要满足几个条件
    1. 重定向到日志文件
    2. 区分日志级别,一般有DEBUG,INFO,WARNING,ERROR,CRITICAL
    3. 日志分割,按照日期分割或者按照大小分割
    • Golang中使用logrus打印日志
    var LevelMap = map[string]logrus.Level{
    	"DEBUG": logrus.DebugLevel,
    	"ERROR": logrus.ErrorLevel,
    	"WARN":  logrus.WarnLevel,
    	"INFO":  logrus.InfoLevel,
    }
    
    // 创建 @filePth: 如果路径不存在会创建 @fileName: 如果存在会被覆盖  @std: os.stdout/stderr 标准输出和错误输出
    func New(filePath string, fileName string, level string, std io.Writer, count uint) (*logrus.Logger, error) {
    	if _, err := os.Stat(filePath); os.IsNotExist(err) {
    		if err := os.MkdirAll(filePath, 755); err != nil {
    			return nil, err
    		}
    	}
    	fn := path.Join(filePath, fileName)
    
    	logger := logrus.New()
    	//timeFormatter := &logrus.TextFormatter{
    	//	FullTimestamp:   true,
    	//	TimestampFormat: "2006-01-02 15:04:05.999999999",
    	//}
    	logger.SetFormatter(&logrus.JSONFormatter{
    		TimestampFormat: "2006-01-02 15:04:05.999999999",
    	}) // 设置日志格式为json格式
    
    	if logLevel, ok := LevelMap[level]; !ok {
    		return nil, errors.New("log level not found")
    	} else {
    		logger.SetLevel(logLevel)
    	}
    
    	//logger.SetFormatter(timeFormatter)
    
    	/*  根据文件大小分割日志
    	// import "gopkg.in/natefinch/lumberjack.v2"
    	logger := &lumberjack.Logger{
    		// 日志输出文件路径
    		Filename:   "D:\test_go.log",
    		// 日志文件最大 size, 单位是 MB
    		MaxSize:    500, // megabytes
    		// 最大过期日志保留的个数
    		MaxBackups: 3,
    		// 保留过期文件的最大时间间隔,单位是天
    		MaxAge:     28,   //days
    		// 是否需要压缩滚动日志, 使用的 gzip 压缩
    		Compress:   true, // disabled by default
    	}
    	*/
    	if 0 == count {
    		count = 90 // 0的话则是默认保留90天
    	}
    	logFd, err := rotatelogs.New(
    		fn+".%Y-%m-%d",
    		// rotatelogs.WithLinkName(fn),
    		//rotatelogs.WithMaxAge(time.Duration(24*count)*time.Hour),
    		rotatelogs.WithRotationTime(time.Duration(24)*time.Hour),
    		rotatelogs.WithRotationCount(count),
    	)
    	if err != nil {
    		return nil, err
    	}
    	defer func() {
    		_ = logFd.Close() // don't need handle error
    	}()
    
    	if nil != std {
    		logger.SetOutput(io.MultiWriter(logFd, std)) // 设置日志输出
    	} else {
    		logger.SetOutput(logFd) // 设置日志输出
    	}
    	// logger.SetReportCaller(true)   // 测试环境可以开启,生产环境不能开,会增加很大开销
    	return logger, nil
    }
    

    Gin中间件介绍

    Gin中间件的是Gin处理Http请求的一个模块或步骤,也可以理解为Http拦截器。

    我们将Http请求拆分为四个步骤
    1、服务器接到客户端的Http请求
    2、服务器解析Http请求进入到路由转发系统
    3、服务器根据实际路由执行操作并得到结果
    4、服务器返回结果给客户端

    Gin中间件的执行包括2个部分(first和last),分布对应的就是在步骤1-2之间(first)和3-4之间(last)的操作。常见的Gin中间件包括日志、鉴权、链路跟踪、异常捕捉等等

    • 默认中间件
    router := gin.Default()
    

    查看源码可以看到

    // Default returns an Engine instance with the Logger and Recovery middleware already attached.
    func Default() *Engine {
    	debugPrintWARNINGDefault()
    	engine := New()
    	engine.Use(Logger(), Recovery())  // 包含 Logger、Recovery 中间件
    	return engine
    }
    
    • 自定义中间件方式1
    func Middleware1(c *gin.Context)  {
    	...  // do something first
    	c.Next()  // 继续执行后续的中间件
    	// c.Abort() 不再执行后面的中间件
    	...  // do something last
    }
    
    • 自定义中间件方式2
    func Middleware2()  gin.HandlerFunc {
        return func(c *gin.Context) {
            ...  // do something first
            c.Next()  // 继续执行后续的中间件
            // c.Abort() 不再执行后面的中间件
            ...  // do something last
    	}
    }
    
    • 全局使用中间件
    route := gin.Default()
    route.Use(Middleware1)
    route.Use(Middleware2())
    
    • 指定路由使用中间件
    route := gin.Default()
    route.Get("/test", Middleware1)
    route.POST("/test", Middleware2())
    
    • 多个中间件执行顺序

    Gin里面多个中间件的执行顺序是按照调用次序来执行的。
    无论在全局使用还是指定路由使用,Gin都支持多个中间件顺序执行

    Gin中间件之日志模块

    • 模块代码
    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)
    }
    
    var SnowWorker, _ = uuid.NewSnowWorker(100) // 随机生成一个uuid,100是节点的值(随便给一个)
    
    // 打印日志
    func Logger() gin.HandlerFunc {
    	accessLog, _ := mylog.New(
    		configure.GinConfigValue.AccessLog.Path, configure.GinConfigValue.AccessLog.Name,
    		configure.GinConfigValue.AccessLog.Level, nil, configure.GinConfigValue.AccessLog.Count)
    	detailLog, _ := mylog.New(
    		configure.GinConfigValue.DetailLog.Path, configure.GinConfigValue.DetailLog.Name,
    		configure.GinConfigValue.DetailLog.Level, nil, configure.GinConfigValue.DetailLog.Count)
    	return func(c *gin.Context) {
    		var buf bytes.Buffer
    		tee := io.TeeReader(c.Request.Body, &buf)
    		requestBody, _ := ioutil.ReadAll(tee)
    		c.Request.Body = ioutil.NopCloser(&buf)
    
    		user := c.Writer.Header().Get("X-Request-User")
    		bodyLogWriter := &BodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
    		c.Writer = bodyLogWriter
    
    		start := time.Now()
    
    		c.Next()
    
    		responseBody := bodyLogWriter.body.Bytes()
    		response := route_response.Response{}
    		if len(responseBody) > 0 {
    			_ = json.Unmarshal(responseBody, &response)
    		}
    		end := time.Now()
    		responseTime := float64(end.Sub(start).Nanoseconds()) / 1000000.0 // 纳秒转毫秒才能保留小数
    		logField := map[string]interface{}{
    			"user":            user,
    			"uri":             c.Request.URL.Path,
    			"start_timestamp": start.Format("2006-01-02 15:04:05"),
    			"end_timestamp":   end.Format("2006-01-02 15:04:05"),
    			"server_name":     c.Request.Host,
    			"server_addr": fmt.Sprintf("%s:%d", configure.GinConfigValue.ApiServer.Host,
    				configure.GinConfigValue.ApiServer.Port), // 无法动态读取
    			"remote_addr":    c.ClientIP(),
    			"proto":          c.Request.Proto,
    			"referer":        c.Request.Referer(),
    			"request_method": c.Request.Method,
    			"response_time":  fmt.Sprintf("%.3f", responseTime), // 毫秒
    			"content_type":   c.Request.Header.Get("Content-Type"),
    			"status":         c.Writer.Status(),
    			"user_agent":     c.Request.UserAgent(),
    			"trace_id":       SnowWorker.GetId(),
    		}
    		accessLog.WithFields(logField).Info("Request Finished")
    		detailLog.WithFields(logField).Info(c.Request.URL)
    		detailLog.WithFields(logField).Info(string(requestBody)) // 不能打印GET请求参数
    		if response.Code != configure.RequestSuccess {
    			detailLog.WithFields(logField).Errorf("code=%d, message=%s", response.Code, response.Message)
    		} else {
    			detailLog.WithFields(logField).Infof("total=%d, page_size=%d, page=%d, size=%d",
    				response.Data.Total, response.Data.PageSize, response.Data.Page, response.Data.Size)
    		}
    	}
    }
    
    • 启用全局日志中间件
    route := gin.New()  // 不用默认的日志中间件
    route.Use(route_middleware.Logger())
    

    异步打印日志

    由于我们的日志中间件使用的是全局中间件,在高并发处理请求时日志落地会导致大量的IO操作,这些操作会拖慢整个服务器,所以我们需要使用异步打印日志

    • 异步函数
    var logChannel = make(chan map[string]interface{}, 300)
    
    func logHandlerFunc() {
    	accessLog, _ := mylog.New(
    		configure.GinConfigValue.AccessLog.Path, configure.GinConfigValue.AccessLog.Name,
    		configure.GinConfigValue.AccessLog.Level, nil, configure.GinConfigValue.AccessLog.Count)
    	detailLog, _ := mylog.New(
    		configure.GinConfigValue.DetailLog.Path, configure.GinConfigValue.DetailLog.Name,
    		configure.GinConfigValue.DetailLog.Level, nil, configure.GinConfigValue.DetailLog.Count)
    	for logField := range logChannel {
    		var (
    			msgStr   string
    			levelStr string
    			detailStr string
    		)
    		if msg, ok := logField["msg"]; ok {
    			msgStr = msg.(string)
    			delete(logField, "msg")
    		}
    		if level, ok := logField["level"]; ok {
    			levelStr = level.(string)
    			delete(logField, "level")
    		}
    		if detail, ok := logField["detail"]; ok {
    			detailStr = detail.(string)
    			delete(logField, "detail")
    		}
    		accessLog.WithFields(logField).Info("Request Finished")
    		if "info" == levelStr {
    			detailLog.WithFields(logField).Info(detailStr)
    			detailLog.WithFields(logField).Info(msgStr)
    		} else {
    			detailLog.WithFields(logField).Error(detailStr)
    			detailLog.WithFields(logField).Error(msgStr)
    		}
    	}
    }
    
    • 调用方法
    go logHandlerFunc()
    ... // 省略
    logChannel <- logField
    
    

    至此,我们完成了Gin中间件的介绍和日志模块的设计,接下来,我们将使用更多的中间件,完善我们的Api服务。

    Github 代码

    请访问 Gin-IPs 或者搜索 Gin-IPs

  • 相关阅读:
    W3C代码标准规范
    我们为什么要遵循W3C标准规范
    WEB标准:标准定义、好处、名词解释、常用术语、命名习惯、浏览器兼容、代码书写规范
    ThinkPHP框架下,给jq动态添加的标签添加点击事件移除标签
    ThinkPHP框架下,jq实现在div中添加标签并且div的大小会随之变化
    Windows下spark1.6.0本地环境搭建
    Mysql中使用SQL计算两个日期时间差值
    jquery正则表达式验证:手机号码
    jquery正则表达式验证:验证邮箱格式是否正确
    jquery正则表达式验证:整数12位,小数钱12位小数点后2位
  • 原文地址:https://www.cnblogs.com/lxmhhy/p/13518211.html
Copyright © 2020-2023  润新知