从0写一个Golang日志处理包

WHY

日志概述

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

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

一般Go应用程序多为并发模型应用,在并发处理实际应用的过程当中也没法避免并发地调用日志方法。缓存

一般来讲,Go中除非声明方法是协程安全的,不然最好都视为协程不安全,Go中默认的日志方法为log,该方法协程安全,可是功能有限。安全

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

设计概述

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

HOW

总体流程

怎么填充日志内容

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

设计两种类型分别用于接收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")

}
相关文章
相关标签/搜索