gin内置日志组件的使用
前言
在之前我们要使用Gin框架定义路由的时候我们一般会使用Default
方法来实现,我们来看一下他的实现:
func Default(opts ...OptionFunc) *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine.With(opts...) }
我们可以看到它注册了两个中间件Logger()
和Recovery()
,而Logger
就是我们今天的主角:gin框架自带的日志组件。
输出日志到文件中
package main import ( "fmt" "github.com/gin-gonic/gin" "io" "os" ) func main() { file, err := os.Create("ginlog") if err != nil { fmt.Println("Create file error! err:", err) } gin.DefaultWriter = io.MultiWriter(file) r := gin.Default() r.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "Hello World!", }) }) r.Run() }
运行上面代码我们会发现,控制台不再会有相关日志的输出,而是打印到了ginlog
文件中:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / --> main.main.func1 (3 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080
当然我们也可以选择既在控制台输出也在文件内输出:
package main import ( "fmt" "github.com/gin-gonic/gin" "io" "os" ) func main() { file, err := os.Create("ginlog") if err != nil { fmt.Println("Create file error! err:", err) } gin.DefaultWriter = io.MultiWriter(file, os.Stdout) r := gin.Default() r.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "Hello World!", }) }) r.Run() }
我们可以看到无论是日志文件ginlog
和控制台,都实现了对日志的打印
定义日志中的路由格式
当我们运行Gin框架的时候,它会自动打印当前所有被定义的路由,比如下面这样的格式:
[GIN-debug] GET / --> main.main.func1 (3 handlers)
而在Gin框架中它允许我们去自己定义路由的输出格式,我们可以自己去定义我们的路由格式:
func _Router_print_init() { gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { fmt.Printf("[三玖]: %v %v %v %v \n", httpMethod, absolutePath, handlerName, nuHandlers) } }
输出的路由格式是这样的:
[三玖]: GET / main.main.func1 3
生产模式与开发模式
在我们程序其实是有两种模式的:
debug
:开发模式release
:生产模式
如果我们希望控制台不在显示日志,可以将模式切换到release
模式:
gin.SetMode(gin.ReleaseMode) r := gin.Default()
我们可以看到控制台已不再输出日志信息了。
第三方包logrus日志包的使用
logrus包的安装与基本使用
logrus包的安装
logrus的安装很简单,只需要终端输入以下命令即可:
go get github.com/sirupsen/logrus
logrus包的基本使用
logrus常用方法:
logrus.Debug("debug") logrus.Info("info") logrus.Warn("warn") logrus.Error("error") logrus.Println("println")
当我们运行该代码的时候会发现打印结果只有四行:
这主要是因为logrus
默认的打印等级是info
,在这个等级之下的不会打印,在我们生产环境下一般会要求不打印Warn以下的日志,我们可以对打印等级进行调整:
logrus.SetLevel(logrus.WarnLevel)
再次运行上面的代码,运行结果就会有所不同:
我们还可以查看当前的打印等级:
fmt.Println(logrus.GetLevel())
设置特定字段
如果我们希望某条日志记录的打印中添加某一条特定的字段,我们可以使用WithField
方法:
log1 := logrus.WithField("key1", "value1") log1.Info("hello world")
通常,在一个应用中、或者应用的一部分中,都有一些固定的Field。比如我们在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip为了避免每次记录日志都要使用log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可,这里我写了一个demo,仅做参考:
package main import ( "github.com/sirupsen/logrus" ) type DefaultLogger struct { *logrus.Entry defaultFields logrus.Fields } func NewDefaultLogger() *DefaultLogger { logger := logrus.New() entry := logrus.NewEntry(logger) return &DefaultLogger{ Entry: entry, defaultFields: logrus.Fields{}, } } func (l *DefaultLogger) WithFields(fields logrus.Fields) *logrus.Entry { allFields := make(logrus.Fields, len(fields)) for k, v := range fields { allFields[k] = v } return l.Entry.WithFields(allFields) } func (l *DefaultLogger) WithDefaultField() { l.Entry = l.Entry.WithFields(l.defaultFields) } func (l *DefaultLogger) Info(msg string) { l.WithDefaultField() l.Entry.Info(msg) } func (l *DefaultLogger) AddDefaultField(key string, value interface{}) { l.defaultFields[key] = value } func main() { defaultLogger := NewDefaultLogger() defaultLogger.AddDefaultField("request_id", "123") defaultLogger.AddDefaultField("user_ip", "127.0.0.1") // 使用默认字段记录日志 defaultLogger.Info("This is a log message with default fields") // 添加额外字段记录日志 defaultLogger.WithFields(logrus.Fields{ "additional_field": "abc", }).Info("This is a log message with additional field") }
输出结果为:
设置显示样式
虽然日志的打印默认是txt
格式的,但是我们也可以将格式修改为json
格式的:
logrus.SetFormatter(&logrus.TextFormatter{}) • 1
将日志输入到文件
package main import ( "github.com/sirupsen/logrus" "os" ) func main() { file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY, 0666) if err != nil { panic(err) } logrus.SetOutput(file) logrus.Error("error") }
我们还可以让控制台和日志文件一起输出:
package main import ( "github.com/sirupsen/logrus" "golang.org/x/sys/windows" "io" "os" ) func main() { file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY|windows.O_APPEND, 0666) if err != nil { panic(err) } writers := []io.Writer{ file, os.Stdout, } lod := io.MultiWriter(writers...) logrus.SetOutput(lod) logrus.Error("error") logrus.Info("info") }
显示行号
logrus.SetReportCaller(true)
logus的Hook机制
在使用logrus这一第三方包的时候,我们可以基于Hook
机制来为logrus
添加一些拓展功能。
首先我们先定义Hook结构体:
type Hook struct { Levels() []logrus.Level // 返回日志级别 Fire(entry *logrus.Entry) error // 日志处理 }
我们Hook
结构体中一般会有两个成员:
Levels
:Hook机制起作用的日志级别Fire
:对应的日志处理方式
这里我们举一个例子,如果我们希望将所有Error
级别的日志单独拎出来,我们可以基于Hook
机制来实现:
package main import ( "fmt" "github.com/sirupsen/logrus" "os" ) type Hook struct { Writer *os.File } func (MyHook *Hook) Fire(entry *logrus.Entry) error { line, err := entry.String() if err != nil { fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) } MyHook.Writer.Write([]byte(line)) return nil } func (MyHook *Hook) Levels() []logrus.Level { return []logrus.Level{ logrus.ErrorLevel, } } func main() { logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true, TimestampFormat: "2006-01-02 15:04:05", FullTimestamp: true}) logrus.SetReportCaller(true) file, _ := os.OpenFile("./error.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) hook := &Hook{Writer: file} logrus.AddHook(hook) logrus.Error("error") }
日志分割
按时间分割
Write
写法
package main import ( "fmt" "github.com/sirupsen/logrus" "io" "os" "path/filepath" "strings" "time" ) type LogFormatter struct{} // Format 格式详情 func (s *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { timestamp := time.Now().Local().Format("2006-01-02 15:04:05") var file string var len int if entry.Caller != nil { file = filepath.Base(entry.Caller.File) len = entry.Caller.Line } //fmt.Println(entry.Data) msg := fmt.Sprintf("[%s] %s [%s:%d] %s\n", strings.ToUpper(entry.Level.String()), timestamp, file, len, entry.Message) return []byte(msg), nil } type LogWriter struct { Writer *os.File logPath string fileDate string //判断是否需要切换日志文件 fileName string //日志文件名 } func (writer *LogWriter) Write(p []byte) (n int, err error) { if writer == nil { logrus.Error("writer is nil") return 0, nil } if writer.Writer == nil { logrus.Error("writer.Writer is nil") return 0, nil } timer := time.Now().Format("2006-01-02 04:12") //需要切换日志文件 if writer.fileDate != timer { writer.fileDate = timer writer.Writer.Close() err = os.MkdirAll(writer.logPath, os.ModePerm) if err != nil { logrus.Error(err) return 0, nil } filename := fmt.Sprintf("%s/%s.log", writer.logPath, writer.fileDate) writer.Writer, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) if err != nil { logrus.Error(err) return 0, nil } } return writer.Writer.Write(p) } func Initing(logPath string, fileName string) { fileDate := time.Now().Format("20060102") filepath := fmt.Sprintf("%s/%s", logPath, fileDate) err := os.MkdirAll(filepath, os.ModePerm) if err != nil { logrus.Error(err) return } filename := fmt.Sprintf("%s/%s.log", filepath, fileName) writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) if err != nil { logrus.Error(err) return } Logwriter := LogWriter{logPath: logPath, fileDate: fileDate, fileName: fileName, Writer: writer} logrus.SetOutput(os.Stdout) writers := []io.Writer{ Logwriter.Writer, os.Stdout, } multiWriter := io.MultiWriter(writers...) logrus.SetOutput(multiWriter) logrus.SetReportCaller(true) logrus.SetFormatter(new(LogFormatter)) } func main() { Initing("./", "fengxu") logrus.Warn("fengxu") logrus.Error("fengxu") logrus.Info("fengxu") }
- Hook写法
package main import ( "fmt" "github.com/sirupsen/logrus" "os" "time" ) type Hook struct { writer *os.File logPath string fileName string fileDate string } func (MyHook *Hook) Levels() []logrus.Level { return logrus.AllLevels } func (MyHook *Hook) Fire(entry *logrus.Entry) error { timer := time.Now().Format("2006-01-02") line, _ := entry.String() //需要切换日志文件 if MyHook.fileDate != timer { MyHook.fileDate = timer MyHook.writer.Close() filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate) err := os.MkdirAll(filepath, os.ModePerm) if err != nil { logrus.Error(err) return err } filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName) MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) } MyHook.writer.Write([]byte(line)) return nil } func InitFile(logPath string, fileName string) { timer := time.Now().Format("2006-01-02") filepath := fmt.Sprintf("%s/%s", logPath, timer) err := os.MkdirAll(filepath, os.ModePerm) if err != nil { logrus.Error(err) return } filename := fmt.Sprintf("%s/%s.log", filepath, fileName) writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) if err != nil { logrus.Error(err) return } logrus.AddHook(&Hook{ writer: writer, logPath: logPath, fileName: fileName, fileDate: timer, }) } func main() { InitFile("./log", "fengxu") logrus.Error("test") }
按日志等级分割
package main import ( "fmt" "github.com/sirupsen/logrus" "os" ) const ( alllog = "all" errorlog = "error" warnlog = "warn" ) type Hook struct { allLevel *os.File errorLevel *os.File warnLevel *os.File } func (MyHook *Hook) Levels() []logrus.Level { return logrus.AllLevels } func (MyHook *Hook) Fire(entry *logrus.Entry) error { line, _ := entry.String() switch entry.Level { case logrus.ErrorLevel: MyHook.errorLevel.Write([]byte(line)) case logrus.WarnLevel: MyHook.warnLevel.Write([]byte(line)) } MyHook.allLevel.Write([]byte(line)) return nil } func InitLevel(logPath string) { err := os.MkdirAll(logPath, os.ModePerm) if err != nil { logrus.Error("创建目录失败") return } allFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, alllog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) errFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, errorlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) warnFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, warnlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) logrus.AddHook(&Hook{allLevel: allFile, errorLevel: errFile, warnLevel: warnFile}) } func main() { InitLevel("./log") logrus.SetReportCaller(true) logrus.Errorln("你好") logrus.Errorln("err") logrus.Warnln("warn") logrus.Infof("info") logrus.Println("print") }
gin集成logrus
main函数(main.go)
package main import ( "gin/Logger/gin/gin_logrus/log" "gin/Logger/gin/gin_logrus/middleware" "github.com/gin-gonic/gin" ) func main() { log.InitFile("./log", "fengxu") r := gin.New() r.Use(middleware.Logmiddleware()) r.GET("/", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run(":8080") }
log.go
’
package log import ( "bytes" "fmt" "github.com/sirupsen/logrus" "os" "time" ) type Hook struct { writer *os.File logPath string fileName string fileDate string } func (MyHook *Hook) Levels() []logrus.Level { return logrus.AllLevels } func (MyHook *Hook) Fire(entry *logrus.Entry) error { timer := time.Now().Format("2006-01-02") line, _ := entry.String() //需要切换日志文件 if MyHook.fileDate != timer { MyHook.fileDate = timer MyHook.writer.Close() filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate) err := os.MkdirAll(filepath, os.ModePerm) if err != nil { logrus.Error(err) return err } filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName) MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) } MyHook.writer.Write([]byte(line)) return nil } type LogFormat struct { } func (l *LogFormat) Format(entry *logrus.Entry) ([]byte, error) { var buff *bytes.Buffer if entry.Buffer != nil { buff = entry.Buffer } else { buff = &bytes.Buffer{} } _, _ = fmt.Fprintf(buff, "%s\n", entry.Message) //这里可以自己去设置输出格式 return buff.Bytes(), nil } func InitFile(logPath string, fileName string) { logrus.SetFormatter(&LogFormat{}) timer := time.Now().Format("2006-01-02") filepath := fmt.Sprintf("%s/%s", logPath, timer) err := os.MkdirAll(filepath, os.ModePerm) if err != nil { logrus.Error(err) return } filename := fmt.Sprintf("%s/%s.log", filepath, fileName) writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) if err != nil { logrus.Error(err) return } logrus.AddHook(&Hook{ writer: writer, logPath: logPath, fileName: fileName, fileDate: timer, }) }
中间件(lmiddleware.go)
package middleware import ( "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "time" ) const ( //自定义状态码和方法的显示颜色 status200 = 42 status404 = 43 status500 = 41 methodGET = 44 ) func Logmiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path raw := c.Request.URL.RawQuery if raw != "" { path = path + "?" + raw } c.Next() //执行其他中间件 //end := time.Now() //timesub := end.Sub(start) //响应所需时间 //ClientIp := c.ClientIP() //客户端ip statuscode := c.Writer.Status() //var statusColor string //switch c.Writer.Status() { //case 200: // statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status200, statuscode) //case 404: // statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status404, statuscode) //default: // statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status500, statuscode) //} // //var methodColor string //switch c.Request.Method { //case "GET": // methodColor = fmt.Sprintf("\033[%dm%s\033[0m", methodGET, c.Request.Method) //} logrus.Infof("[GIN] %s |%d |%s |%s", start.Format("2006-01-02 15:04:06"), statuscode, c.Request.Method, path, ) } }
项目结构:
结语
至此我们对Gin框架的简单学习就到此为止了,更多的学习大家可以前去查看Gin框架官方文档
:
Gin框架官方文档
后面就要开始对Gorm
的学习了,下篇见!