张子阳的博客

首页 读书 技术 店铺 关于
张子阳的博客 首页 读书 技术 店铺 关于

Go Tips: 定时更新日志的输出位置

2018-12-22 作者: 张子阳 分类: Go 语言

Go语言自带的log package非常简单和易用,只要在启动程序的时候调用log.SetOutput配置日志文件的路径,然后通过调用log.Printf()、log.Panic() 等方法就可以将日志输出到文件了。然而,对于一些服务程序而言,当操作系统启动时就开始运行,那么所有日志都会输出到启动时配置的那个文件当中。实际应用中,为了查看方便,我们经常需要按时间来生成日志,比如每天生成一个日志。那么可以通过单独运行一个goroutine,定时检查日志的最后创建时间来重新调用log.SetOutput方法,从而设置新的日志输出路径。

下面的代码实现了这一过程:

package main

import (
    "fmt"
    "log"
    "math/rand"
    "os"
    "path/filepath"
    "time"
)

func main() {
    testMyLog()
}

func testMyLog() {
    // logger := &MyLog{LoopDuration: time.Second, FileFormat: "20060102150405"} // 测试配置
    logger := &MyLog{}
    logger.Setup()

    for i := 0; i < 5; i++ {
        fmt.Printf("%v, 第%d次循环\n", time.Now().Format("150405"), i+1)
        log.Printf("第%d次循环\n", i+1)
        time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
    }
}

// MyLog 日志
type MyLog struct {
    lastFile     *os.File      // 最后创建的日志文件
    lastFileDate *time.Time    // 最后创建日志文件的时间
    FileFormat   string        // 文件格式
    LoopDuration time.Duration // 循环检查的时间间隔
}

// Setup 配置文件
func (x *MyLog) Setup() {
    if x.LoopDuration.Seconds() == 0 {
        x.LoopDuration = time.Minute
    }
    if x.FileFormat == "" {
        x.FileFormat = "20060102"
    }

    runningDir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
    logDir := runningDir + "/logs"
    fmt.Printf("日志目录: %s\n", logDir)
    if _, err := os.Stat(logDir); os.IsNotExist(err) {
        os.Mkdir(logDir, 0755)
    }
    // 先设置一遍
    x.setLogFile()

    go func() {
        for {
            time.Sleep(x.LoopDuration)
            x.setLogFile()
        }
    }()
}

func (x *MyLog) setLogFile() {
    if x.needNewFile() {

        path := fmt.Sprintf("./logs/%s", time.Now().Format(x.FileFormat))
        newFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)

        if err != nil {
            fmt.Printf("os.OpenFile(): %v\n", err)
        } else {
            // 关闭旧日志文件
            if x.lastFile != nil {
                err := x.lastFile.Close()
                if err != nil {
                    fmt.Printf("x.lastFile.Close(): %v\n", err)
                }
            }

            // 更新日期
            x.lastFile = newFile
            now := time.Now()
            x.lastFileDate = &now

            // 配置新日志
            fmt.Printf("%v, 配置日志:%s\n", time.Now().Format("20060102150405"), newFile.Name())
            log.SetOutput(newFile)
        }
    }
}

func (x *MyLog) needNewFile() bool {

    if x.lastFileDate == nil || x.lastFile == nil {
        return true
    }

    compare := "20060102"
    //compare := "20060102150405" // 测试配置
    now := time.Now().Format(compare)
    last := x.lastFileDate.Format(compare)

    if now != last {
        return true
    }

    return false
}

将上面“测试配置”的注释打开后(每秒钟生成一个日志文件),输出如下:

# go run main.go

日志目录: /Users/zhangzy/code/go/src/test/logs
20181222122614, 配置日志:./logs/20181222122614
122614, 第1次循环
122614, 第2次循环
20181222122615, 配置日志:./logs/20181222122615
122616, 第3次循环
20181222122616, 配置日志:./logs/20181222122616
20181222122617, 配置日志:./logs/20181222122617
122617, 第4次循环
122618, 第5次循环

可以看到一共创建了4个日志文件,并将结果写入了进去。这里有一个不完善的地方,就是可能出现空日志。

对于日志这样的通用功能,github上面已经有很多的开源项目。使用现成的项目会更方便快捷,自己重复造轮子需要花更多时间,而且还可能存在更多的潜在BUG。但在自己重复造轮子这个过程中,对这门语言却可以学到更多东西,而不仅仅是调调别人的API而已。

上面的代码还有一个主要的问题:它仅仅实现了按时间来创建新文件,如果想根据其他规则,例如根据不同的用户来创建日志(每个用户创建一个日志),则不支持了。此时,可以在MyStruct中包含一个*log.Logger对象,然后在这个对象上调用SetOutput()配置日志位置,再通过Printf()方法来输出日志。

除此以外,Golang自带的日志并没有提供分级功能(Trace、Debug、Info、Warn、Error、Fatal),而对熟悉Log4j等著名日志库的同学来说,这是必不可少的功能。这些同样可以通过扩展MyLog、调用SetPrefix()配置前缀来实现。这里就不演示了,以后有时间再完善一下。

感谢阅读,希望这篇文章能给你带来帮助!