gin框架实践连载七 | 日志组件

2,880 阅读4分钟

引言

1、安装第3方库

logrus完全兼容标准的log库,还支持文本、JSON 两种日志输出格式。很多知名的开源项目都使用了这个库

go get github.com/sirupsen/logrus

2、构建log工具

tool下新建log.go

package main

import (
  log "github.com/sirupsen/logrus"
)

func init() {
  log.SetLevel(logrus.TraceLevel)
  log.SetFormatter(&logrus.JSONFormatter{})

  log.Trace("trace msg")
  log.Debug("debug msg")
  log.Info("info msg")
  log.Warn("warn msg")
  log.Error("error msg")
  log.Fatal("fatal msg")
  log.Panic("panic msg")
}

新增日志的配置

app.ini新增

[log]
Level = trace
ReportCaller = true
Formatter = test
config/config.go新增

type Log struct {
	Level        string
	Formatter    string
	ReportCaller bool
}

LogSetting      = &Log{}

func loadLog() {
	sec, err := Cfg.GetSection("log")
	if err != nil {
		log.Fatalf("Fail to get section 'log': %v", err)
	}

	err = sec.MapTo(LogSetting)
	if err != nil {
		log.Fatalf("Cfg.MapTo LogSetting err: %v", err)
	}

}

优化log.go

package tool

import (
	"fmt"
	log "github.com/sirupsen/logrus"

	inLog "go-api/tool/internal/log"
	"go-api/config"
)

var (
	Formatter = map[string]log.Formatter{
		"json": &log.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"},
		"text": &log.TextFormatter{TimestampFormat: "2006-01-02 15:04:05"},
		"test": &inLog.TestFormatter{TimestampFormat: "2006-01-02 15:04:05"},
	}

	Log       = log.StandardLogger()
	ApiLog    = apiLog()
	MysqlLog  = mysqlLog()
	AccessLog = accessLog()
)

type F = log.Fields


func defaultFormatter() {
	SetFormatter(config.LogSetting.Formatter)
}

func defaultLevel() {
	SetLevel(config.LogSetting.Level)
}

func defaultReportCaller() {
	SetReportCaller(config.LogSetting.ReportCaller)
}

func SetReportCaller(b bool) {
	log.SetReportCaller(b)
}

func SetFormatter(formatter string) {
	if Formatter, ok := Formatter[formatter]; !ok {
		panic(fmt.Errorf("Log Formatter %s", "unkonm"))
	} else {
		log.SetFormatter(Formatter)
	}
}

func SetLevel(level string) {
	if level, err := log.ParseLevel(level); err != nil {
		panic(fmt.Errorf("Log Level %s", err))
	} else {
		log.SetLevel(level)
	}
}

func init() {
	defaultReportCaller()//是否显示文件名和行数
	defaultFormatter()//默认格式化方式
	defaultLevel()//默认支持Log等级
    log.AddHook(&inLog.TestHook{})//添加test 钩子
}

func apiLog() *log.Entry {
	return log.WithField("topic", "api")
}

func mysqlLog() *log.Entry {
	return log.WithField("topic", "mysql")
}

func accessLog() *log.Entry {
	return log.WithField("topic", "access")
}

目前logrusz支持2种格式 text和接送、我们可以自定义日志格式

日志格式实现需要logrus.Formatter接口

//github.com/sirupsen/logrus/formatter.go
type Formatter interface {
	Format(*Entry) ([]byte, error)
}

internal解释

在tool下新增internal/log/test_formatter.go

package log

import (
	log "github.com/sirupsen/logrus"
	"bytes"
	"fmt"
)

type TestFormatter struct {
	TimestampFormat string
}

func (t *TestFormatter) Format(entry *log.Entry) ([]byte, error) {
	var b *bytes.Buffer
	if entry.Buffer != nil {
		b = entry.Buffer
	} else {
		b = &bytes.Buffer{}
	}

	timestampFormat := t.TimestampFormat
	t.appendKeyValue(b, "time", entry.Time.Format(timestampFormat))
	t.appendKeyValue(b, "level", entry.Level.String())
	t.appendKeyValue(b, "msg", entry.Message)
	t.appendKeyValue(b, "Format", "test")

	b.WriteByte('\n')
	return b.Bytes(), nil
}

func (f *TestFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
	if b.Len() > 0 {
		b.WriteByte(' ')
	}
	b.WriteString(key)
	b.WriteByte('=')
	f.appendValue(b, value)
}

func (f *TestFormatter) appendValue(b *bytes.Buffer, value interface{}) {
	stringVal, ok := value.(string)
	if !ok {
		stringVal = fmt.Sprint(value)
	}

	b.WriteString(fmt.Sprintf("%q", stringVal))
}

单元测试日志格式

package tests

import (
	. "go-api/tool"
	"testing"
)

func TestLogFormattest(t *testing.T) {
	SetFormatter("test")
	Log.Info("info msg")
}

func TestLogFormattext(t *testing.T) {
	SetFormatter("text")
	Log.Info("info msg")
}

func TestLogFormatjson(t *testing.T) {
	SetFormatter("json")
	Log.Info("info msg")
}

每条日志输出前都会执行钩子的特定方法。所以,我们可以添加输出字段、根据级别将日志输出到不同的目的地,我们这里实现一个测试钩子将错误以上级别的同时输出到控制台和文件

钩子需要实现logrus.Hook接口

// github.com/sirupsen/logrus/hooks.go
type Hook interface {
  Levels() []Level //方法返回感兴趣的日志级别切片
  Fire(*Entry) error //具体方法
}

在tool新增 /internal/log/test_hook.go

package log

import (
	"os"

	log "github.com/sirupsen/logrus"
)

type TestHook struct {
}

func (t *TestHook) Levels() []log.Level {
	return []log.Level{
		log.ErrorLevel,
		log.FatalLevel,
		log.PanicLevel,
	}
}

func (t *TestHook) Fire(entry *log.Entry) error {
	file, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE, 0755)
	if err != nil {
		log.Fatalf("create file log.txt failed: %v", err)
	}

	entry.Data["Hook"] = "test"
	line, _ := entry.String()

	file.Write([]byte(line))
	return nil
}

钩子的应用场景

比如可以在生产环境将error级别的日志都存储到txt、可以发送邮件给开发者等等

3、使用日志组件自定义日志中间件

app/moddleware新增logger.go

package middleware

import (
	"bytes"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"

	. "go-api/tool"
)

type LogParams struct {
	Request *http.Request
	Start   time.Time
	// TimeStamp shows the time after the server returns a response.
	TimeStamp time.Time
	// StatusCode is HTTP response code.
	StatusCode int
	// Latency is how much time the server cost to process a certain request.
	Latency time.Duration
	// ClientIP equals Context's ClientIP method.
	ClientIP string
	// Method is the HTTP method given to the request.
	Method string
	// Path is a path the client requests.
	Path string
	// ErrorMessage is set if error has occurred in processing the request.
	ErrorMessage string
	// Response is HTTP response body
	Response string
	// request
	request string
}

type LogWriter struct {
	gin.ResponseWriter
	NewWirter *bytes.Buffer
}

// 为了实现双写
func (w LogWriter) Write(p []byte) (int, error) {
	if n, err := w.NewWirter.Write(p); err != nil {
		return n, nil
	}
	return w.ResponseWriter.Write(p)
}

type LogFormatter func(params LogParams) string

type LogFunc func(params LogParams, f LogFormatter)

var defaultLogFormatter = func(param LogParams) string {

	if param.Latency > time.Minute {
		// Truncate in a golang < 1.8 safe way
		param.Latency = param.Latency - param.Latency%time.Second
	}
	return fmt.Sprintf("%3d| %13v | %15s | %s | %v  | %s",
		//param.TimeStamp.Format("2006/01/02 - 15:04:05"),
		param.StatusCode,
		param.Latency,
		param.ClientIP,
		param.Method,
		param.Path,
		param.ErrorMessage,
	)
}

var defaultLog = func(params LogParams, f LogFormatter) {
	AccessLog.Info(f(params))
}

var apiLog = func(params LogParams, f LogFormatter) {

	ApiLog.WithFields(F{
		"start_time": params.Start.Format("2006/01/02 - 15:04:05"),
		"exec_time":  fmt.Sprintf("%13v", params.Latency),
		"http_code":  params.StatusCode,
		"ip":         params.ClientIP,
		"method":     params.Method,
		"url":        params.Path,
		"request":    params.request,
		"Response":   params.Response,
	}).Info(f(params))
}

func Logger() gin.HandlerFunc {
	return LoggerWithFormatter(defaultLogFormatter)
}

func ApiLogger() gin.HandlerFunc {
	f := func(param LogParams) string {

		return fmt.Sprintf("%s", param.ErrorMessage)
	}
	return LoggerWithFormatterLogFunc(f, apiLog)
}

func LoggerWithFormatterLogFunc(f LogFormatter, l LogFunc) gin.HandlerFunc {
	return LoggerWith(f, l)
}

func LoggerWithFormatter(f LogFormatter) gin.HandlerFunc {
	return LoggerWith(f, defaultLog)
}

func LoggerWith(f LogFormatter, l LogFunc) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Start timer
		logWirter := &LogWriter{ResponseWriter: c.Writer, NewWirter: bytes.NewBufferString("")}
		c.Writer = logWirter
		start := time.Now()
		path := c.Request.URL.String()
		// Process request
		c.Next()

		param := LogParams{
			Request: c.Request,
		}
		// Stop timer
		param.Start = start
		param.TimeStamp = time.Now()
		param.Latency = param.TimeStamp.Sub(start)
		param.ClientIP = c.ClientIP()
		param.Method = c.Request.Method
		param.StatusCode = c.Writer.Status()
		param.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String()
		param.Response = logWirter.NewWirter.String()
		param.request = c.Request.PostForm.Encode()

		param.Path = path
		l(param, f)
	}
}

4、使用日志组件自定义异常中间件

app/moddleware新增recovery.go


package middleware

import (
	"fmt"
	"runtime"
	"strings"

	"github.com/gin-gonic/gin"

	. "go-api/tool"
)

// print stack trace for debug
func trace(message string) string {
	var pcs [32]uintptr
	n := runtime.Callers(3, pcs[:]) // skip first 3 caller

	var str strings.Builder
	str.WriteString(message + "\nTraceback:")
	for _, pc := range pcs[1:n] {
		fn := runtime.FuncForPC(pc)
		file, line := fn.FileLine(pc)
		str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))
	}
	return str.String()
}

func Recovery() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				message := fmt.Sprintf("%s", err)
				AccessLog.Error(trace(message))
				c.JSONP(500, JSONRET{
					Error_code: 500,
					Msg:        "Internal Server Error",
					Data:       nil,
				})
			}
		}()
		c.Next()
	}
}

5、改造路由使用自定义中间件

func initGin() *gin.Engine {
	//设置gin模式
	gin.SetMode(config.RunMode)
	engine := gin.New()
	engine.Use(middleware.Logger(), middleware.Recovery())//使用自定义访问日志中间件和异常中间件
	return engine
}

apiv1 := r.Group("/api/v1", middleware.ApiLogger())//使用api日志中间件
	{
		//获取用户列表
		apiv1.GET("/users", controller.GetUsers)
		//获取指定用户
		apiv1.GET("/user/:id", controller.GetUser)
		//新增用户
		apiv1.POST("/users", controller.AddUser)
		//更新指定用户
		apiv1.PUT("/users/:id", controller.EditUser)
		//删除指定用户
		apiv1.DELETE("/users/:id", controller.DeleteUser)
	}

6、总结

日志保证不了项目的健壮性,但可以让修复和查找问题带来极大的便利,日志记得好,bug不愁找,后续我会详细出一篇日志库logrus详细使用攻略

7、系列文章