Go实现日志
那接着来看日志选项,一个日志中会有很多选项,比如输出地方,输出格式,日志等级,是否输出行号等等。在输出中,也要输出日志等级,那肯定不是输出数字的,那是要输出例如"DEBUG"这种的,所以用map来映射。提供了initOptions函数来初始化日志选项,参数是传入函数。这里还没有实现logger结构体,所以还没有到使用设置的地方,讲到logger结构体的时候会再讲解。在日志输出时候,是会判别该日志等
0.前言
该日志的实现主要是参考自Go日志包logurs。
实现的日志包主要有以下几个功能:
- 支持文件名和行号。
- 支持多日志级别。
- 支持输出到本地文件和标准输出。
- 支持JSON和TEXT格式的日志输出,支持自定义日志格式。
- 支持自定义配置。
- 支持选项模式。
未来可能实现:支持结构化输出, 支持Hook.................
日志包名称为sulog。代码保存在https://github.com/liwook/Go-projects/tree/main/log
1.定义日志等级和日志选项
那首先是会有日志等级,一共6个等级。
在日志输出时候,是会判别输出的等级是否符合设定的日志等级,那就需要比较,所以日志类型用数值类型就好。
在输出中,也要输出日志等级,那肯定不是输出数字的,那是要输出例如"DEBUG"这种的,可以用数组或者切片来映射。
//option.go
type Level uint8
const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
PanicLevel
FatalLevel
)
var LevelNameMapping = []string{
DebugLevel: "DEBUG",
InfoLevel: "INFO",
WarnLevel: "WARN",
ErrorLevel: "ERROR",
PanicLevel: "PAINC",
FatalLevel: "FATAL",
}
那接着来看日志选项,一个日志中会有很多选项,比如输出地方,输出格式,日志等级,是否输出行号等等。这里我们就把这些选项打包封装到一个结构体options中。
// 日志选项结构体
type options struct {
output io.Writer
level Level
formatter Formatter //格式,比如json格式
disableCaller bool //设置是否打印文件名和行号
}
//格式定义,在formatter.go文件中
//Formatter 作为接口
type Formatter interface {
Format(entry *Entry) error
}
有了选项后,那就需要进行设置选项。提供了initOptions函数来初始化日志选项,参数是传入函数。
默认输出是os.Stderr,输出格式是text文本格式,默认日志等级是0(即是DebugLevel)。
type Option func(*options)
func initOptions(opts ...Option) (o *options) {
o = &options{}
for _, opt := range opts {
opt(o)
}
if o.output == nil {
o.output = os.Stderr
}
if o.formatter == nil {
o.formatter = &TextFormatter{}
}
return
}
对每一个日志选项创建设置函数 WithXXXX
。该日志支持如下选项设置
- WithOutput(output io.Writer):设置输出位置。
- WithLevel(level Level):设置输出级别。
- WithFormatter(formatter Formatter):设置输出格式。
- WithDisableCaller(caller bool):设置是否打印文件名和行号。
func WithOutput(output io.Writer) Option {
return func(o *options) {
o.output = output
}
}
func WithLevel(levle Level) Option {
return func(o *options) {
o.level = levle
}
}
func WithFormatter(formatter Formatter) Option {
return func(o *options) {
o.formatter = formatter
}
}
func WithDisableCaller(caller bool) Option {
return func(o *options) {
o.disableCaller = caller
}
}
结合initOptions函数用法如下:(比如使用WithOutput)
initOptions(WithOutput(os.Stderr))
这里还没有实现logger结构体,所以还没有到使用设置的地方,讲到logger结构体的时候会再讲解。
2.创建Logger
有了前面的铺垫,可以快速定义结构体logger。
logger中有options类型变量opt。那接着来看看创建Logger的函数New,其内部使用initOptions来初始化logger的opt变量。
而std是日志包的默认使用对象,没有设置参数。
日志包会有一个默认的全局Logger,通过 var std = New()
创建了一个全局的默认Logger。
sulog.Debug、sulog.Info和sulog.Warnf等函数,则是通过调用std Logger所提供的方法来打印日志的。
//logger.go
type logger struct {
opt *options
mu sync.Mutex//为了可以同步多协程写日志
}
var std = New()
func New(opts ...Option) *logger {
logger := &logger{opt: initOptions(opts...)}
return logger
}
func SetOptions(opts ...Option) {
std.SetOptions(opts...)
}
func (l *logger) SetOptions(opts ...Option) {
l.mu.Lock()
defer l.mu.Unlock()
//这里加锁也是为了可以多协程设置,写日志时刻就不能修改选项
for _, opt := range opts {
opt(l.opt)
}
}
这时修改日志的选项,就可以使用
sulog.SetOptions(sulog.WithLevel(sulog.DebugLevel))
定义了一个Logger之后,还需要给该Logger添加最核心的日志打印方法,要提供所有支持级别的日志打印方法。
如果日志级别是xxx,则通常会提供两类方法,分别是非格式化方法xxx(args ...any)
和格式化方法xxxf(format string, args ...any)
,例如:
func (l *logger) Debug(args ...any) {
l.log(DebugLevel, args...)
}
func (l *logger) Debugf(format string, args ...any) {
l.logf(DebugLevel, format, args...)
}
//Debug()调用log()
func (l *logger) log(level Level, args ...any) {
if l.opt.level > level { //日志等级不符合
return
}
newEntry := l.entry()
defer l.releaseEntry(newEntry)
newEntry.Log(level, args...)
}
//Debugf()调用logf()
func (l *logger) logf(level Level, fomat string, args ...any) {
if l.opt.level > level { //日志等级不符合
return
}
newEntry := l.entry()
defer l.releaseEntry(newEntry)
newEntry.Logf(level, fomat, args...)
}
本日志也实现了如下方法:Debug、Debugf、Info、Infof、Warn、Warnf、Error、Errorf、Panic、Panicf、Fatal、Fatalf。这里没有详细的展示。
需要注意的是,Panic、Panicf要调用panic()函数,Fatal、Fatalf函数要调用 os.Exit(1)
函数。
3.日志内容和输出
日志内容
我们定义一个Entry结构体类型。其日志内容数据和日志配置,都保存在Entry
对象中。
日志内容对象Entry
,主要功能是存储日志内容以及进行日志内容写入。
type Entry struct {
logger *logger
Buffer *bytes.Buffer //日志内容的存储地
DataMap map[string]any //为了日志是json格式使用的
Level Level
Time time.Time
File string
Line int
Func string
Message string //日志数据
}
//创建entry
func entry(logger *logger) *Entry {
return &Entry{
logger: logger,
Buffer: new(bytes.Buffer),
DataMap: make(map[string]any, 5)}
}
为什么要有Entry类型呢?首先这样可以简化了logger结构体的打印方法代码(如上面的代码),也方便管理日志内容,而且这样也方便做日志内容结构化。
输出
用Entry的Log或Logf方法来完成日志的写入,自带格式的使用logf,其余使用Log,最终是使用log方法。
在log方法中,还会判断是否需要记录文件名和行号,如果需要则调用 runtime.Caller()
来获取文件名和行号,调用 runtime.Caller()
时,要注意传入正确的栈深度。
func (e *Entry) Log(level Level, args ...any) {
e.log(level, fmt.Sprint(args...))
}
func (e *Entry) Logf(level Level, format string, args ...any) {
e.log(level, fmt.Sprintf(format, args...))
}
func (e *Entry) log(level Level, msg string) {
e.Time = time.Now()
e.Level = level
e.Message = msg
if !e.logger.opt.disableCaller {
if pc, file, line, ok := runtime.Caller(4); !ok {
e.File = "???"
e.Func = "???"
} else {
e.File, e.Line, e.Func = file, line, runtime.FuncForPC(pc).Name()
e.Func = e.Func[strings.LastIndex(e.Func, "/")+1:]
}
}
e.write()
}
log方法中调用 e.write
来格式化日志(格式化后,会把内容写到e.Buffer中),并写入日志。
output类型为 io.Writer,在
e.writeh中,调用e.logger.opt.output.Write(e.Buffer.Bytes())
即可将日志写入到指定的位置中。
func (e *Entry) write() {
e.logger.mu.Lock()
defer e.logger.mu.Unlock()
e.logger.opt.formatter.Format(e)
e.logger.opt.output.Write(e.Buffer.Bytes())
}
看回Debug方法中,其实是只要写一次日志,就需要创建一个Entry结构体(logger.entry()就是创建entry),写完后就释放,这是比较耗性能的。
所以,这块在创建一个新的Entry对象时,使用sync.Pool
对象 entryPool,主要保存Entry指针空对象,使用完后再放回sync.Pool中,防止在记录日志的时候大量开辟内存空间,触发GC操作,从而提高日志记录的速度。
使用sync.Pool的关键就是对象的复用,避免重复创建、销毁,而且其是协程安全的,这对于使用者来说是极其方便的。
sync.Pool的用法:使用前,设置好对象的 New
函数,用于在 Pool
里没有缓存的对象时,创建一个。之后,在程序的任何地方、任何时候仅通过 Get()
、Put()
方法就可以取、还对象了。
所以需要再logger结构体中添加sync.Pool类型变量entryPool。在New函数中也需要添加对entryPool的初始化。
type logger struct {
//..........
entryPool *sync.Pool //新添加,存放临时的Entry对象,减少GC对Entry对象的内存回收,提高Entry对象复用,提高效率
}
func New(opts ...Option) *logger {
logger := &logger{opt: initOptions(opts...)}
logger.entryPool = &sync.Pool{New: func() any { return entry(logger) }} //新添加的
return logger
}
//获取entry对象
func (l *logger) entry() *Entry {
return l.entryPool.Get().(*Entry)
}
func (l *logger) releaseEntry(e *Entry) {
e.DataMap = map[string]any{}
e.Line, e.File, e.Func = 0, "", ""
e.Buffer.Reset()
l.entryPool.Put(e)
}
4.自定义日志输出格式
前面的(Entry).format方法使用了自定义格式输出。有多种格式类型,我们可以定义成接口类型Formatter。
将Entry中的数据内容,如DataMap字段存储的有结构的数据键值对,Message中存储的无结构数据,以及Time存储的日志记录时间等等,格式化成我们想要的数据格式
//formatter.go
type Formatter interface {
Format(entry *Entry) error
}
那么接着实现两种格式:JSON和TEXT格式。
JSON格式
json的编解码使用字节开发的"github.com/bytedance/sonic"。
通过sonic.ConfigDefault.NewEncoder(e.Buffer).Encode(e.DataMap)来把日志内容写到e.Buffer。
type JsonFormatter struct {
DisableTimestamp bool
TimestampFormat string
}
func (f *JsonFormatter) Format(e *Entry) error {
if !f.DisableTimestamp {
if f.TimestampFormat == "" {
f.TimestampFormat = time.RFC3339
}
e.DataMap["time"] = e.Time.Format(f.TimestampFormat)
}
e.DataMap["level"] = LevelNameMapping[e.Level]
if e.File != "" {
e.DataMap["file"] = e.File + ":" + strconv.Itoa(e.Line)
e.DataMap["func"] = e.Func
}
e.DataMap["message"] = e.Message
return sonic.ConfigDefault.NewEncoder(e.Buffer).Encode(e.DataMap)
}
TEXT格式的就不展示了。
5.测试
func main() {
//使用默认全局变量
sulog.Info("std info log")
sulog.SetOptions(sulog.WithLevel(sulog.ErrorLevel))
sulog.Info("std can not info") //设置了ErrorLevel等级,那InfoLevle就输出不了
sulog.SetOptions(sulog.WithFormatter(&sulog.JsonFormatter{}))
sulog.Error("bad error")
sulog.Errorf("%s %d", "myhome", 111) //用户自定义message输出格式
//输出到文件
file, err := os.OpenFile("./test.log", os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatal("create file test.log failed")
}
defer file.Close()
//自定义logger变量,New函数中设置选项
l := sulog.New(sulog.WithLevel(sulog.InfoLevel),
sulog.WithOutput(file))
l.SetOptions(sulog.WithFormatter(&sulog.JsonFormatter{IgnoreBasicFields: true}))
l.Info("log with json")
}
更多推荐
所有评论(0)