日志

标准库的log包

Go标准库中有log包,提供了简单的日志功能。

输出 格式输出 换行输出
log.Print() log.Printf() log.Pringln() 类似fmt.print*
log.Fatal() log.Fatalf() log.Fatalln() 相当于log.Print* + os.Exit(1)
log.Panic() log.Panicf() log.Panicln() 相当于log.Print* + panic(0

日志输出需要使用日志记录器Logger。

log包提供了一个缺省的Logger即std。std是小写的,包外不可见,所以提供了Default()方法返回std给包外使用。如下:

package main

import (
	"log"
)

func main() {
        // 这里就是使用缺省的logger对象,可以点进Println查看源码
	log.Println("这是缺省的logger")
}

源码如下:

// go源码
// Println calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Println.
func Println(v ...any) {
	if std.isDiscard.Load() {
		return
	}
	std.Output(2, fmt.Sprintln(v...))
}
// 再点击std可以查看到,这里有一个默认的std,他的输出是os.Stderr标准错误输出到控制台,flag为LstdFlags
var std = New(os.Stderr, "", LstdFlags)

// Default returns the standard logger used by the package-level output functions.
func Default() *Logger { return std }

标准库的flag如下:

// 源码41行左右
//	2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
const (
	Ldate         = 1 << iota     // 1 当前时区日期: 2009/01/23
	Ltime                         // 2 当前时区时间: 01:23:23
	Lmicroseconds                 // 4 微秒: 01:23:23.123123.  assumes Ltime.
	Llongfile                     // 8 绝对路径和行号: /a/b/c/d.go:23
	Lshortfile                    // 16 文件名和行号: d.go:23. overrides Llongfile
	LUTC                          // 32 使用UTC(GMT),而不是本地时区
	Lmsgprefix                    // 64 默认前缀是放在行首的,这个标记为把前缀prefix放到消息message之前
	LstdFlags     = Ldate | Ltime // 3 initial values for the standard logger  位或,二进制的01 | 10 = 11 =》十进制3
)

上表列出的方法底层都使用std.Output输出日志内容。而std本质上是使用了标准错误输出无前缀LstdFlags 标准标记的记录器Logger实例

std使用
// 使用缺省Logger
log.Print("abcde\n")
log.Printf("%s\n","abcd")
log.Println("abcd")

log.Fatal("xyz") // 等价于 log.Print("xyz");os.Exit(1)

log.Panic("xyz") // 等价于 log.Print("xyz");panic()
自定义Logger

Go源码

// multiple goroutines; it guarantees to serialize access to the Writer.
type Logger struct {
	mu        sync.Mutex  // ensures atomic writes; protects the following fields
	prefix    string      // prefix on each line to identify the logger (but see Lmsgprefix)
	flag      int         // properties
	out       io.Writer   // destination for output
	buf       []byte      // for accumulating text to write
	isDiscard atomic.Bool // whether out == io.Discard
}

// New creates a new Logger. The out variable sets the
// destination to which log data will be written.
// The prefix appears at the beginning of each generated log line, or
// after the log header if the Lmsgprefix flag is provided.
// The flag argument defines the logging properties.
func New(out io.Writer, prefix string, flag int) *Logger {
	l := &Logger{out: out, prefix: prefix, flag: flag}
	if out == io.Discard {
		l.isDiscard.Store(true)
	}
	return l
}

如果觉得缺省Logger std不满意,可以New构建一个自定义Logger并指定前缀、Flags。

package main

import (
	"log"
	"os"
)

func main() {
	log.Println("这是缺省的logger")

        
	infologger := log.New(os.Stderr, "Info:", 3|log.Lmsgprefix)
	infologger.Print("这是自定义的普通消息")

	errLogger := log.New(os.Stderr, "Error:", log.LstdFlags)
	errLogger.Fatalln("这是一个自定义的错误消息")

}

log.New 参数详解:

  • 1、out io.Writer :er结尾说明是一个接口,out应该有自己的类型,同时其类型又实现了Writer的所有方法,所以out也可以看做为io.Writer类型的实例,out主要用于写入设备
  • 2、prefix string :前缀,用于在日志前面增加一串字符串,可以通过flag参数设置为log.Lmsgprefix将它设置为在message前面
  • 3、flag int :确定Logger的工作方式如下:
// 源码41行左右
//	2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
const (  // 可以做位与、为或
	Ldate         = 1 << iota     // 1 当前时区日期: 2009/01/23
	Ltime                         // 2 当前时区时间: 01:23:23
	Lmicroseconds                 // 4 微秒: 01:23:23.123123.  assumes Ltime.
	Llongfile                     // 8 绝对路径和行号: /a/b/c/d.go:23
	Lshortfile                    // 16 文件名和行号: d.go:23. overrides Llongfile
	LUTC                          // 32 使用UTC(GMT),而不是本地时区
	Lmsgprefix                    // 64 默认前缀是放在行首的,这个标记为把前缀prefix放到消息message之前
	LstdFlags     = Ldate | Ltime // 3 initial values for the standard logger  位或,二进制的01 | 10 = 11 =》十进制3
)

image

写日志文件

New方法签名func New(out io.Writer, prefix string, flag int) *Logger 中out参数提供Writer接口即可,那么就可以提供一个可写文件对象

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
         // os.Open为只读打开,OpenFile按照指定方式打开文件
	file, err := os.OpenFile("C:\\Users\\Administrator\\Desktop\\test.txt",
         os.O_APPEND|os.O_WRONLY, // 追加| 只写
         777)  // 权限为777
	if err != nil {
		fmt.Println(err)
	}
        defer file.Close() // 使用结束关闭文件句柄
	logger := log.New(file, "Info:", 3)
	logger.Print("这是一条测试的日志")

}

ZeroLog

log模块太简陋了,实际使用并不方便。

  • logrus有日志级别、Hook机制、日志输出格式,很好用
  • zap是Uber的开源高性能日志库
  • zerolog更注重开发体验,高性能、有日志级别、链式API,json格式记录,号称0内存分配

官网: https://zerolog.io/

安装

Text
go get -u github.com/rs/zerolog/log
缺省Logger
package main

import (
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"os"
	"time"
)

func main() {

	zerolog.TimeFieldFormat = "2006/01/02 15:04:05 -0700"
	log.Print("这是zerolog的默认消息") // 使用全局缺省logger
	// 输出{"level":"debug","time":"2023/07/23 13:55:05 +0800","message":"这是zerolog的默认消息"}
       
}

log.Print 产生debug级别的消息,zerolog源码117行左右

// Print sends a log event using debug level and no extra field.
// Arguments are handled in the manner of fmt.Print.
func Print(v ...interface{}) {
	Logger.Debug().CallerSkipFrame(1).Msg(fmt.Sprint(v...))
}

// 源码13行左右,定义了一个全局导出的缺省Logger,使用链式调用,缺省Logger使用标准错误输出
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()

log.Print()、log.Printf()方法使用方式和标准库log模块类似

级别

zerolog提供以下级别,tracelevel级别最低-1

// zerolog源码112行
type Level int8

const (
	// DebugLevel defines debug log level.
	DebugLevel Level = iota     0
	// InfoLevel defines info log level.
	InfoLevel    1
	// WarnLevel defines warn log level.
	WarnLevel    2
	// ErrorLevel defines error log level.
	ErrorLevel   3
	// FatalLevel defines fatal log level.
	FatalLevel   4
	// PanicLevel defines panic log level.
	PanicLevel   5
	// NoLevel defines an absent log level.
	NoLevel      6
	// Disabled disables the logger.
	Disabled     7

	// TraceLevel defines trace log level.
	TraceLevel Level = -1
	// Values less than TraceLevel are handled as numbers.
)

级别有

  • gLevel全局级别
    • zerolog.SetGlobalLevel(级别数字或常量) 来设置全局级别
    • zerolog.GlobalLevel() 获取当前全局级别
  • 每个Logger的级别
  • 消息的级别
package main

import (
	"fmt"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	fmt.Println("全局级别的glevel为:", zerolog.GlobalLevel())
	fmt.Println("缺省logger的级别为:", log.Logger.GetLevel())
	log1 := log.Level(zerolog.WarnLevel).With().Logger()
	fmt.Println("log1的级别为", log1.GetLevel())

	fmt.Println("~~~~~~~~~~~~~~~~")
	log.Trace().Msg("缺省logger输出trace级别消息") // 可以输出
	log.Info().Msg("缺省logger输出info级别消息") // 可以输出
	log.Warn().Msg("缺省logger输出warn级别消息") // 可以输出
	log.Error().Msg("缺省logger输出error级别消息") // 可以输出
	//log.Fatal().Msg("缺省logger输出fatal级别消息") 
	//log.Panic().Msg("缺省logger输出panic级别消息")
	
	log1.Debug().Msg("log1输出debug级别消息") // 输出不了,因为log1的消息级别为debug,小于log1的级别warn
	log1.Error().Msg("log1输出error级别消息") // 可以输出,因为log1的消息级别为error,大于log1的级别warn
}

可以看到,使用缺省logger,全部可以输出日志消息,而log1的24行可以输出,23行不能输出,因为有消息级别和Logger级别

log1的级别为warn,而log1.Debug()输出的消息级别为debug级别0,消息级别小于log1级别,所以消息不能输出。log1.Error()产生error消息,消息级别 大于等于log1级别的warn,因此可以输出消息。

而缺省的Logger级别为trace,任何消息级别都大于等于缺省的logger的级别,因此都可以输出

下面可以调整全局级别看一看

package main

import (
	"fmt"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	zerolog.SetGlobalLevel(zerolog.ErrorLevel)
	fmt.Println("全局级别的glevel为:", zerolog.GlobalLevel())

	fmt.Println("缺省logger的级别为:", log.Logger.GetLevel())
	log1 := log.Level(zerolog.WarnLevel).With().Logger() // 创建一个子logger
	fmt.Println("log1的级别为", log1.GetLevel())

	fmt.Println("~~~~~~~~~~~~~~~~")
	log.Trace().Msg("缺省logger输出trace级别消息") // 不能输出
	log.Info().Msg("缺省logger输出info级别消息")   // 不能输出
	log.Warn().Msg("缺省logger输出warn级别消息")   // 不能输出
	log.Error().Msg("缺省logger输出error级别消息") // 可以输出
	//log.Fatal().Msg("缺省logger输出fatal级别消息")
	//log.Panic().Msg("缺省logger输出panic级别消息")

	log1.Warn().Msg("log1输出warn级别消息") // 输出不了,因为log1的消息级别为debug,虽然等于log1的级别warn,但是小于全局的glevel的error级别,所以还是输出不了
	log1.Error().Msg("log1输出error级别消息") // 可以输出,因为log1的消息级别为error,大于log1的级别warn,且等于全局的glevel的error级别,所以可以输出
}
// 输出
全局级别的glevel为: error
缺省logger的级别为: trace
log1的级别为 warn
~~~~~~~~~~~~~~~~
{"level":"error","time":"2023-07-23T14:22:39+08:00","message":"缺省logger输出error级别消息"}
{"level":"error","time":"2023-07-23T14:22:39+08:00","message":"log1输出error级别消息"}

缺省logger和log1都只有error级别的输出,说明将glevel调整到error级别后,所有logger输出消息必须大于等于glevel。

特别注意:zerolog.SetGlobalLevel()设置的是全局变量gLevel,它影响所有的Logger

所以我们得出以下公式:

日志消息是否能够输出,应当满足下面的要求:消息级别 >= max(全局glevel,当前logger的level)

zerolog.SetGlobalLevel(zerolog.Disabled)
// zerolog.Disabled 为7,没有消息级别可以大于等于7,相当于禁用所有的Logger,自然不能够输出。
上下文

zerolog是以Json对象格式输出的,还可以自定义一些键值对字段增加到上下文中以输出

package main

import (
	"fmt"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	var all = []Person{}
	for i := 0; i < 10; i++ {
		all = append(all, Person{Name: fmt.Sprintf("name%d", i), Age: i + 1})
	}
	zerolog.SetGlobalLevel(zerolog.InfoLevel)
	log.Info().Bool("success", true).Str("Reason", "File Not Found").Msg("文件没有找到")
	log.Info().Str("name", "Tom").Floats32("Scores", []float32{62.3, 99.3, 100}).Send()
	fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~")
	log.Info().Any("data", all).Send()
	log.Info().Interface("data", all).Msg("测试啊啊啊")

}

错误日志
package main

import (
	"errors"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 自定义time字段时间的格式,TimeFormatUnix时间戳
	zerolog.ErrorFieldName = "err"                   // 修改日志Json中的缺省字段名error

	// 错误日志
	err := errors.New("自定义错误")
	log.Error().Err(err).// err 字段,错误消息内容
	Send() // 有错误消息了,message可以省略
	//log.Fatal().Err(err).Send()
	log.Info().Err(nil).Send()

}
// 输出
{"level":"error","err":"自定义错误","time":1690095612}
{"level":"info","time":1690095612}
全局Logger
// 全局Logger定义如下
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()

可以覆盖全局Logger

log.Logger = log.With().Str("time", "2020-01-02 15:04").Logger()
log.Info().Send()
自定义Logger
	zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
	logger := log.With().
		Str("School", "magedu").
		Caller(). // 增加日志调用的位置信息字段
		Logger()  // 返回logger
	logger.Info().Send()
	log.Info().Send()

// 输出
{"level":"info","School":"magedu","time":"2023-07-23 15:05:23","caller":"C:/Users/Administrator/GolandProjects/awesomeProject/hello.go:29"}
{"level":"info","time":"2023-07-23 15:05:23"}
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
logger := zerolog.New(os.Stderr).With().Caller().Logger().Level(zerolog.InfoLevel)
fmt.Println(logger.GetLevel())
logger.Info().Send()
logger.Error().Send()
log.Info().Send()

写日志文件

package main

import (
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"os"
)
func main() {
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 自定义time字段时间的格式,TimeFormatUnix时间戳
	zerolog.ErrorFieldName = "err"                   // 修改日志Json中的缺省字段名error
	file, err := os.OpenFile("fuck.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
	if err != nil {
		log.Fatal().Err(err).Msg("open file error")
	}
	defer file.Close()
	levelwrite := zerolog.MultiLevelWriter(file, os.Stderr)
	logger := zerolog.New(levelwrite).With().Timestamp().Logger()
	logger.Info().Msg("日志写入到文件同时在控制台打印")
}

如果只输出到文件可以使用zerolog.New(f).With().Timestamp().Logger()