从0写一个Golang日志处理包

WHY

日志概述

日志几乎是每个实际的软件项目从开发到最后实际运行过程中都必不可少的东西。它对于查看代码运行流程,记录发生的事情等方面都是很重要的。

一个好的日志系统应当能准确地记录需要记录的信息,同时兼具良好的性能,接下来本文将从0写一个Golang日志处理包。

通常Go应用程序多为并发模型应用,在并发处理实际应用的过程中也无法避免并发地调用日志方法。

通常来说,Go中除非声明方法是协程安全的,否则最好都视为协程不安全,Go中默认的日志方法为log,该方法协程安全,但是功能有限。

第三方Go日志包如ZAP、logrus等均为较为不错的日志库,也提供了较多的配置选项,但是对于高度定制需求的日志则有些不适用,从0开始自己写一个就较为适用。

设计概述

  • 按json方式输出到文件
  • 日志类型支持丰富
  • 有日志分级设定
  • 按天分隔日志文件
  • 异常退出能把未写入的日志写完

HOW

整体流程

怎么填充日志内容

要做到按json方式输出到文件,并且支持丰富的类型,第一个能想到的则是Golang中强大的interface

设计两种类型分别用于接收map型和string型日志,再根据日志分级设定分别暴露出对外接口:

func Debug(msg map[string]interface{}) {
	writeLog("DEBUG", msg)
}
func DebugMsg(msg interface{}) {
	writeLog("DEBUG", map[string]interface{}{"msg": msg})
}
func Info(msg map[string]interface{}) {
	writeLog("INFO", msg)
}
func InfoMsg(msg interface{}) {
	writeLog("INFO", map[string]interface{}{"msg": msg})
}
func Warn(msg map[string]interface{}) {
	writeLog("WARN", msg)
}
func WarnMsg(msg interface{}) {
	writeLog("WARN", map[string]interface{}{"msg": msg})
}
func Error(msg map[string]interface{}) {
	writeLog("ERROR", msg)
}
func ErrorMsg(msg interface{}) {
	writeLog("ERROR", map[string]interface{}{"msg": msg})
}

最终都是使用writeLog进行日志的处理,writeLog方法定义如下:

func writeLog(level string, msg map[string]interface{})

用哪种方式写入文件

Golang对于文件的写入方式多种多样,通常来讲最后都是使用操作系统的磁盘IO方法把数据写入文件

在选型上这块使用bufio方式来构建,使用默认的4096长度,如果收集的日志长度超过了缓冲区长度则自动将内容写入到文件,同时增加一组定时器,每秒将缓冲区内容写入到文件中,这样在整体性能上较为不错

处理协程抢占问题

针对多协程抢占的问题,Golang提供有两个比较标准的应对方式:使用channel或者加锁

在这里采用读写锁的方式来进行处理bufio,bufio的WriteString方法需串行处理,要不然会导致错误,而Flush方法可以多协程同时操作

基于这些特性,我们在使用WriteString方法的时候使用写锁,使用Flush方法时采用读锁:

	fileWriter.Mu.Lock()
	fileWriter.Writer.WriteString(a)
	fileWriter.Mu.Unlock()
	fileWriter.Mu.RLock()
	err = fileWriter.Writer.Flush()
	if err != nil {
		log.Println("flush log file err", err)
	}
	fileWriter.Mu.RUnlock()

跨天的日志文件处理

首先明确一个问题,在每日结束次日开始时将打开一个新的日志文件,那么还在缓冲区未完成刷新的数据怎么处理呢?

bufio提供了Reset方法,但是该方法注释说明将丢弃未刷新的数据而直接重新指向新的io writer,因此我们不能直接使用该方法,否则这个时间节点附近的数据将会丢掉

实际测试证明如果先关闭原IO,再重新创建新的文件描述符,最后调用Reset方法指向新的描述符过后这段时间将会丢掉达约20ms的数据

基于此,我们使用了二级指针:

1.判断当前日期是否就是今天,如果是则等待下个判断周期,如果不是则开始准备指针重定向操作

2.判断哪一个文件描述符为空,如果为空则为其创建新的描述符,并指定配对的filebuffer,如果不为空则说明它就是当前正在操作的文件

3.将filewriter指向新的filebuffer

4.对老的filebuffer进行Flush操作,之后将filebuffer和file都置为空

经过这样的操作过后,跨天的日志文件处理就不会有数据丢失的情况了

			if today != time.Now().Day() {
				today = time.Now().Day()
				if file[0] == nil {
					file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
					if err != nil {
						log.Fatal("open log file err: ", err)
					}
					fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
					fileWriter.Writer = fileBuffer[0]
					if fileBuffer[1].Buffered() > 0 {
						fileBuffer[1].Flush()
					}
					fileBuffer[1] = nil
					file[1].Close()
					file[1] = nil
				} else if file[1] == nil {
					file[1], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
					if err != nil {
						log.Fatal("open log file err: ", err)
					}
					fileBuffer[1] = bufio.NewWriterSize(file[1], 4096)
					fileWriter.Writer = fileBuffer[1]
					if fileBuffer[0].Buffered() > 0 {
						fileBuffer[0].Flush()
					}
					fileBuffer[0] = nil
					file[0].Close()
					file[0] = nil
				}

			}

异常退出的处理

一个基本的概念就是程序在操作系统退出的时候通常都会得到系统信号,比如linux的kill操作就是给应用程序发送系统信号

信号分很多种,比如常见的ctrl+c对应的信号则是Interrupt信号,这块去搜索“posix信号”也有详细的解释说明

基于此,常规的异常处理我们可以捕获系统信号然后做一些结束前的处理操作,这样的信号可以在多个包同时使用,均会收到信号,不用担心信号强占的问题

比如这个包在接收到退出信号时则刷新所有缓存数据并关闭所有文件描述符:

func exitHandle() {

	<-exitChan

	if file != nil {
		if fileWriter.Writer.Buffered() > 0 {
			fileWriter.Writer.Flush()
		}
		//及时关闭file句柄
		if file[0] != nil {
			file[0].Close()
		}
		if file[1] != nil {
			file[1].Close()
		}

	}
	os.Exit(1) //使用os.Exit强行关掉
}

完整源码

package logger

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"xxx/config"
	"sync"
	"syscall"
	"time"
)

var file []*os.File
var err error
var fileBuffer []*bufio.Writer
var exitChan chan os.Signal

type fileWriterS struct {
	Writer *bufio.Writer
	Mu     sync.RWMutex
}

var fileWriter fileWriterS
var today int

func LoggerInit() {

	filePath := config.Get("app", "log_path")    //config处可以直接换成自己的config甚至直接写死
	_, err := os.Stat(filePath)
	if err != nil {
		if os.IsNotExist(err) {
			os.MkdirAll(filePath, os.ModePerm)
		} else {
			log.Fatal("log path err:", err)
		}
	}

	file = make([]*os.File, 2)
	file[0] = nil
	file[1] = nil
	fileBuffer = make([]*bufio.Writer, 2)
	fileBuffer[0] = nil
	fileBuffer[1] = nil
	today = time.Now().Day()
	file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		log.Fatal("open log file err: ", err)
	}
	fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
	fileWriter.Writer = fileBuffer[0]
	exitChan = make(chan os.Signal)
	signal.Notify(exitChan, os.Interrupt, os.Kill, syscall.SIGTERM)
	go exitHandle()
	go func() {
		time.Sleep(1 * time.Second)
		for {
			if fileWriter.Writer.Buffered() > 0 {
				fileWriter.Mu.RLock()
				err = fileWriter.Writer.Flush()
				if err != nil {
					log.Println("flush log file err", err)
				}
				fileWriter.Mu.RUnlock()
			}
			time.Sleep(1 * time.Second)

			if today != time.Now().Day() {
				today = time.Now().Day()
				if file[0] == nil {
					file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
					if err != nil {
						log.Fatal("open log file err: ", err)
					}
					fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
					fileWriter.Writer = fileBuffer[0]
					if fileBuffer[1].Buffered() > 0 {
						fileBuffer[1].Flush()
					}
					fileBuffer[1] = nil
					file[1].Close()
					file[1] = nil
				} else if file[1] == nil {
					file[1], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
					if err != nil {
						log.Fatal("open log file err: ", err)
					}
					fileBuffer[1] = bufio.NewWriterSize(file[1], 4096)
					fileWriter.Writer = fileBuffer[1]
					if fileBuffer[0].Buffered() > 0 {
						fileBuffer[0].Flush()
					}
					fileBuffer[0] = nil
					file[0].Close()
					file[0] = nil
				}

			}

		}
	}()

}

func exitHandle() {

	<-exitChan

	if file != nil {
		if fileWriter.Writer.Buffered() > 0 {
			fileWriter.Writer.Flush()
		}

		if file[0] != nil {
			file[0].Close()
		}
		if file[1] != nil {
			file[1].Close()
		}

	}
	os.Exit(1)
}

func Debug(msg map[string]interface{}) {
	writeLog("DEBUG", msg)
}
func DebugMsg(msg interface{}) {
	writeLog("DEBUG", map[string]interface{}{"msg": msg})
}
func Info(msg map[string]interface{}) {
	writeLog("INFO", msg)
}
func InfoMsg(msg interface{}) {
	writeLog("INFO", map[string]interface{}{"msg": msg})
}
func Warn(msg map[string]interface{}) {
	writeLog("WARN", msg)
}
func WarnMsg(msg interface{}) {
	writeLog("WARN", map[string]interface{}{"msg": msg})
}
func Error(msg map[string]interface{}) {
	writeLog("ERROR", msg)
}
func ErrorMsg(msg interface{}) {
	writeLog("ERROR", map[string]interface{}{"msg": msg})
}

func writeLog(level string, msg map[string]interface{}) {

	will_write_map := make(map[string]interface{})
	t := time.Now()
	will_write_map["@timestamp"] = fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.%03d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
	will_write_map["level"] = level

	for p, v := range msg {
		will_write_map[p] = v
	}

	//buffer满了会自动flush
	bf := bytes.NewBuffer([]byte{})
	jsonEncoder := json.NewEncoder(bf)
	jsonEncoder.SetEscapeHTML(false)
	jsonEncoder.Encode(will_write_map)
	a := bf.String()

	//	fmt.Println(a)
	fileWriter.Mu.Lock()
	fileWriter.Writer.WriteString(a)
	fileWriter.Mu.Unlock()
	//	fileWriter.WriteString("\n")

}