目录
摘要
痛点
正文
1.分析 io.Writer 接口
2.实现 io.Writer 接口
3.将它作为原生输出
4.将它作为 Gin 框架的输出
摘要
自定义一个日志输出,将go语言和gin框架的日志自动按天拆分。本文通过实现io.Writer接口的方式,替换原生和gin框架的默认Writer,并植入了自定义的逻辑。该示例只讲述了如何按天切分日志,如果需要更多定制的内容,可以很方便的改写demo代码。
痛点
网络上没有原生日志切割相关的内容,动不动就讲引入第三方库。例如logrus,但我不想为了一个很简单的需求,即“按天记录日志”,去引入一整个库。这个需求的原因是,在linux下运行的nohup.out文件总是积累的很快,删了看不到日志,不删又怕哪天堵满磁盘。
正文
注意:本文内容的顺序与思考顺序不同,是按知识点排序的。
1.分析 io.Writer 接口
以下代码来自 go1.19,在 io包 [ io.go:90 ]左右的内容
// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
Write(p []byte) (n int, err error)
}
可以见到,Writer 这个接口非常的简单,只包含了一个 Write 函数,接受一个二进制切片,返回一个数字和一个异常。注释似乎是说:返回的数字是写成功的长度,如果和输入的切片长度不等,应当返回一个异常。
2.实现 io.Writer 接口
import (
"io"
"os"
"time"
)
// 自定义writer的对象
var myWriter dateFileWriter
// 当前要写入日志的文件
var targetFile *os.File
// 自定义一个writer专门用于写日志
type dateFileWriter struct {
io.Writer
}
// 为自定义writer实现Write接口
func (b *dateFileWriter) Write(p []byte) (n int, err error) {
return targetFile.Write(p)
}
// RefreshLogFileUsage 刷新指向的日志文件
func RefreshLogFileUsage() {
fileName := time.Now().Format("2006_01_02") + ".log"
tryFile, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, 0777)
if err != nil {
os.WriteFile(fileName, []byte(""), 0777)
targetFile, _ = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0777)
} else {
targetFile = tryFile
}
}
// MyLogWriter 返回自定义的 writer
func MyLogWriter() io.Writer {
RefreshLogFileUsage()
return &myWriter
}
首先我们定义一个文件指针 targetFile,这就是我们自定义 Writer 会写入内容的文件。
然后我们自定义一个 dateFileWriter,实现了 io.Writer 接口,做的事情非常简单,就是把接收的内容直接写入到指向的文件里面去。
我的需求是按日期拆分,所以定义了一个辅助函数 RefreshLogFileUsage(),它会判断当前日期的日志是否存在,如果不存在,他会新建一个以当前日期命名的文件。需要用定时任务在每天凌晨触发这个函数,才能实现完整的功能。如果你没有引入过定时任务,也可以在每次写入之前执行这个函数。
所以这段代码的逻辑是:一个 Writer 接口的实现会接收一些内容,将他们写入指定的文件,可以手动修改这个指定的文件,以实现根据运行时间将日志写到不同文件的需求。
同理你可以自己扩展一些其他内容,例如拆分 error 和 info 的输出文件等。
3.将它作为原生输出
你只需要在项目伊始执行这段代码
log.SetOutput(MyLogWriter())
将原生 log 包的输出指定为自己的实现类就可以了。
注意,fmt不能这样操作
另外,如果你希望log的内容同时输出在控制台和日志文件中,需要这样改写
log.SetOutput(io.MultiWriter(os.Stdout,MyLogWriter()))
这里可以顺便看下 io.MultiWriter() 是干啥的,在 io 包 [ multi.go:120 ] 左右
// MultiWriter creates a writer that duplicates its writes to all the
// provided writers, similar to the Unix tee(1) command.
//
// Each write is written to each listed writer, one at a time.
// If a listed writer returns an error, that overall write operation
// stops and returns the error; it does not continue down the list.
func MultiWriter(writers ...Writer) Writer {
allWriters := make([]Writer, 0, len(writers))
for _, w := range writers {
if mw, ok := w.(*multiWriter); ok {
allWriters = append(allWriters, mw.writers...)
} else {
allWriters = append(allWriters, w)
}
}
return &multiWriter{allWriters}
}
可以看到这个 Writer 就是接收了多个 Writer,他可以把写入行为分发给多个 Writer 同时执行。
注意:原生的 log 包可以随时通过 SetOutput 函数修改日志输出行为,也就是说,如果你只想处理原生日志,无需自定义一个结构体,只需要在需要的时候通过 SetOutput 修改文件指向就行了。
4.将它作为 Gin 框架的输出
参见上面这条 注意 中的内容,显然,Gin框架的输出行为不能随时改变,他的函数是这个
// 设置 gin 默认的日志输出
gin.DefaultWriter = MyLogWriter()
// 创建 gin 实例
Router = gin.Default()
重点:一旦 gin 的实例被创建,就不能通过 gin.DefaultWriter() 这种方式修改他的日志输出了。我简单的看了一下,gin.Default() 函数会创建一个 Engine 对象,日志作为一个中间件被设置在了这个对象里。而 Engine 对象在运行时支持添加中间件,并不支持删除或者修改中间件。
此处的不支持修改其实不严谨,但我确实没找到 Gin 提供的修改这些中间件相关的函数,但其实他们是支持修改的,只不过呢,操作起来可能有些麻烦。你可以通过这种方式找到这些中间件,当然也包括需要动态修改的日志 Writer
你的gin实例.RouterGroup.Handlers
这里是一个 HandlerFunc 接口的数组,感觉这样改起来应该很复杂,所以我不建议通过这种方式实现。
所以咱们就老老实实的,在 gin 的实例被创建前,设置默认输出到我们自定义的 Writer,然后通过动态修改我们自己对象的方式,实现动态输出 gin 的日志。
实现效果: