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{})
将日志输入到文件
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
的学习了,下篇见!