引言
- 真实项目的排错和查找很大部分依赖于日志
- golang 自带的log标准库对于生产项目不够细致
- 所以需要我们引入 github.com/sirupsen/lo…
- github地址
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)
}
在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详细使用攻略