Go log 日志
日志是 程序调试 程序问题分析 常用的方法分析日志 的信息 分析挖掘问题
使用fmt.Println系列函数也可 达到目的 把信息输出到终端或者其他文件中
不过fmt.Println系列函数 输出的 比较简单 没有时间 没有源代码的行数等 排查问题不便
Go语言 提供了标准的log包 跟踪日志的记录
func main() {
log.Println("飞雪","flysnow")
}
2017/04/29 13:18:44 飞雪 flysnow
日志包log 提供 可定制化的配制 定制日志的抬头信息
func init(){
log.SetFlags(log.Ldate|log.Lshortfile)
}
自定义日志的抬头信息 时间+文件名+源代码所在行号 是log.Ldate|log.Lshortfile 中间是 位运算符| 通过函数log.SetFlags 设置
2017/04/29 main.go:10: 飞雪无情 flysnow
log包选项常量
提供了 可定义的选项常量const (
Ldate = 1 << iota //日期示例: 2009/01/23
Ltime //时间示例: 01:23:23
Lmicroseconds //毫秒示例: 01:23:23.123123.
Llongfile //绝对路径和行号: /a/b/c/d.go:23
Lshortfile //文件和行号: d.go:23.
LUTC //日期时间转为0时区的
LstdFlags = Ldate | Ltime //Go提供的标准抬头信息
)
抬头信息 有日期 时间 毫秒时间 绝对路径 行号 文件名 等
设置了Lmicroseconds 那么Ltime就不生效
设置了Lshortfile 那Llongfile也不会生效
LUTC 特殊 如果 配置了 时间标签 LUTC的话 就会把输出的日期时间转为0时区的日期时间显示
log.SetFlags(log.Ldate|log.Ltime |log.LUTC) 对东八区的时间 会减去8个小时
LstdFlags表示标准 日志抬头信息 默认的 包含日期和具体时间
log包 业务区分
Go语言 设置日志的前缀 比如 用户中心系统的日志func init(){
log.SetPrefix("【UserCenter】")
log.SetFlags(log.LstdFlags | log.Lshortfile |log.LUTC)
}
通过 log.SetPrefix 指定输出日志的前缀 【UserCenter】 日志的打印输出 标记出 日志是属于哪些业务
【UserCenter】2017/04/29 05:53:26 main.go:11: 飞雪 : flysnow
log包 Print系列的函数 Fatal以及Panic系列函数 Fatal表示程序遇到了致命的错误 要退出
Fatal记录日志后程序退出 Fatal相当于先调用Print打印日志 然后再调用os.Exit(1)退出程序
Panic系列的函数也一样 先用Print记录日志 后调用panic()抛出恐慌 除非使用recover()函数 否则程序打印错误堆栈信息 程序终止
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...))
}
func Fatalln(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...))
os.Exit(1)
}
func Panicln(v ...interface{}) {
s := fmt.Sprintln(v...)
std.Output(2, s)
panic(s)
}
log包实现原理
日志包log 关键的输出日志 在于std.Output方法func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
var std = New(os.Stderr, "", LstdFlags)
std是*Logger 通过 log.New 函数创建 默认输出到os.Stderr设备 前缀为空 日志抬头信息为标准抬头LstdFlags
os.Stderr 对应UNIX 标准错误警告信息的输出设备 被作为默认的日志输出目的地
标准输出设备 os.Stdout
标准输入设备os.Stdin
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
UNIX的标准的三种设备 分别用于输入 输出和警告错误信息 理解了os.Stderr 看下Logger 结构体 日志的信息和操作 都是通过这个Logger操作的
type Logger struct {
mu sync.Mutex // ensures atomic writes; protects the following fields
prefix string // prefix to write at beginning of each line
flag int // properties
out io.Writer // destination for output
buf []byte // for accumulating text to write
}
字段mu是个互斥锁 主要是是保证这个日志记录器Logger在多goroutine下安全
字段prefix是每一行日志的前缀
字段flag是日志抬头信息
字段out是日志输出的目的地 默认情况下是os.Stderr
字段buf是一次日志输出文本缓冲 最终会被写到out里
func (l *Logger) Output(calldepth int, s string) error {
now := time.Now() // get this early.
var file string
var line int
l.mu.Lock() //加锁 保证多goroutine下的安全
defer l.mu.Unlock()
if l.flag&(Lshortfile|Llongfile) != 0 {//如果配置了获取文件和行号的话
l.mu.Unlock() //因为runtime.Caller代价比较大 先不加锁
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
l.mu.Lock()//获取到行号等信息后 再加锁 保证安全
}
//把日志信息和设置的日志抬头进行拼接
l.buf = l.buf[:0]
l.formatHeader(&l.buf, now, file, line)
l.buf = append(l.buf, s...)
if len(s) == 0 || s[len(s)-1] != '\\\\n' {
l.buf = append(l.buf, '\\\\n')
}
//输出拼接好的缓冲buf里的日志信息到目的地
_, err := l.out.Write(l.buf)
return err
}
log包互斥锁
为多goroutine安全 在获取调用堆栈信息时 先解锁 因为这个过程比较重 获取到文件 行号等信息后 加互斥锁保证安全formatHeader 方法 格式化 日志抬头信息 存储在buf 缓冲中 再把日志信息拼接到缓冲buf 后 为一次log日志输出追加一个换行符
每次日志输出一行
有了最终的日志信息buf 写到输出的目的地out里就可以了 是实现 io.Writer 接口的类型 只要实现了这个接口 都可以当作输出目的地
func (l *Logger) SetOutput(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.out = w
}
log包的 SetOutput 函数 可以设置输出目的地 介绍下 runtime.Caller 它可以获取运行时方法的调用信息
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
参数skip表示跳过栈帧数 0表示不跳过 也就是runtime.Caller的调用者 1的话就是再向上一层 表示调用者的调用者
log日志包里使用的是2 也就是表示 在源代码中调用log.Print、log.Fatal和log.Panic这些函数的调用者
以main函数调用log.Println为例 是main->log.Println->*Logger.Output->runtime.Caller 方法调用栈 所以这时候 skip的值分别代表
0表示*Logger.Output中调用runtime.Caller的源代码文件和行号
1表示log.Println中调用*Logger.Output的源代码文件和行号
2表示main中调用log.Println的源代码文件和行号
所以这也是log包里的这个 skip 的值为什么一直是2的原因
log日志定制
日志记录 根本在日志记录器Logger 定制日志就是创建不同的Loggervar (
Info *log.Logger
Warning *log.Logger
Error * log.Logger
)
func init(){
errFile,err:=os.OpenFile("errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)
if err!=nil{
log.Fatalln("打开日志文件失败:",err)
}
Info = log.New(os.Stdout,"Info:",log.Ldate | log.Ltime | log.Lshortfile)
Warning = log.New(os.Stdout,"Warning:",log.Ldate | log.Ltime | log.Lshortfile)
Error = log.New(io.MultiWriter(os.Stderr,errFile),"Error:",log.Ldate | log.Ltime | log.Lshortfile)
}
func main() {
Info.Println("飞雪 :","flysnow")
Warning.Printf("飞雪 :%s\\\\n","flysnow")
Error.Println("欢迎关注留言")
}
根据日志级别定义了三种不同的Logger
Info Warning Error 用于不同级别日志的输出
这三种日志记录器都是使用log.New函数进行创建
创建Logger时 Info和Warning比较正常 Error采用了多个目的地输出 可同时把错误日志输出到os.Stderr以及创建的errors.log文件中
io.MultiWriter 函数可以包装多个io.Writer为一个io.Writer 就可以达到同时对多个io.Writer输出日志的目的
io.MultiWriter的实现也很简单 定义一个类型实现io.Writer 然后在实现的Write方法里循环调用要包装的多个Writer接口的Write方法即可
func (t *multiWriter) Write(p []byte) (n int, err error) {
for _, w := range t.writers {
n, err = w.Write(p)
if err != nil {
return
}
if n != len(p) {
err = ErrShortWrite
return
}
}
return len(p), nil
}
第三方log框架
多个 Logger 区分不同的日志级别 麻烦 可使用第三方的log框架通过不同级别的方法来记录不同级别的日志 设置记录日志的级别等
log包 Logger
package main;
import (
"log"
"os"
"time"
"fmt"
)
func main() {
logFile, err := os.Create("./" + time.Now().Format("20060102") + ".txt");//创建输出日志文件
if err != nil {
fmt.Println(err);
}
//创建Logger //参数1:日志写入目的地 //参数2:每条日志的前缀 //参数3:日志属性
loger := log.New(logFile, "test_", log.Ldate|log.Ltime|log.Lshortfile);
fmt.Println(loger.Flags());//Flags返回Logger的输出选项
loger.SetFlags(log.Ldate | log.Ltime | log.Lshortfile); // SetFlags 设置输出选项
fmt.Println(loger.Prefix());//返回输出前缀
loger.SetPrefix("test_"); //设置输出前缀
loger.Output(2, "打印一条日志信息");//输出一条日志
loger.Printf("第%d行 内容:%s", 11, "我是错误");//格式化输出日志
loger.Fatal("我是错误"); //等价于print();os.Exit(1);
loger.Panic("我是错误"); //等价于print();panic();
//log的导出函数 //导出函数基于std,std是标准错误输出
//var std = New(os.Stderr, "", LstdFlags)
fmt.Println(log.Flags());//获取输出项
fmt.Printf(log.Prefix()); //获取前缀
log.Output(2, "输出内容"); //输出内容
log.Printf("第%d行 内容:%s", 22, "我是错误"); //格式化输出
log.Fatal("我是错误");
log.Panic("我是错误");
}
尊贵的董事大人
英文标题不为空时 视为本栏投稿
需要关键字 描述 英文标题