0%

Go语言实现简单的日志记录

前言

一个完整的日志库不仅仅涵盖日志记录功能,还要包括日志level、行号、文件切分,甚至包含统计与分析等,
Go语言中的日志库也是很多,其中知名度比较高的有:

库名 star
logrus 14940
zap 9827
zerolog 3386
seelog 1464

备注:star数获取时间为2020-05-28 23:26:00

一千个人有一千个需求,不管是哪个开源日志库,用着总有不顺手的时候,没关系,那就自己实现一个吧,
相信自己,来,就让咱们先从实现简单的日志记录功能开始吧~「手动狗头」

思路

  1. 功能设计

    根据自己的需求,我想要的日志记录功能有:

    • 按照level输出日志
    • 能够同时输出到文件和控制台
    • 控制台能够根据level将内容输出为不同颜色
    • 日志文件根据大小进行分割
    • 输出行号
  2. API设计

    一般来说,根据level不同,设计有不同的API,level大概可以分为: trace、warn、error、fatal,
    也就是说对外的API可以概括为: T(…inter), W(…), E(…), F(…)

    1
    2
    3
    4
    5
    6
    type logger interface{
    T(format string, v ...interface{})
    W(format string, v ...interface{})
    E(format string, v ...interface{})
    F(format string, v ...interface{})
    }
  3. 结构设计

    根据需求,日志记录器logger的结构需要包含writers、文件名、文件保存路径、文件分割大小
    完整结构设计如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type myLog struct {
    sync.Once
    sync.Mutex //用于outs并发访问
    outs map[logType]io.Writer //writer集合
    file *os.File //文件句柄
    fileName string //日志名
    dir string //日志存放路径
    size int64 //单个日志文件的大小限制
    }
  4. 关键方法实现

    • 日志文件大小检测

      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
      func (m *myLog) checkLogSize() {
      if m.file == nil {
      return
      }
      m.Lock()
      defer m.Unlock() //此处必须加锁,否则会出现并发问题
      fileInfo, err := m.file.Stat()
      if err != nil {
      panic(err)
      }
      if m.size > fileInfo.Size() {
      return
      }
      //需要分割,重新打开一个新的文件句柄替换老的,并关闭老的文件句柄,
      newName := path.Join(m.dir, time.Now().Format("2006_01_02_15:04:03")+".log")
      name := path.Join(m.dir, m.fileName)

      err = os.Rename(name, newName)
      if err != nil {
      panic(err)
      }

      file, err := os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
      if err != nil {
      panic(err)
      }

      m.file.Close()
      m.file = file
      m.outs[logTypeFile] = file
      return
      }
    • 控制台带颜色输出内容

      1
      2
      3
      func setColor(msg string, text int) string {
      return fmt.Sprintf("%c[%dm%s%c[0m", 0x1B, text, msg, 0x1B)
      }
    • 获取行号

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      func shortFileName(file string) string {
      short := file
      for i := len(file) - 1; i > 0; i-- {
      if file[i] == '/' {
      short = file[i+1:]
      break
      }
      }
      return short
      }

完整代码实现

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package logUtil

import (
"fmt"
"io"
"os"
"path"
"runtime"
"strconv"
"sync"
"time"
)

const (
colorRed = 31
colorYellow = 33
colorBlue = 34

levelT = "[T] "
levelE = "[E] "
levelW = "[W] "

defaultFileSize = 60 * 1024 * 1024
minFileSize = 1 * 1024 * 1024
defaultLogDir = "log"
defaultLogName = "default.log"

logTypeStd logType = iota + 1
logTypeFile
)

type (
logType int

LogOption func(log *myLog)

myLog struct {
sync.Once
sync.Mutex
outs map[logType]io.Writer //writer集合
file *os.File //文件句柄
fileName string //日志名
dir string //日志存放路径
size int64 //单个日志文件的大小限制
}
)

var (
defaultLogger = &myLog{}
)

func (m *myLog) init() {
if m.dir == "" {
m.dir = defaultLogDir
}
if m.fileName == "" {
m.fileName = defaultLogName
}
if m.size == 0 {
m.size = defaultFileSize
} else {
if m.size < minFileSize {
panic(fmt.Sprintf("invalid size: %d", m.size))
}
}

if m.outs == nil {
m.outs = make(map[logType]io.Writer)
}
if !isExist(m.dir) {
if err := os.Mkdir(m.dir, 0777); err != nil {
panic(err)
}
}
name := path.Join(m.dir, m.fileName)
file, err := os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
panic(err)
}

m.file = file
m.outs[logTypeStd] = os.Stdout
m.outs[logTypeFile] = file
}

func (m *myLog) checkLogSize() {
if m.file == nil {
return
}
m.Lock()
defer m.Unlock()
fileInfo, err := m.file.Stat()
if err != nil {
panic(err)
}
if m.size > fileInfo.Size() {
return
}
//需要分割
newName := path.Join(m.dir, time.Now().Format("2006_01_02_15:04:03")+".log")
name := path.Join(m.dir, m.fileName)

err = os.Rename(name, newName)
if err != nil {
panic(err)
}

file, err := os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
panic(err)
}

m.file.Close()
m.file = file
m.outs[logTypeFile] = file
return
}

func (m *myLog) write(level string, content string) {
m.checkLogSize()
var colorText int
switch level {
case levelT:
colorText = colorBlue
case levelW:
colorText = colorYellow
case levelE:
colorText = colorRed
}

for k, wr := range m.outs {
if k == logTypeStd {
fmt.Fprintf(wr, setColor(content, colorText))
} else {
fmt.Fprintf(wr, content)
}
}
}

func WithSize(size int64) LogOption {
return func(log *myLog) {
log.size = size
}
}

func WithLogDir(dir string) LogOption {
return func(log *myLog) {
log.dir = dir
}
}

func WithFileName(name string) LogOption {
return func(log *myLog) {
log.fileName = name
}
}

func InitLogger(args ...LogOption) {
defaultLogger.Do(func() {
for _, af := range args {
af(defaultLogger)
}
defaultLogger.init()
})
}

//Info
func T(format string, v ...interface{}) {
_, file, line, _ := runtime.Caller(1)
timeStr := time.Now().Format("2006-01-02 15:04:05.0000") + " "
codeLine := "[" + timeStr + shortFileName(file) + ":" + strconv.Itoa(line) + "]"
content := levelT + codeLine + fmt.Sprintf(format, v...) + "\n"
defaultLogger.write(levelT, content)
}

//Error
func E(format string, v ...interface{}) {
_, file, line, _ := runtime.Caller(1)
timeStr := time.Now().Format("2006-01-02 15:04:05.0000") + " "
codeLine := "[" + timeStr + shortFileName(file) + ":" + strconv.Itoa(line) + "]"
content := levelE + codeLine + fmt.Sprintf(format, v...) + "\n"
defaultLogger.write(levelE, content)
}

//Warn
func W(format string, v ...interface{}) {
_, file, line, _ := runtime.Caller(1)
timeStr := time.Now().Format("2006-01-02 15:04:05.0000") + " "
codeLine := "[" + timeStr + shortFileName(file) + ":" + strconv.Itoa(line) + "]"
content := levelW + codeLine + fmt.Sprintf(format, v...) + "\n"
defaultLogger.write(levelW, content)
}

func isExist(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}

func shortFileName(file string) string {
short := file
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
short = file[i+1:]
break
}
}
return short
}

func setColor(msg string, text int) string {
return fmt.Sprintf("%c[%dm%s%c[0m", 0x1B, text, msg, 0x1B)
}

此处附上代码地址: logUtil,欢迎指正!