一般而言日志可分为事件日志和消息日志两种
- 事件日志记录发生在系统运行过程中的事件,用来审计操作、诊断问题等。对理解复杂系统的运行非常关键。
- 消息日志被应用在比如即时通信中,用来记录来往的消息。
标准的日志定义在IETF中,会记录时间、地点、参与者、起因、简要经过等信息。
日志记录一般格式:[时间][日志级别][地点][参与者][事件][起因]
对于系统日志一般还会定义严重性等级(Severity Level)用来标识该条日志记录的紧要程度
紧要程度 | 日志级别 |
---|---|
0 | EMERGENCY |
1 | ALERT |
2 | CREITICAL |
3 | ERROR |
4 | WARNING |
5 | NOTICE |
6 | INFORMATION |
7 | DEBUG |
log
- Go内置了标准的
log
日志库 -
log
支持并发操作即协程安装 - 源码地址 https://golang.org/pkg/log/
package main
import "log"
type User struct {
Id int
Name string
}
func main() {
user := User{Id: 1, Name: "admin"}
log.Printf("id = %v, name = %v", user.Id, user.Name)
}
2021/03/31 22:18:33 id = 1, name = admin
Golang默认的日志记录分为两部分,前面是事件发生的时间,后面是事件记录。
日志输出
-
log
日志库默认会输出日志到标准错误stderr
-
log
日志库会将每条日志前自动添加上日期和时间 - 若日志不是以换行符为结尾,则会自动补上换行符。
log
日志库提供了三组函数用于输出日志
输出 | 格式化 | 换行 | 描述 |
---|---|---|---|
log.Print | log.Printf | log.Println | 正常输出日志 |
log.Fatal | log.Fatalf | log.Fatalln | 输出日志后退出程序 |
log.Panic | log.Panicf | log.Panicln | 输出日志与相关调用信息后退出程序 |
-
log.Fatal()
会记录FATAL
级别错误日志,并通过调用os.Exit(1)
来结束程序 -
log.Panic()
会记录PANIC
级别错误日志,当写入消息消息后会抛出一个panic
。
日志记录器 log.Logger
- Logger是对
log
的简单封装,使用Logger可以使日志记录更加便捷。
type Logger struct {
mu sync.Mutex // ensures atomic writes; protects the following fields
prefix string // 日志行前缀
flag int // 日志打印格式标志,用于指定每行日志的打印格式。
out io.Writer // 日志输出位置,需实现io.Writer接口。
buf []byte // 日志内容
}
字段 | 描述 |
---|---|
prefix | 日志行前缀 |
flag | 日志打印格式标志,用于指定每行日志的打印格式。 |
out | 日志输出位置,需实现io.Writer接口。 |
buf | 日志内容 |
Go标准库中的Logger最大的优点在于简单,用户只需要设置任何io.Writer
作为日志记录输出并向其发送想要写入的日志即可,与之相对的劣势也是由于简单。
- 基本信息的日志级别中只有
log.Print()
不支持类似INFO/DEBUG等级别 - 对于错误日志有
log.Fatal()
和log.Panic()
,缺少一个ERROR级别,需要在不抛出panic
或退出程序的情况下记录错误。 - 缺乏日志格式化能力
- 缺乏日志切割能力
创建日志记录器 log.New
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
参数 | 类型 | 描述 |
---|---|---|
out | io.Writer | 设置日志数据接入的目的地,需实现io.Writer 接口。 |
prefix | string | 每行日志前缀字符串 |
flag | int | 设置日志记录选项 |
例如:以系统标准输出(屏幕)作为日志记录器输出位置,以[DEBUG]
作为每行日志前缀,同时自动添加时间。
logger := log.New(os.Stdout, "[DEBUG] ", log.Ldate|log.Ltime|log.Lshortfile)
user := User{Id: 1, Name: "admin"}
logger.Printf("id = %v, name = %v", user.Id, user.Name)
[DEBUG] 2021/03/31 23:00:03 main.go:17: id = 1, name = admin
日志输出位置 out
-
os.Stdin
、os.Stdout
、os.Stderr
表示已经打开的文件,分别指向标准输入、标准输出、标准错误的文件描述符。这三个变量都被声明为*os.File
类型的指针,而os.File
类型实现了io.Writer
接口。
package os
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
func NewFile(fd uintptr, name string) *File {
h := syscall.Handle(fd)
if h == syscall.InvalidHandle {
return nil
}
return newFile(h, name, "file")
}
type File struct {
*file // os specific
}
-
ioutil.Discard
是一个io.Writer
接口,调用其Write()
方法将不做任何操作并始终返回成功,当某个等级的日志不重要时,可使用ioutil.Discard
来禁用。
package ioutil
var Discard io.Writer = io.Discard
-
io.MultiWriter()
函数调用返回的是一个io.Writer
接口类型值,入参是一个变参函数可接受任意数量实现io.Writer
接口的值,返回的值会将所有传入的值绑在一起。当对返回值写入时,会向所有绑在一起的io.Writer
值做写入。因此可以同时向多个Writer
做输出。
package io
func MultiWriter(writers ...Writer) Writer
设置输出端 log.SetOutput
func (l *Logger) SetOutput(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.out = w
}
- 默认输出端为
os.stdout
系统标准输出 - 设置输出到指定文件
file,err := os.OpenFile(*logFileName, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
log.SetOutput(file)
- 设置同时将日志输出到多个位置
file,err := os.OpenFile(*logFileName, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
writer := io.MultiWriter(file, os.Stderr)
log.SetOutput(writer)
- 禁止输出
ioutil.Discard
是一个io.Writer
,对其进行的Write
调用都将无条件成功,类似于黑洞。
log.SetOutput(ioutil.Discard)
例如:自定义日志输出位置
func init(){
dirpath := "./tmp/log/"
filename := fmt.Sprintf("%s.log", time.Now().Format("20060102"))
if _,err := os.Stat(dirpath); os.IsNotExist(err) {
err := os.MkdirAll(dirpath, os.ModePerm)
if err!=nil {
log.Fatalln(err)
return
}
}
filePath := path.Join(dirpath, filename)
fh,err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
if err!=nil {
log.Fatalln(err)
return
}
log.SetOutput(fh)
}
func main(){
log.Println("log tester\n")
}
设置前缀 log.SetPrefix
- 调用
log.SetPrefix()
会为每条日志文本前添加一个前缀
log.SetPrefix("[ERROR] ")
user := User{Id: 1, Name: "admin"}
log.Printf("id = %v, name = %v", user.Id, user.Name)
[ERROR] 2021/03/31 22:32:31 id = 1, name = admin
- 设置前缀文本颜色
//设置颜色
const (
red = uint8(91 + iota)
greeen
yellow
blue
magenta
)
//color 为文本设置颜色
func color(str string, color uint8) string {
text := fmt.Sprintf("\x1b[%dm%s\x1b[0m", color, str)
fmt.Printf("%v\n", text)
return text
}
日志格式标志 flag
log
包定义了一些日志格式标志的常量
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
Lmsgprefix // move the "prefix" from the beginning of the line to before the message
LstdFlags = Ldate | Ltime // initial values for the standard logger
)
格式标志 | 常量值 | 描述 |
---|---|---|
log.Ldate | 1 | 输出当地时区日期 |
log.Ltime | 2 | 输出当地时区时间 |
log.Lmicroseconds | 3 | 输出毫秒级时间,会覆盖Ltime。 |
log.Llongfile | 4 | 输出长文件:全路径+执行文件+行数 |
log.Lshortfile | 5 | 输出段文件:执行文件名+行数,会覆盖Llongfile。 |
log.LUTC | 6 | 若已设置Ldate 或Ltime 则输出UTC时间,而非当地时区。 |
log.LstdFlags | 8 | 默认值,等价于 log.Ldate|log.Ltime
|
可在log.New()
创建日志记录器时指定,也可使用Logger.setFlat()
方法动态指定。
设置选项 log.SetFlags
- 调用
log.SetFlags
为输出的日志添加选项,可同时设置多个,多个|
分割。 - 设置选项可为每条日志输出的文本前增加额外信息
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Llongfile | log.LUTC)
log.SetPrefix("[ERROR] ")
user := User{Id: 1, Name: "admin"}
log.Printf("id = %v, name = %v", user.Id, user.Name)
[ERROR] 2021/03/31 14:31:21.368463 D:/go/project/example/main.go:14: id = 1, name = admin
- 取消日志格式化输出,可设置为0。
log.SetFlags(0)
协程安全
日志记录器默认是多goroutine
安全的,这也就意味着多个goroutine
可以同时调用来自同一个日志记录器的输出函数,而不会发生写冲突。
例如:定制不同日志等级的日志记录器
日志等级 | 记录信息 | 输出位置 |
---|---|---|
Trace | 记录所有信息 | 禁止输出 |
Info | 记录重要信息 | 输出到标准输出屏幕上 |
Warn | 记录警告信息 | 输出到标准输出屏幕上 |
Error | 记录错误信息 | 输出到日志文件中 |
$ mkdir logx && cd logx
$ vim logx.log
package logx
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"time"
)
var (
Trace *log.Logger // 记录所有日志
Info *log.Logger // 记录重要日志
Warn *log.Logger // 记录警告信息
Error *log.Logger // 记录错误信息
)
func init() {
//禁用Trace,不输出任何信息。
Trace = log.New(ioutil.Discard, "[TRACE] ", log.Ldate|log.Ltime|log.Lshortfile)
//信息输出到屏幕
Info = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime|log.Lshortfile)
//警告输出到屏幕
Warn = log.New(os.Stdout, "[WARN] ", log.Ldate|log.Ltime|log.Lshortfile)
//错误输出到文件
dirpath := "./tmp/log/"
filename := fmt.Sprintf("%s.log", time.Now().Format("20060102"))
file, err := MkDirFile(dirpath, filename, os.ModePerm)
if err != nil {
log.Fatalln(err)
}
Error = log.New(io.MultiWriter(file, os.Stderr), "[ERROR] ", log.Ldate|log.Ltime|log.Llongfile)
}
func MkDirFile(dirpath, filename string, perm os.FileMode) (*os.File, error) {
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
err := os.MkdirAll(dirpath, perm)
if err != nil {
return nil, err
}
}
filePath := path.Join(dirpath, filename)
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_APPEND, perm)
if err != nil {
return nil, err
}
return f, nil
}
运行测试
func main() {
logx.Trace.Println("trace test")
logx.Info.Println("info test")
logx.Warn.Println("warn test")
logx.Error.Println("error test")
}
例如:将错误记录到日志同时打印到文件
package main
import (
"io"
"log"
"os"
)
var (
Error *log.Logger
)
func init() {
filename := "error.log"
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln(err)
}
out := io.MultiWriter(file, os.Stderr)
Error = log.New(out, "[ERROR] ", log.Ldate|log.Ltime|log.Llongfile)
}
func main() {
user := &struct {
Id int
Name string
}{
Id: 1,
Name: "admin",
}
Error.Printf("id = %v, name = %v", user.Id, user.Name)
}
创建文件
file,err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
选项 | 描述 |
---|---|
os.O_CREATE | 若文件不存在则创建新文件 |
os.O_WRONLY | 打开文件的读写权限 |
os.O_APPEND | 写入数据时采用追加而非覆盖的方式 |
例如:
- 指定日志文件保存路径,若目录不存在则创建。
- 向指定目录的日志文件内写入日志,同时打印到标准输出。
package main
import (
"io"
"log"
"os"
)
var (
Error *log.Logger
)
//MakeDir 创建目录
func MakeDir(dir string) (bool, error) {
//判断文件是否存在
_, err := os.Stat(dir)
if err == nil {
return true, nil
}
//递归创建文件夹
err = os.MkdirAll(dir, 0755)
if err != nil {
return false, err
}
return true, nil
}
func init() {
dir := "./runtime/log/"
filename := "error.log"
filepath := dir + filename
ok, err := MakeDir(dir)
if err != nil && !ok {
panic(err)
}
file, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln(err)
}
out := io.MultiWriter(file, os.Stderr)
Error = log.New(out, "[ERROR] ", log.Ldate|log.Ltime|log.Llongfile)
}
func main() {
user := &struct {
Id int
Name string
}{
Id: 1,
Name: "admin",
}
Error.Printf("id = %v, name = %v", user.Id, user.Name)
}
例如:根据日志不同紧急程度输出日志
紧急程度 | 级别名称 | 级别方法 | 日志前缀 | 颜色 |
---|---|---|---|---|
0 | fatal | Fatal/Fatalf | [FATAL] | 红色背景色 |
1 | error | Error/Errorf | [ERROR] | 红色前景色 |
2 | warn | Warn/Warnf/Warningf | [WARN] | 杨红背景色 |
3 | info | Info/Infof | [INFO] | 请求前景色 |
4 | debug | Debug/Debugf | [DEBUG] | 黄色前景色 |
$ vim ./log/log.go
package log
import (
"fmt"
"io/ioutil"
"log"
"os"
"sync"
)
var (
mutex sync.Mutex
infoLogger *log.Logger = log.New(os.Stdout, color("[INFO] ", blue), log.LstdFlags|log.Lshortfile)
errorLogger *log.Logger = log.New(os.Stdout, color("[ERROR] ", red), log.LstdFlags|log.Lshortfile)
loggers []*log.Logger = []*log.Logger{errorLogger, infoLogger}
)
//设置日志方法
var (
Info = infoLogger.Println
Infof = infoLogger.Printf
Error = errorLogger.Println
Errorf = errorLogger.Printf
)
//设置颜色
const (
red = uint8(91 + iota)
greeen
yellow
blue
magenta
)
//color 为文本设置颜色
func color(str string, color uint8) string {
text := fmt.Sprintf("\x1b[%dm%s\x1b[0m", color, str)
return text
}
//设置日志级别
const (
InfoLevel = iota
ErrorLevel
Disabled
)
//SetLevel 设置日志级别
func SetLevel(level int) {
//添加互斥锁
mutex.Lock()
defer mutex.Unlock()
//设置日志记录器输出位置
for _, logger := range loggers {
logger.SetOutput(os.Stdout) //输出定向到标准输出
}
//设置自定义输出位置
if level > InfoLevel {
infoLogger.SetOutput(ioutil.Discard) //输出会被定向到 ioutil.Discard 不打印该日志
} else if level > ErrorLevel {
errorLogger.SetOutput(ioutil.Discard) //输出会被定向到 ioutil.Discard 不打印该日志
}
}
网友评论