前言

日志的记录和我们的业务逻辑并没有太大的关系,但是良好的日志记录习惯则对于一个项目来说非常重要。

那我们提到日志的时候,我们的需求是什么?

  • 记录错误信息,方便调试
  • 记录调用信息,方便统计
  • 日志文件分割

Go标准库 log 模块,主要提供了3类接口, Print, Panic, Fatal。相比于fmt.Pringxxx,在输出的位置做了线程安全的保护。

标准log库相对比较简单,在实际项目中往往使用封装更好功能更强大的三方库,比如logrus, zap, glog等。

这里介绍下logrus。

logrus简介

logrus Github项目地址

logrus提供结构化的日志定义和输出,并且完全兼容标准log库的功能。这意味着可以无痛将项目中使用标准库的部分替换成logrus。

logrus项目目前已进入维护模式,不再提供新的功能,维护的重点将放在安全功能增强,bug修复等。

logrus使用重点

loggus结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventurous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks LevelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
// logs with colors, but to a file it wouldn't. You can easily implement your
// own that implements the `Formatter` interface, see the `README` or included
// formatters for examples.
Formatter Formatter

// Flag for whether to log caller info (off by default)
ReportCaller bool

// The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged.
Level Level
// Used to sync writing to the log. Locking is enabled by Default
mu MutexWrap
// Reusable empty entry
entryPool sync.Pool
// Function to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc
}

其中,重点理解一下 Formatter, Out, Level 和 Hooks

1. Formatters

logrus通过 formatter来定义日志输出的格式。内置的格式有两种

  • TextFormatter (默认)
  • JSONFormatter
① TextFormatter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
type TextFormatter struct {
ForceColors bool
DisableColors bool // 是否使用彩色日志输出到控制台

ForceQuote bool
DisableQuote bool // 是否对日志中的键值对添加引号

// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
EnvironmentOverrideColors bool


DisableTimestamp bool
FullTimestamp bool // Enable logging the full timestamp when a TTY is attached instead of just the time passed since beginning of execution.
TimestampFormat string // TimestampFormat to use for display when a full timestamp is printed. 时间戳格式化, '2006-01-02 15:04:05',注意这个时间,是golang的诞生时间,和time包的format一样,这个字符串时间是唯一且固定的,格式可以不一样

// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool

// The keys sorting function, when uninitialized it uses sort.Strings.
SortingFunc func([]string)

// Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool

// PadLevelText Adds padding the level text so that all the levels output at the same length
// PadLevelText is a superset of the DisableLevelTruncation option
PadLevelText bool

// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool

// Whether the logger's out is to a terminal
isTerminal bool

// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
FieldMap FieldMap

// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)

terminalInitOnce sync.Once

// The max length of the level text, generated dynamically on init
levelTextMaxLength int
}

使用示例:

1
2
3
4
5
6
7
8
9
func main(){
logrus.SetFormatter(&logrus.TextFormatrer{
DisableColors: true,
ForceQuote: false,
TimestampFormat: "2006-01-02 15:04:05"
})
logrus.WithField("name", "ball").WithField("say", "hi").Info("info log")
}
// time="2021-06-08 14:51:22" level=info msg="info log" name=ball say=hi
② JSONFormatter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
// The format to use is the same than for time.Format or time.Parse from the standard
// library.
// The standard Library already provides a set of predefined format.
TimestampFormat string

// DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool

// DisableHTMLEscape allows disabling html escaping in output
DisableHTMLEscape bool

// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
DataKey string

// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &JSONFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message",
// FieldKeyFunc: "@caller",
// },
// }
FieldMap FieldMap

// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the json data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from json fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)

// PrettyPrint will indent all json logs
PrettyPrint bool
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main(){
logrus.SetFormatter(&logrus.JSONFormatter{
TimestampFormat:"2006-01-02 15:04:05",
PrettyPrint: true,
})
logrus.WithField("name", "ball").WithField("say", "hi").Info("info log")
}
// {
// "level": "info",
// "msg": "info log",
// "name": "ball",
// "say": "hi",
// "time": "2021-05-10 16:36:05"
// }
③ 自定义formatter

实现 Formatter接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Formatter interface {
Format(*Entry) ([]byte, error)
}

type Entry struct {
// Contains all the fields set by the user.
Data Fields

// Time at which the log entry was created
Time time.Time

// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
Level Level

//Calling method, with package name
Caller *runtime.Frame

//Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
Message string

//When formatter is called in entry.log(), a Buffer may be set to entry
Buffer *bytes.Buffer
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"bytes"
"fmt"

log "github.com/Sirupsen/logrus"
)

func main() {
// 初始化自定义 formatter
formatter := &MyFormatter{
Prefix: "prefix",
Suffix: "suffix",
}
log.SetFormatter(formatter)
log.Infoln("hello world")
}

// MyFormatter 自定义 formatter
type MyFormatter struct {
Prefix string
Suffix string
}

// Format implement the Formatter interface
func (mf *MyFormatter) Format(entry *log.Entry) ([]byte, error) {
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
// entry.Message 就是需要打印的日志
b.WriteString(fmt.Sprintf("%s - %s - %s", mf.Prefix, entry.Message, mf.Suffix))
return b.Bytes(), nil
}


// 输出:
// prefix - hello world - suffix
2. Output

只要实现了 io.writer 接口都可以,比如默认的 错误输出 os.Stderr, 可以更改为标准输出os.Stdout

可以输出到文件,也可以输出到kafka等。

1
2
3
// 可以实现同时标准输出和输出到日志文件
mw := io.MultiWriter(os.Stdout, logFile)
logrus.SetOutput(mw)
3. Level

日志的级别,只会输出当前级别及更高级别的日志。

  • TraceLevel
  • DebugLevel
  • InfoLevel
  • WarnLevel
  • FatalLevel
  • PanicLevel
4.Hook

hook机制允许使用者对logrus进行拓展,通过自定义的hook函数,将日志文件分发到文本,es,mq等地方去。

1
2
3
4
5
6
7
type Hook interface {
// 定义哪些等级的日志触发 hook 机制
Levels() []Level
// hook 触发器的具体执行操作
// 如果 Fire 执行失败,错误日志会重定向到标准错误流
Fire(*Entry) error
}

自定义 hook,我们需要实现 Hook接口。

logrus会保存维护所有的实现了Hook接口的函数,然后根据日志等级来出发执行不同的hook逻辑。

使用示例:

  • 创建自定义hook, 将 error级别及以上的日志输出到 err.log 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    type MyHook struct{}

    // 此方法定义了该 hook逻辑的触发日志等级
    func (h *MyHook) Levels()[]log.Level{
    return []log.Level{
    log.ErrorLevel,
    log.PanicLevel,
    }
    }

    // 此方法定义了,当该hook逻辑触发时,需要执行哪些操作,如果操作error,重定向至标准输出
    func (h *MyHook)Fire(entry *log.Entry) error{
    f, err := os.OpenFile("err.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil{
    return err
    }
    if _, err := f.Write([]byte(entry.Message)); err != nil{
    return err
    }
    return nil
    }
  • 添加hook

    1
    2
    3
    func main(){
    logrus.AddHook(&MyHook{})
    }