Go中的日志及第三方日志包logrus
有别的语言使用基础的同学工作中都会接触到日志的使用,Go中自然也有log相关的实现。Go log模块主要提供了3类接口,分别是 “Print 、Panic 、Fatal ”,对每一类接口其提供了三种调用方式,分别是 “Xxxx 、Xxxxln 、Xxxxf”,基本和fmt中的相关函数类似。
1. Go中的log包
1. 基本使用
- log.Print:打印日志,和fmt包没什么区别,只是加上了上面的日志格式
- log.Fatal :会先将日志内容打印到标准输出,接着调用系统的os.exit(1) 接口,退出程序并返回状态 1 。但是有一点需要注意,由于是直接调用系统接口退出,defer函数不会被调用
- log.Panic:该函数把日志内容刷到标准错误后调用 panic 函数
log 结构的定义如下:
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
}
可见在结构体中有sync.Mutex类型字段,所以log中所有的操作都是支持并发的。
下面看一下这三种log打印的用法:
package main
import (
"log"
)
func main() {
log.Print("我就是一条日志")
log.Printf("%s,","谁说我是日志了,我是错误")
log.Panic("哈哈,我好痛")
}
输出:
2019/05/23 22:14:36 我就是一条日志
2019/05/23 22:14:36 谁说我是日志了,我是错误,
2019/05/23 22:14:36 哈哈,我好痛
panic: 哈哈,我好痛
goroutine 1 [running]:
log.Panic(0xc00007bf78, 0x1, 0x1)
D:/soft/go/src/log/log.go:333 +0xb3
main.main()
E:/go_path/src/webDemo/demo.go:12 +0xfd
使用非常简单,可以看到log的默认输出带了时间,非常的方便。Panic方法在输出后调用了Panic方法,所以抛出了异常信息。上面的示例中没有演示Fatal方法,你可以试着把log.Fatal()放在程序的第一行,你会发现下面的代码都不会执行。因为上面说过,它在打印完日志之后会调用os.exit(1)方法,所以系统就退出了。
2. 定制打印参数
上面说到log打印的时候默认是自带时间的,那如果除了时间以外,我们还想要别的信息呢,当然log也是支持的。
SetFlags(flag int)方法提供了设置打印默认信息的能力,下面的字段是log中自带的支持的打印类型:
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
LstdFlags = Ldate | Ltime // initial values for the standard logger
这是log包定义的一些抬头信息,有日期、时间、毫秒时间、绝对路径和行号、文件名和行号等,在上面都有注释说明,这里需要注意的是:如果设置了Lmicroseconds,那么Ltime就不生效了;设置了Lshortfile, Llongfile也不会生效,大家自己可以测试一下。
LUTC比较特殊,如果我们配置了时间标签,那么如果设置了LUTC的话,就会把输出的日期时间转为0时区的日期时间显示。
最后一个LstdFlags表示标准的日志抬头信息,也就是默认的,包含日期和具体时间。
使用方法:
func init(){
log.SetFlags(log.Ldate|log.Lshortfile)
}
使用init方法,可以在main函数执行之前初始化代码。另外,虽然参数是int类型,但是上例中使用位运算符传递了多个常量为什么会被识别到底传了啥进去了呢。这是因为源码中去做解析的时候,也是根据不同的常量组合的位运算去判断你传了啥的。所以先看源码,你就可以大胆的传了。
package main
import (
"log"
)
func main() {
log.SetFlags(log.Ldate|log.Lshortfile)
log.Print("我就是一条日志")
log.Printf("%s,","谁说我是日志了,我是错误")
}
输出:
2019/05/23 demo.go:11: 我就是一条日志
2019/05/23 demo.go:12: 谁说我是日志了,我是错误,
3. 如何传自定义参数进日志
在Java开发中我们会有这样的日志需求:为了查日志更方便,我们需要在一个http请求或者rpc请求进来到结束的作用链中用一个唯一id将所有的日志串起来,这样可以在日志中搜索这个唯一id就能拿到这次请求的所有日志记录。
所以现在的任务是如何在Go的日志中去定义这样的一个id。Go中提供了这样的一个方法:SetPrefix(prefix string),通过log.SetPrefix可以指定输出日志的前缀。
package main
import (
uuid "github.com/satori/go.uuid"
"log"
)
func main() {
uuids, _ := uuid.NewV1()
log.SetPrefix(uuids.String() +" ")
log.SetFlags(log.Ldate|log.Lshortfile)
log.Print("我就是一条日志")
log.Printf("%s,","谁说我是日志了,我是错误")
}
输出:
1791d770-7d6a-11e9-b2ee-00fffa4e4d0c 2019/05/23 demo.go:13: 我就是一条日志
1791d770-7d6a-11e9-b2ee-00fffa4e4d0c 2019/05/23 demo.go:14: 谁说我是日志了,我是错误,
4. log 输出的底层实现
从源码中我们可以看到,无论是Print,Panic,还是Fatal他们都是使用std.Output(calldepth int, s string)方法。std的定义如下:
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
var std = New(os.Stderr, "", LstdFlags)
即每一次调用log的时候都会去创建一个Logger对象。另外New中传入的第一个参数是os.Stderr,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")
)
前两种分别用于输入、输出和警告错误信息。
我们再来看一下,所有的输出都会调用的方法:std.Output(calldepth int, s string)
func (l *Logger) Output(calldepth int, s string) error {
now := time.Now()
var file string
var line int
//加锁,保证多goroutine下的安全
l.mu.Lock()
defer l.mu.Unlock()
//如果配置了获取文件和行号的话
if l.flag&(Lshortfile|Llongfile) != 0 {
//因为runtime.Caller代价比较大,先不加锁
l.mu.Unlock()
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
}
formatHeader方法主要是格式化日志抬头信息,就是我们上面提到设置的日志打印格式,解析完之后存储在buf这个缓冲中,最后再把我们自己的日志信息拼接到缓冲buf的后面,然后为一次log日志输出追加一个换行符,这样每次日志输出都是一行一行的。
上面我们提到过runtime.Caller(calldepth)这个方法,runtime包非常有意思,后面也会去说,他提供了一个运行时环境,可以在运行时去管理内存分配,垃圾回收,时间片切换等等,类似于Java中虚拟机做的活。(是不是很疑惑为什么在Go中竟然可以去做Java中虚拟机能做的事情,其实想想协程的概念,再对比线程的概念,就不会疑惑为啥会给你提供这么个包)。
Caller方法的解释是:
Caller方法查询有关函数调用的文件和行号信息,通过调用Goroutine的堆栈。参数skip是堆栈帧框架升序方式排列的数字值,0标识Caller方法的调用。(出于历史原因,Skip的含义在调用者和调用者之间有所不同。)
返回值报告程序计数器、文件名和相应文件中行号的查询。如果无法恢复信息,则Boolean OK为 fasle。
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的原因。
5. 如何自定义自己的日志框架
通过上面的学习,你其实知道了,日志的实现是通过New()函数构造了Logger对象来处理的。那我们只用构造不同的Logger对象来处理不同类型的日记即可。下面是一个简单的实现:
package main
import (
"io"
"log"
"os"
)
var (
Info *log.Logger
Warning *log.Logger
Error * log.Logger
)
func init(){
infoFile,err:=os.OpenFile("/data/service_logs/info.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)
warnFile,err:=os.OpenFile("/data/service_logs/warn.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)
errFile,err:=os.OpenFile("/data/service_logs/errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)
if infoFile!=nil || warnFile != nil || 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)
Info = log.New(io.MultiWriter(os.Stderr,infoFile),"Info:",log.Ldate | log.Ltime | log.Lshortfile)
Warning = log.New(io.MultiWriter(os.Stderr,warnFile),"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("我就是一条日志啊")
Warning.Printf("我真的是一条日志哟%s\n","别骗我")
Error.Println("好了,我要报错了")
}
2. 第三方日志包logrus
上面介绍了Go中的log包,Go标准库的日志框架非常简单,仅仅提供了Print,Panic和Fatal三个函数。对于更精细的日志级别、日志文件分割,以及日志分发等方面,并没有提供支持 。也有很多第三方的开源爱好者贡献了很多好用的日志框架,毕竟Go是新兴预言,目前为止没有哪个日志框架能产生与Java中的slf4j一样的地位,目前流行的日志框架有seelog,zap,logrus,还有beego中的日志框架部分。
这些日志框架可能在某些方面不能满足你的需求,所以使用之前先了解清楚。因为logrus目前在GitHub上的star最高,11011。所以本篇文章介绍logrus的使用,大家可以举一反三。 logrus的GitHub地址:
1. logrus特性
logrus支持如下特性:
- 完全兼容Go标准库日志模块。logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是Go标准库日志模块的API的超集。如果你的项目使用标准库日志模块,完全可以用最低的代价迁移到logrus上。
- 可扩展的Hook机制。允许使用者通过hook方式,将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等。
- 可选的日志输出格式。**logrus内置了两种日志格式,JSONFormatter和TextFormatter。**如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式。
- Field机制。logrus鼓励通过Field机制进行精细化、结构化的日志记录,而不是通过冗长的消息来记录日志。
- logrus是一个可插拔的、结构化的日志框架。
logrus不提供的功能:
- 没有提供行号和文件名的支持
- 输出到本地文件系统没有提供日志分割功能
- 没有提供输出到ELK等日志处理中心的功能
这些功能都可以通过自定义hook来实现 。
2. 简单的入门
安装:
go get github.com/sirupsen/logrus
2.1 一个简单的入门:
package main
import log "github.com/sirupsen/logrus"
func main() {
log.Info("我是一条日志")
log.WithFields(log.Fields{"key":"value"}).Info("我要打印了")
}
输出:
time="2019-05-24T08:13:47+08:00" level=info msg="我是一条日志"
time="2019-05-24T08:13:47+08:00" level=info msg="我要打印了" key=value
2.2 设置log的日志输出为json格式
将日志输出格式设置为JSON格式:
log.SetFormatter(&log.JSONFormatter{})
package main
import (
log "github.com/sirupsen/logrus"
)
func initLog() {
// 设置日志格式为json格式
log.SetFormatter(&log.JSONFormatter{})
}
func main() {
initLog()
log.WithFields(log.Fields{
"age": 12,
"name": "xiaoming",
"sex": 1,
}).Info("小明来了")
log.WithFields(log.Fields{
"age": 13,
"name": "xiaohong",
"sex": 0,
}).Error("小红来了")
log.WithFields(log.Fields{
"age": 14,
"name": "xiaofang",
"sex": 1,
}).Fatal("小芳来了")
}
输出:
{"age":12,"level":"info","msg":"小明来了","name":"xiaoming","sex":1,"time":"2019-05-24T08:20:19+08:00"}
{"age":13,"level":"error","msg":"小红来了","name":"xiaohong","sex":0,"time":"2019-05-24T08:20:19+08:00"}
{"age":14,"level":"fatal","msg":"小芳来了","name":"xiaofang","sex":1,"time":"2019-05-24T08:20:19+08:00"}
看到这里输出的日志格式与上面的区别,这里是json格式,上面是纯文本。
2.3 设置日志打印级别
logrus 提供 6 档日志级别,分别是:
PanicLevel
FatalLevel
ErrorLevel
WarnLevel
InfoLevel
DebugLevel
设置日志输出级别:
log.SetLevel(log.WarnLevel)
2.4 自定义输出字段
logrus 默认的日志输出有 time、level 和 msg 3个 Field,其中 time 可以不显示,方法如下:
log.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
自定义 Field 的方法如下:
log.WithFields(log.Fields{
"age": 14,
"name": "xiaofang",
"sex": 1,
}).Fatal("小芳来了")
2.5 自定义日志输出路径
logrus默认日志输出为stderr,你可以修改为任何的io.Writer。比如os.File文件流。
func init() {
//设置输出样式,自带的只有两种样式logrus.JSONFormatter{}和logrus.TextFormatter{}
logrus.SetFormatter(&logrus.JSONFormatter{})
//设置output,默认为stderr,可以为任何io.Writer,比如文件*os.File
file, _ := os.OpenFile("1.log", os.O_CREATE|os.O_WRONLY, 0666)
log.SetOutput(file)
//设置最低loglevel
logrus.SetLevel(logrus.InfoLevel)
}
3. 高级功能-hook机制
上面说过logrus是一个支持可插拔,结构化的日志框架,可插拔的特性就在于它的hook机制。一些功能需要用户自己通过hook机制去实现定制化的开发。比如说在log4j中常见的日志按天按小时做切分的功能官方并没有提供支持,你可以通过hook机制实现它。
Hook接口定义如下:
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
logrus的hook原理是:在每次写入日志时拦截,修改logrus.Entry 。logrus在记录Levels()返回的日志级别的消息时,会触发HOOK, 然后按照Fire方法定义的内容,修改logrus.Entry 。logrus.Entry里面就是记录的每一条日志的内容。
所以在Hook中你需要做的就是在Fire方法中定义你想如何操作这一条日志的方法,在Levels方法中定义你想展示的日志级别。
如下是一个在所有日志中打印一个特殊字符串的Hook:
TraceIdHook
package hook
import (
"github.com/sirupsen/logrus"
)
type TraceIdHook struct {
TraceId string
}
func NewTraceIdHook(traceId string) logrus.Hook {
hook := TraceIdHook{
TraceId: traceId,
}
return &hook
}
func (hook *TraceIdHook) Fire(entry *logrus.Entry) error {
entry.Data["traceId"] = hook.TraceId
return nil
}
func (hook *TraceIdHook) Levels() []logrus.Level {
return logrus.AllLevels
}
主程序:
package main
import (
uuid "github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
"webDemo/hook"
)
func initLog() {
uuids, _ := uuid.NewV1()
log.AddHook(hook.NewTraceIdHook(uuids.String() +" "))
}
func main() {
initLog()
log.WithFields(log.Fields{
"age": 12,
"name": "xiaoming",
"sex": 1,
}).Info("小明来了")
log.WithFields(log.Fields{
"age": 13,
"name": "xiaohong",
"sex": 0,
}).Error("小红来了")
log.WithFields(log.Fields{
"age": 14,
"name": "xiaofang",
"sex": 1,
}).Fatal("小芳来了")
}
该hook会在日志中打印出一个uuid字符串。
Go中的日志及第三方日志包logrus的更多相关文章
- 跟我一起学.NetCore之日志作用域及第三方日志框架扩展
前言 上一节对日志的部分核心类型进行简单的剖析,相信现在再使用日志的时候,应该大概知道怎么一回事了,比如记录器是怎么来的,是如何将日志内容写入到不同目的地的等:当然还有很多细节没深入讲解,抽时间小伙伴 ...
- Android 代码混淆及第三方jar包不被混淆
为了保护代码被反编译,android引入了混淆代码的概念 1.设置混淆 在工程下找到project.properties文件 在文件中加入proguard.config=${sdk.dir}/tool ...
- eclipse打包jar及第三方jar包一起导出(生成SDK)
一.前言: 因公司需求,需要将某个工具类供外部使用,所以需要生成jar文件.但是jar内还包含了第三方的jar,普通的打包方式无法将lib下的第三方jar包提取. 这将会导致工具jar无法运行,或Ex ...
- LR中日志设置和日志函数
LR中日志参数的设置与使用 1.Run-Time Setting日志参数的设置 在loadrunner的vuser菜单下的Run-Time Setting的General的LOG选项中可以对在执行脚本 ...
- Jmeter 日志设置---如何设置java协议中被测jar的日志?
先转载一下Jmeter的日志设置: Jmeter运行出现问题可以通过调整jmeter的日志级别定位问题,但运行测试时建议关闭jmeter日志,jmeter打印日志耗费系统性能. Jmeter日志默认存 ...
- nginx日志中添加请求的response日志
换个新公司,做一些新鲜的事情,经过一天的琢磨,终于成功添加response日志 在nginx的日志中添加接口response的日志 由于此功能在nginx内置的功能中没有,需要安装第三方模块ngx_l ...
- 相同类中方法间调用时日志Aop失效处理
本篇分享的内容是在相同类中方法间调用时Aop失效处理方案,该问题我看有很多文章描述了,不过大多是从事务角度分享的,本篇打算从日志aop方面分享(当然都是aop,失效和处理方案都是一样),以下都是基于s ...
- 普通Java项目中使用Sl4j+Log4j2打印日志
因工作需要,采用JavaFx开发了一个windows窗口程序.在开发过程中,由于没有引入日志框架,只能自己手动在控制台打印些信息,给调试带来了很多麻烦:因此决定引入日志框架.由于之前接触的项目 ...
- Spring MVC 中使用AOP 进行统一日志管理--注解实现
1.AOP简介 AOP称为面向切面编程 AOP的基本概念 (1)Aspect(切面):通常是一个类,里面可以定义切入点和通知 (2)JointPoint(连接点):程序执行过程中明确的点,一般是方法的 ...
随机推荐
- 阿里云服务器纯净版centos7.4 LNMP安装
Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable ...
- git中常用的操作命令有哪些?常用操作命令归纳
git中常用的操作命令有哪些?本篇文章就给到大家归纳了一些git中常用操作命令.有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. git开始 全局配置:配置用户名和e-mail地址 1 ...
- 对scanner.close方法的误解以及无法补救的错误
scanner错误关闭导致的异常 public class test2 { public static void main(String[] args) { Scanner scanner1 = ne ...
- 你不得不知的几个互联网ID生成器方案
服务化.分布式已成为当下系统开发的首选,高并发操作在数据存储时,需要一套id生成器服务,来保证分布式情况下全局唯一性,以确保系统的订单创建.交易支付等场景下数据的唯一性,否则将造成不可估量的损失. 基 ...
- jekyll搭建个人博客1
目录 配置环境 使用模板 配置环境 简介 jekyll是一个简单的免费的,生成静态网页的工具,不需要数据库支持.但是可以配合第三方服务,例如Disqus.最关键的是jekyll可以免费部署在Githu ...
- C语言指针专题——指针难学的4点原因
前一篇跟大家聊了聊指针的概念,可是就算了解了指针是什么,为什么依然感觉难学?我试着从几个点切入,聊聊指针难学之处. 文末会给大家推荐几本书,有需要的朋友可以看看! 难点1. 讨厌的星号 定义指针变量p ...
- CAD2014学习笔记-文字编辑与尺寸标注
基于 虎课网huke88.com CAD教程 文字与表格 输入文字:TEXT.MTEXT 插入表格:table 新建表格样式 尺寸标注 测量工具:Di.DLI 开启标注:打开工具-工具栏-标注 对齐/ ...
- Postgresql_fdw
Postgresql_fdw 测试环境 Ubuntu 16.04 LTS云主机2台,主机名为pg1(192.168.0.34)和pg2(192.168.0.39). 安装postgresql 下面这个 ...
- 【弱化版】【P3371 【模板】单源最短路径(弱化版)】-C++
→原题传送门← 看到题目描述我就知道,这道题不能用SPFA[手动补滑稽] 那么我这道题目采用的是dijkstra算法不了解的去补一下知识哈. dij的模板: #include<bits/stdc ...
- 个人永久性免费-Excel催化剂功能第38波-比Vlookup更好用的查找引用函数
谈起Excel的函数,有一个函数生来自带明星光环,在表哥表姐群体中无人不知,介绍它的教程更是铺天盖地,此乃VLOOKUP函数也.今天Excel催化剂在这里冒着被火喷的风险,大胆地宣布一个比VLOOKU ...