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

【Gin-API系列】Gin中间件之日志模块(四)的更多相关文章

  1. Gin框架系列03:换个姿势理解中间件

    什么是中间件 中间件,英译middleware,顾名思义,放在中间的物件,那么放在谁中间呢?本来,客户端可以直接请求到服务端接口. 现在,中间件横插一脚,它能在请求到达接口之前拦截请求,做一些特殊处理 ...

  2. Gin框架系列01:极速上手

    Gin是什么? Gin是Go语言编写的web框架,具备中间件.崩溃处理.JSON验证.内置渲染等多种功能. 准备工作 本系列演示所有代码都在Github中,感兴趣的同学可以自行查阅,欢迎大家一起完善. ...

  3. openresty开发系列36--openresty执行流程之6日志模块处理阶段

    openresty开发系列36--openresty执行流程之6日志模块处理阶段 一)header_filter_by_lua 语法:header_filter_by_lua <lua-scri ...

  4. gin中如何自定义中间件

    package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { // 新建一个没有 ...

  5. 【腾讯Bugly干货分享】微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ff5932cde42f1f03de29b1 本文来源: 微信客户端开发团队 ...

  6. Gin实战:Gin+Mysql简单的Restful风格的API(二)

    上一篇介绍了Gin+Mysql简单的Restful风格的API,但代码放在一个文件中,还不属于restful风格,接下来将进行进一步的封装. 目录结构 ☁ gin_restful2 tree . ├─ ...

  7. go系列(6)- beego日志模块的使用

    1.安装日志模块 切换到工作目录,下载日志模块 cd /data/work/go/ go get github.com/astaxie/beego/logs 2.导入日志模块 使用的时候,需要导入模块 ...

  8. Mysql 系列 | 日志模块

    了解了 SQL 执行的流程,知道每一条语句都经过连接器.查询存储.分析器.优化器.执行器最后到存储引擎的过程.查询语句是如此,更新语句也不例外. 不同的是,更新语句会修改表数据,这里就涉及到两个重要的 ...

  9. python基础系列教程——Python3.x标准模块库目录

    python基础系列教程——Python3.x标准模块库目录 文本 string:通用字符串操作 re:正则表达式操作 difflib:差异计算工具 textwrap:文本填充 unicodedata ...

随机推荐

  1. xmake从入门到精通12:通过自定义脚本实现更灵活地配置

    xmake是一个基于Lua的轻量级现代化c/c++的项目构建工具,主要特点是:语法简单易上手,提供更加可读的项目维护,实现跨平台行为一致的构建体验. 本文主要详细讲解下,如何通过添加自定义的脚本,在脚 ...

  2. 聊一聊Flutter的setState()

    Flutter 里面包含两种widget 一种可变的,一种不可变的: 在可变的widget中可以使用 setstate(){} 函数. 官方也给出了例子: _onClick(){ setState() ...

  3. vue -电子时钟

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. 详解Java的对象创建

    1. 前言 在<还不清楚怎样面向对象?>和<面向对象再探究>两篇文章中,都介绍了关于面向对象程序设计的概念和特点.其中也涉及到了许多代码,比如: Dog dog = new D ...

  5. Python三角函数

    Python三角函数: 函数: ''' math.sin(x) 返回的x弧度的正弦值. math.asin(x) 返回x的反正弦弧度值. math.cos(x) 返回x的弧度的余弦值. math.ac ...

  6. PHP unset() 函数

    unset() 函数用于销毁给定的变量.高佣联盟 www.cgewang.com PHP 版本要求: PHP 4, PHP 5, PHP 7 语法 void unset ( mixed $var [, ...

  7. CF R630 div2 1332 E Height All the Same

    LINK:Height All the Same 比赛的时候 被这道题给打自闭了 还有1个多小时的时候开始想 想了30min 无果 放弃治疗. 心态炸了 F不想看了 应该要把题目全看一遍的 下次不能这 ...

  8. mysql启动报错,The server quit without updating PID file

    环境 MacOS 10.12.2 mysql Ver 14.14 Distrib 5.7.16, for osx10.11 (x86_64) using EditLine wrapper (该部分可跳 ...

  9. 【oracle】-表连接类型:内连接,外连接...

    一.数据准备 1.emp表 delete from emp; insert into emp (EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO) ...

  10. Dropwizard+jersey+MDC实现日志跟踪以及接口响应时间统计

    一.实现接口响应时间统计 1.1添加全局请求过滤器并注册 import org.apache.log4j.MDC; import org.slf4j.Logger; import org.slf4j. ...