golang日志库的简单实现

在开发过程中,通常我们会将信息输出到终端来查看变量和结果是否和预想的一样,但是如果丢到线上后依然是输出到终端的话,它就会占用部分资源,但是我们并没有使用到,并且后期出错查询也很困难,因此写一个在开发过程中输出到终端,在编译后输出到文件的程序很有必要

1
2
3
4
5
fileObj,_:=os.OpenFile("./xx.log",os.O_APPEND|os.O_CREATE|os.O_WRONLY,0644)
	//设置日志输出位置为fileObj,默认的值是os.Stdout,即终端
	log.SetOutput(fileObj)
	log.Printf("这是一条错误输出")
	time.Sleep(3*time.Second)

需求分析

  1. 支持往不同的地方输出日志(终端和文件中)
  2. 日志分级别
    1. Debug
    2. Trace
    3. Info
    4. Warning
    5. Error
    6. Fatal
  3. 日志要支持级别开关控制,比如说开发的时候所有级别都输出,但是上线后只有INFO级别往下的才能输出
  4. 完整的日志记录要包含时间、行号、文件名、日志级别、日志信息
  5. 日志文件要切割
    1. 按文件大小切割
      1. 我们初始化时有设置一个maxFileSize文件最大值,可以通过在每次写日志时都判断一下当前文件大小来进行切割
    2. 按日志切割(直接将文件名设置为当前时间就可以通过日志切割)

runtime.Caller()

pc,file,line,ok:=runtime.Caller(skip int) 这个函数是运行时gc的获取的部分信息,它返回四个值

  • pc返回的是执行函数指针,可以通过这个获取函数名 runtime.FuncForPC(pc).Name()等信息
  • file是执行函数所在文件名目录,使用 path.Base(file)可以获取到绝对路径中的最后一层目录,
  • line是执行的函数所在行号
  • ok 是否可以获取到信息,返回false时前面三个值都为零值

执行函数指的是调用Caller的那个函数,通过skip可以向上调用更高级别的执行函数信息

skip是要提升的堆栈帧数,如就在当前函数调用的Caller,它的值就是0,如果要在另一个函数调用它,并且获取的值要是另一个函数的,那它就得提升一个,即为1,如果main再调用,想获取main的信息,就是2,即a函数调用caller就升一层,b调用a再升一层,想获取b的信息skip就为2

代码

公用代码:logPublicfunc.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package mylogger

import (
	"fmt"
	"strings"
)
//定义一个接口处理两种不同日志
type Logger interface {
	Debug(format string,a ...interface{})
	Info(format string,a ...interface{})
}

//定义日志级别为非0的整数,使用type可以在以后打印类型时知道它的作用,而uint则无法理解用于什么地方
type LogLevel uint16
//设置每个级别对应的uint16值unknow为0,表示输入的格式不正确
const (
	UNKNOW LogLevel =iota
	DEBUG
	INFO
)

//通过传入的字符串来判断级别,返回的是uint常量
func parseLogLevel(s string) (LogLevel,error){
	//将传入的字符串全部转为大写
	s=strings.ToUpper(s)
	switch s {
	case "DEBUG":
		return DEBUG,nil
	case "INFO":
		return INFO,nil
	default:
		//创建一个err,如果err不为空则是输入有误
		err:=fmt.Errorf("无效的日志级别,请检查")
		return UNKNOW,err
	}
}

func getLogString(lv LogLevel) string{
	switch lv {
	case DEBUG:
		return "DEBUG"
	case INFO:
		return "INFO"
	default:
		return "UNKNOW"
	}
}

日志写到终端中:consolelogger.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package mylogger
//往终端中写日志

import (
	"fmt"
	"os"
	"time"
)

//自定义一个日志库结构体
type ConsoleLogger struct {
	Level LogLevel
}

//初始化一个Logger结构体
func NewConsoleLogger(s string) *ConsoleLogger{
	level,err:=parseLogLevel(s)
	if err!=nil{
		//如果err不为空,则level为零值=0,0对应着UNKNOW
		fmt.Println(err)
		os.Exit(0)
	}
	return &ConsoleLogger{Level: level}
}
//实现开关级别,通过对象和参数的比较来执行比设置的级别高的日志消息
func (c *ConsoleLogger)enable(loglevel LogLevel)bool{
	return loglevel>=c.Level
}

//可以通过可变参数(参照fmt.Printf函数)实现格式化输出,实现传变量进字符串
//Sprintf返回一个格式化好的字符串
func (c *ConsoleLogger)logFmt(lv LogLevel,format string,a ...interface{}){
	//设置日志时间格式
	var timeInit =time.Now().Format("2006-01-02 15:04:05")
	if c.enable(lv){
		msg:=fmt.Sprintf(format,a...)
		fmt.Fprintf(os.Stdout,"[%s] [%s] %s\n",timeInit,getLogString(lv),msg)
	}
}

func (c *ConsoleLogger)Debug(format string,a ...interface{}){
	c.logFmt(DEBUG,format,a...)
}

func (c *ConsoleLogger)Info(format string,a ...interface{}){
	c.logFmt(INFO,format,a...)
}

日志写到文件中,且以大小分隔:fileLogger.go

可以使用channel让日志和业务代码分开来,写日志不影响正常业务的运行。写日志报错只会导致日志写入失败,但是程序依然在运行

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package mylogger

import (
	"fmt"
	"os"
	"path"
	"time"
)

//往文件中写日志

type FileLogger struct {
	Level LogLevel
	FileName string	//日志文件保存的文件名
	FilePath string	//日志文件保存的路径
	MaxFileSize int64 //每个日志文件的最大值
	FileObj *os.File	//初始化时打开一个文件
	ErrFileObj *os.File	//记录错误日志的文件
	logChan chan *logMsg	//管道,用于存放日志信息
}

type logMsg struct {
	level LogLevel	//日志级别
	msg string		//日志信息
	timestamp string //时间戳
}

//初始化一个Logger结构体
func NewFileLogger(s,fileName,filePath string,maxFileSize int64) *FileLogger{
	level,err:=parseLogLevel(s)
	if err!=nil{
		//如果err不为空,则level为零值=0,0对应着UNKNOW
		fmt.Println(err)
		os.Exit(0)
	}
	f1:= &FileLogger{
		Level: level,
		FileName:fileName,
		FilePath: filePath,
		MaxFileSize: maxFileSize,
		logChan: make(chan *logMsg,50000),
	}
	err1:=f1.initFile()
	if err1!=nil{
		//日志文件都无法打开,直接panic中断程序运行
		panic(err1)
	}
	return f1
}
//实现开关级别,通过对象和参数的比较来执行比设置的级别高的日志消息
func (f *FileLogger)enable(loglevel LogLevel)bool{
	return loglevel>=f.Level
}

//初始化,打开一个文件向内写日志
func (f *FileLogger)initFile() error{
	//拼接文件路径和文件名,然后打开此文件,没有就进行创建
	fullFileName:=path.Join(f.FilePath,f.FileName)
	fileObj,err:=os.OpenFile(fullFileName,os.O_APPEND|os.O_CREATE|os.O_WRONLY,0644)
	if err!=nil{
		fmt.Printf("open log file failed,err:%v",err)
		return err
	}
	//专门记录错误的日志
	errfileObj,err:=os.OpenFile(fullFileName+".err",os.O_APPEND|os.O_CREATE|os.O_WRONLY,0644)
	if err!=nil{
		fmt.Printf("open log file failed,err:%v",err)
		return err
	}
	f.FileObj=fileObj
	f.ErrFileObj=errfileObj
	//开启一个后台goroutine往文件内写日志
	//不能开启多个,因为同一时间不能有多个程序操作同一个文件
	go f.writeLogBackground()
	//return nil可以实现调用此方法时加if判断来判断是否有错误
	return nil
}

//判断当前文件大小,超过指定值就进行切割,将要写入的文件传进去判断
func (f *FileLogger)checkSize(file *os.File) bool{
	fileInfo,err:=file.Stat()
	if err!=nil{
		fmt.Printf("get file info failed,err:%v\n",err)
		return false
	}
	//如果当前文件大小大于等于预设时文件的最大值,就应该返回true,进行切割
	return fileInfo.Size()>=f.MaxFileSize
}

func (f *FileLogger)splitFile(file *os.File) (*os.File,error){
	//此时则需要进行文件切割,如果返回为false就会直接跳过该判断,继续往原文件写内容
	fileInfo,err:=file.Stat()
	if err!=nil{
		fmt.Printf("open file faile,err:%v\n",err)
		return nil,err
	}
	//然后rename原文件,fileInfo.Name()可以获取文件名,可以同时适配普通文件和错误文件
	logName:=path.Join(f.FilePath,fileInfo.Name())//拿到当前日志的路径,join的拼接是用/来拼接的
	//在重命名时要先关闭当前的日志文件
	nowStr:=time.Now().Format("20060102150405")//获取当前时间
	file.Close()
	newName:=logName+".bak"+nowStr
	os.Rename(logName,newName)//重命名
	//打开新的文件并赋值给FileObj
	fileObj,err:=os.OpenFile(logName,os.O_APPEND|os.O_CREATE|os.O_WRONLY,0644)
	if err!=nil{
		fmt.Printf("open file faild,err:%v\n",err)
		return f.FileObj,err
	}
	return fileObj,nil
}

//开携程让日志和业务代码区分开
func (f *FileLogger)writeLogBackground(){
	//for循环让此函数一直运行等待从管道中取数据
	for{
	//普通日志文件分割
	if f.checkSize(f.FileObj){
		fileObj,err:=f.splitFile(f.FileObj)
		if err!=nil{
			fmt.Printf("open file faild,err:%v\n",err)
			return
		}
		f.FileObj=fileObj
	}
	select {
	//如果无法从logChan中取出数据,就一直阻塞在此处
	case logtmp:=<-f.logChan:
			logInfo:=fmt.Sprintf("[%s] [%s] %s\n",logtmp.timestamp,getLogString(logtmp.level),logtmp.msg)
			fmt.Fprintf(f.FileObj,logInfo)
			//错误日志文件分割
			if logtmp.level>=INFO{
				if f.checkSize(f.ErrFileObj){
					errfileObj,err:=f.splitFile(f.ErrFileObj)
					if err!=nil{
						fmt.Printf("open file faild,err:%v",err)
						return
					}
					f.ErrFileObj=errfileObj
				}
				//如果要记录的日志等级(输入的参数)大于等于INFO级别,则还要在err日志文件中再记录一遍
				fmt.Fprintf(f.ErrFileObj,logInfo)
			}
	}
	}
}

//可以通过可变参数(参照fmt.Printf函数)实现格式化输出,实现传变量进字符串
//Sprintf返回一个格式化好的字符串
func (f *FileLogger)logFmt(lv LogLevel,format string,a ...interface{}){
	if f.enable(lv){
		//设置日志时间格式
		var timeInit =time.Now().Format("2006-01-02 15:04:05")
		msg:=fmt.Sprintf(format,a...)
		//先把日志发到通道中
		logtmp:=&logMsg{level: lv,msg:msg,timestamp: timeInit}
		//通过select监听管道,避免极端情况下通道阻塞影响业务代码的运行
		//写的进去和写不进去都直接跳过,然后继续执行代码
		select {
		case f.logChan<-logtmp:

		default:
		//如果写不进去就进行跳过,保证业务代码正常运行,但是此时日志写不进去(极端情况)
		}
	}

}


func (f *FileLogger)Debug(format string,a ...interface{}){
	f.logFmt(DEBUG,format,a...)
}

func (f *FileLogger)Info(format string,a ...interface{}){
	f.logFmt(INFO,format,a...)
}

//在用完后记得关闭文件
func (f *FileLogger)Close(){
	f.FileObj.Close()
	f.ErrFileObj.Close()
}

main文件:logermain.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"mylogger"
	"time"
)

var logger mylogger.Logger

func main() {
	//这里的参数表示会输出大于等于debug等级的日志
	//可以在consolelogger.go中设置大于则输出到终端,小于则输出到文件中
	//输出到终端
	logger = mylogger.NewConsoleLogger("debug")
	for i:=0;i<2;i++{
		logger.Debug("我是一条debug消息")
		id:=10010
		name:="张三"
		logger.Info("我是一条info消息,来自%d-%s",id,name)
	}

	//输出到文件中
	logger=mylogger.NewFileLogger("info",time.Now().Format("2006-01-02"),"./",10*1024)//for {
		//如果消息级别为0表示是错误的等级
		for{
		logger.Debug("我是一条debug消息")
		id:=10010
		name:="张三"
		logger.Info("我是一条info消息,来自%d-%s",id,name)
	}

}
Licensed under CC BY-NC-SA 4.0