golang中实现一个log包-1

之前工作中写golang项目的时候,一直都是用别人封装好的日志包,这里尝试自行去实现一个简单封装的日志包,当然,是基于golang自带的log包去封装了。

1.自带log包的基本使用

这里先看自带的log包中日志对象的定义:

1
2
3
4
5
6
7
8
9
10
11
// A Logger represents an active logging object that generates lines of
// output to an io.Writer. Each logging operation makes a single call to
// the Writer's Write method. A Logger can be used simultaneously from
// 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
}

从这里可以看出,log个人对象最重要的是包含一个io.Writer 类型的属性out, 只要是实现了io.Writer这个interface定义的Write()方法的对象,就可以声明为一个io.Writer类型的对象了。

在log包中, 有两个主要的方法去对Logger对象的out属性赋值,一个是直接调用New()方法创建一个Logger对象;另外一个便是调用SetOutput()方法对一个Logger对象的out属性进行赋值。

1
2
3
4
5
6
7
8
9
10
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}

// SetOutput sets the output destination for the logger.
func (l *Logger) SetOutput(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.out = w
}

os.File对象实现了io.Writer接口对应的方法,故而可以赋值给Logger的out属性,常见的os.File有很多
os.Stdin / os.Stdout / os.Stderr 这三个os.File类型的对象分别对应系统的三个标准输出

1
2
3
4
5
6
7
// file.go

var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

这里我们的目标是要把日志输出到文件中,所以选择去调用 os.OpenFile()主动创建一个文件描述符,整体的代码如下

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
// demo1.go
package main

import (
"log"
"os"
)

func main() {

// 追加的方式打开一个写文件描述符,文件不存在则创建
writer, err := os.OpenFile("./1.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
if err != nil {
panic("create log writer error")
}

// 日志写入1.log 文件中
infoLog := log.New(writer, "[info]", log.Ldate|log.Ltime)
infoLog.Println("info log 1.log , here")
// 1.log中输出如下:
// [info]2020/06/18 14:40:57 info log 1.log , here

// 追加的方式打开一个新的写文件描述符,文件不存在则创建
writer2, err := os.OpenFile("./2.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
if err != nil {
panic("create log writer error")
}
writer.Close()
//这里如果关闭了文件描述符, 并不会报错,只是日志无法再写入文件
infoLog.Println("info log 2.log after close , here")

// 这里重新执行logger的 output,之后日志将写入write2指定的2.log中
infoLog.SetOutput(writer2)
infoLog.Println("info log 2.log , here")
// 2.log中输出如下:
// [info]2020/06/18 14:40:57 info log 2.log , here
}

2.简单封装一个支持日志级别的日志包(v1)

功能说明:

  1. 支持info,warn,err三种日志级别,运行过程中忽略低级别日志打印
  2. 三种级别日志写入同一个日志文件中
  3. 日志打印信息中显示文件名和行号
  4. 不考虑日志文件rolling和缓冲区

具体实现:

1
2
3
4
5
6
7
8
9
10
11
// 工程目录
elog
├── go.mod
├── log
│ ├── v1
│ │ └── log.go
│ └── v2
│ └── log.go
├── logs
│ └── 1.log
└── main.go

main.go

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
package main

import (
log "elog/log/v1"
)

var logger *log.ELog

func initLogger() {
logPath := "./logs"
logFile := "1.log"
logLevel := log.InfoLevel
logger = log.NewLogger(logLevel, logPath, logFile)
}

func main() {

initLogger()

logger.Info("hello info")
test()
}

func test() {
logger.Info("hello info test", "kkkkkk")
logger.Warn("hello waring", "warn1", ",warn2")
logger.Err("hello err", "err1", "error2")
}

log/v1/log.go :

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package v1

import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)

const (
InfoLevel = iota
WarnLevel
ErrLevel
)

// fileLogger代表了一种级别的日志文件对象,
type fileLogger struct {
logger *log.Logger
writer io.Writer //记录打开日志文件描述符,便于后续关闭
curOpenFile string //当前打开文件名
openTime time.Time //当前日志打开时间
}

// ELog 代表一个日志对象,包含了三个日志级别的日志文件对象
type ELog struct {
mu sync.Mutex
minLevel int // 日志打印的最低等级,低于此等级的日志将被忽略
infoLogger *fileLogger
warnLogger *fileLogger
errLogger *fileLogger
}

func (l *ELog) Info(v ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()

if l.minLevel > InfoLevel {
return
}

_, file, line, ok := runtime.Caller(1)
if !ok {
file = "unknow"
line = 0
} else {
file = filepath.Base(file)
}
//这里先使用runtime.Caller获取文件名和行号,后标准化输出
// 下面的Warn()函数直接使用log.logger.Outpu()函数指明调用者的文件名和行号,两种方式都OK
l.infoLogger.logger.Printf("%15s:%4d - %s", file, line, v)
}

func (l *ELog) Warn(v ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()

if l.minLevel > WarnLevel {
return
}
// l.warnLogger.logger.Println(v)

//这里使用Output指定callpath
str := fmt.Sprintf("%s", v)
l.warnLogger.logger.Output(2, str)
}

func (l *ELog) Err(v ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()

if l.minLevel > ErrLevel {
return
}
//这里使用Output指定callpath
str := fmt.Sprintf("%s", v)
l.errLogger.logger.Output(2, str)
}

//Close 目前因为3个日志写入同一个文件,故只需要关闭一次
func (l *ELog) Close() {
l.mu.Lock()
defer l.mu.Unlock()

l.infoLogger.writer.(*os.File).Close()
}

// 返回一个ELog对象,该对象实现了三种日志级别格各自的logger对象,低层写入同一个文件
func NewLogger(level int, logPath string, logFile string) *ELog {
mkLogDir(logPath)
file := logPath + "/" + logFile
writer, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
panic("open log file error")
}

infoLog := log.New(writer, "[info]", log.Ldate|log.Ltime)
warnLog := log.New(writer, "[warn]", log.Ldate|log.Ltime|log.Llongfile)
errLog := log.New(writer, "[err]", log.Ldate|log.Ltime|log.Lshortfile)

now := time.Now()

infoLogFile := &fileLogger{
logger: infoLog,
curOpenFile: file,
writer: writer,
openTime: now,
}

warnLogFile := &fileLogger{
logger: warnLog,
curOpenFile: file,
writer: writer,
openTime: now,
}

errLogFile := &fileLogger{
logger: errLog,
curOpenFile: file,
writer: writer,
openTime: now,
}

l := &ELog{
infoLogger: infoLogFile,
warnLogger: warnLogFile,
errLogger: errLogFile,
minLevel: level,
}
return l
}

// 判断所给路径是否为文件夹
func IsDir(path string) bool {
s, err := os.Stat(path)
if err != nil {
return false
}
return s.IsDir()
}

func mkLogDir(logPath string) {

if !IsDir(logPath) {
fmt.Println("dddd")
os.Mkdir(logPath, 0777)

}
}

// 判断所给路径文件/文件夹是否存在
func Exists(path string) bool {
_, err := os.Stat(path) //os.Stat获取文件信息
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}

额外说明:

  1. ELog代表了一个需要被实例化的日志对象,其中包含三个表示不同等级日志处理的属性
  2. ELog.Info() 通过手动调用runtime.Caller获取文件名和行号,后标准化输出,ELog.Warn()函数直接使用log.logger.Output()函数指明调用者的文件名和行号。 查看log.logger.Output代码可以看到,内部也是通过调用runtime.Caller获取文件名和行号。 log包通过log.Lshortfile , log.Llongfile 这两个flag来去问打印文件名的完整路径或者短文件名。
  3. ELog提供了Close()函数,去关闭已经打开的文件描述符,因为对应同一个文件描述符,故只需要关闭一次

… 下一篇接着实现一个支持rolling的日志封装