3.6 编写一个简单的文件日志

涉及知识点

    自定义 log。

本文目标

在上一节中,我们解决了API's可以任意访问的问题,那么我们现在还有一个问题,就是我们的日志,都是输出到控制台上的,这显然对于一个项目来说是不合理的,因此我们这一节简单封装log库,使其支持简单的文件日志!

新建logging

我们在pkg下新建logging目录,新建file.golog.go文件,写入内容:

编写file文件

1、 file.go:
1
package logging
2
3
import (
4
"os"
5
"time"
6
"fmt"
7
"log"
8
)
9
10
var (
11
LogSavePath = "runtime/logs/"
12
LogSaveName = "log"
13
LogFileExt = "log"
14
TimeFormat = "20060102"
15
)
16
17
func getLogFilePath() string {
18
return fmt.Sprintf("%s", LogSavePath)
19
}
20
21
func getLogFileFullPath() string {
22
prefixPath := getLogFilePath()
23
suffixPath := fmt.Sprintf("%s%s.%s", LogSaveName, time.Now().Format(TimeFormat), LogFileExt)
24
25
return fmt.Sprintf("%s%s", prefixPath, suffixPath)
26
}
27
28
func openLogFile(filePath string) *os.File {
29
_, err := os.Stat(filePath)
30
switch {
31
case os.IsNotExist(err):
32
mkDir()
33
case os.IsPermission(err):
34
log.Fatalf("Permission :%v", err)
35
}
36
37
handle, err := os.OpenFile(filePath, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)
38
if err != nil {
39
log.Fatalf("Fail to OpenFile :%v", err)
40
}
41
42
return handle
43
}
44
45
func mkDir() {
46
dir, _ := os.Getwd()
47
err := os.MkdirAll(dir + "/" + getLogFilePath(), os.ModePerm)
48
if err != nil {
49
panic(err)
50
}
51
}
Copied!
    os.Stat:返回文件信息结构描述文件。如果出现错误,会返回*PathError
    1
    type PathError struct {
    2
    Op string
    3
    Path string
    4
    Err error
    5
    }
    Copied!
    os.IsNotExist:能够接受ErrNotExistsyscall的一些错误,它会返回一个布尔值,能够得知文件不存在或目录不存在
    os.IsPermission:能够接受ErrPermissionsyscall的一些错误,它会返回一个布尔值,能够得知权限是否满足
    os.OpenFile:调用文件,支持传入文件名称、指定的模式调用文件、文件权限,返回的文件的方法可以用于I/O。如果出现错误,则为*PathError
1
const (
2
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
3
O_RDONLY int = syscall.O_RDONLY // 以只读模式打开文件
4
O_WRONLY int = syscall.O_WRONLY // 以只写模式打开文件
5
O_RDWR int = syscall.O_RDWR // 以读写模式打开文件
6
// The remaining values may be or'ed in to control behavior.
7
O_APPEND int = syscall.O_APPEND // 在写入时将数据追加到文件中
8
O_CREATE int = syscall.O_CREAT // 如果不存在,则创建一个新文件
9
O_EXCL int = syscall.O_EXCL // 使用O_CREATE时,文件必须不存在
10
O_SYNC int = syscall.O_SYNC // 同步IO
11
O_TRUNC int = syscall.O_TRUNC // 如果可以,打开时
12
)
Copied!
    os.Getwd:返回与当前目录对应的根路径名
    os.MkdirAll:创建对应的目录以及所需的子目录,若成功则返回nil,否则返回error
    os.ModePermconst定义ModePerm FileMode = 0777

编写log文件

2、log.go
1
package logging
2
3
import (
4
"log"
5
"os"
6
"runtime"
7
"path/filepath"
8
"fmt"
9
)
10
11
type Level int
12
13
var (
14
F *os.File
15
16
DefaultPrefix = ""
17
DefaultCallerDepth = 2
18
19
logger *log.Logger
20
logPrefix = ""
21
levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
22
)
23
24
const (
25
DEBUG Level = iota
26
INFO
27
WARNING
28
ERROR
29
FATAL
30
)
31
32
func init() {
33
filePath := getLogFileFullPath()
34
F = openLogFile(filePath)
35
36
logger = log.New(F, DefaultPrefix, log.LstdFlags)
37
}
38
39
func Debug(v ...interface{}) {
40
setPrefix(DEBUG)
41
logger.Println(v)
42
}
43
44
func Info(v ...interface{}) {
45
setPrefix(INFO)
46
logger.Println(v)
47
}
48
49
func Warn(v ...interface{}) {
50
setPrefix(WARNING)
51
logger.Println(v)
52
}
53
54
func Error(v ...interface{}) {
55
setPrefix(ERROR)
56
logger.Println(v)
57
}
58
59
func Fatal(v ...interface{}) {
60
setPrefix(FATAL)
61
logger.Fatalln(v)
62
}
63
64
func setPrefix(level Level) {
65
_, file, line, ok := runtime.Caller(DefaultCallerDepth)
66
if ok {
67
logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
68
} else {
69
logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
70
}
71
72
logger.SetPrefix(logPrefix)
73
}
Copied!
    log.New:创建一个新的日志记录器。out定义要写入日志数据的IO句柄。prefix定义每个生成的日志行的开头。flag定义了日志记录属性
    1
    func New(out io.Writer, prefix string, flag int) *Logger {
    2
    return &Logger{out: out, prefix: prefix, flag: flag}
    3
    }
    Copied!
    log.LstdFlags:日志记录的格式属性之一,其余的选项如下
    1
    const (
    2
    Ldate = 1 << iota // the date in the local time zone: 2009/01/23
    3
    Ltime // the time in the local time zone: 01:23:23
    4
    Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
    5
    Llongfile // full file name and line number: /a/b/c/d.go:23
    6
    Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
    7
    LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
    8
    LstdFlags = Ldate | Ltime // initial values for the standard logger
    9
    )
    Copied!
当前目录结构:
1
gin-blog/
2
├── conf
3
│ └── app.ini
4
├── main.go
5
├── middleware
6
│ └── jwt
7
│ └── jwt.go
8
├── models
9
│ ├── article.go
10
│ ├── auth.go
11
│ ├── models.go
12
│ └── tag.go
13
├── pkg
14
│ ├── e
15
│ │ ├── code.go
16
│ │ └── msg.go
17
│ ├── logging
18
│ │ ├── file.go
19
│ │ └── log.go
20
│ ├── setting
21
│ │ └── setting.go
22
│ └── util
23
│ ├── jwt.go
24
│ └── pagination.go
25
├── routers
26
│ ├── api
27
│ │ ├── auth.go
28
│ │ └── v1
29
│ │ ├── article.go
30
│ │ └── tag.go
31
│ └── router.go
32
├── runtime
Copied!
我们自定义的logging包,已经基本完成了,接下来让它接入到我们的项目之中吧。我们打开先前包含log包的代码,如下:
    1.
    打开routers目录下的article.gotag.goauth.go
    2.
    log包的引用删除,修改引用我们自己的日志包为github.com/EDDYCJY/go-gin-example/pkg/logging
    3.
    将原本的log.Println(...)改为logging.Info(...)
例如auth.go文件的修改内容:
1
package api
2
3
import (
4
"net/http"
5
6
"github.com/gin-gonic/gin"
7
"github.com/astaxie/beego/validation"
8
9
"github.com/EDDYCJY/go-gin-example/pkg/e"
10
"github.com/EDDYCJY/go-gin-example/pkg/util"
11
"github.com/EDDYCJY/go-gin-example/models"
12
"github.com/EDDYCJY/go-gin-example/pkg/logging"
13
)
14
...
15
func GetAuth(c *gin.Context) {
16
...
17
code := e.INVALID_PARAMS
18
if ok {
19
...
20
} else {
21
for _, err := range valid.Errors {
22
logging.Info(err.Key, err.Message)
23
}
24
}
25
26
c.JSON(http.StatusOK, gin.H{
27
"code" : code,
28
"msg" : e.GetMsg(code),
29
"data" : data,
30
})
31
}
Copied!

验证功能

修改文件后,重启服务,我们来试试吧!
获取到API的Token后,我们故意传错误URL参数给接口,如:http://127.0.0.1:8000/api/v1/articles?tag_id=0&state=9999999&token=eyJhbG..
然后我们到$GOPATH/gin-blog/runtime/logs查看日志:
1
$ tail -f log20180216.log
2
[INFO][article.go:79]2018/02/16 18:33:12 [state 状态只允许0或1]
3
[INFO][article.go:79]2018/02/16 18:33:42 [state 状态只允许0或1]
4
[INFO][article.go:79]2018/02/16 18:33:42 [tag_id 标签ID必须大于0]
5
[INFO][article.go:79]2018/02/16 18:38:39 [state 状态只允许0或1]
6
[INFO][article.go:79]2018/02/16 18:38:39 [tag_id 标签ID必须大于0]
Copied!
日志结构一切正常,我们的记录模式都为Info,因此前缀是对的,并且我们是入参有问题,也把错误记录下来了,这样排错就很方便了!
至此,本节就完成了,这只是一个简单的扩展,实际上我们线上项目要使用的文件日志,是更复杂一些,开动你的大脑 举一反三吧!

参考

本系列示例代码

关于

修改记录

    第一版:2018年02月16日发布文章
    第二版:2019年10月01日修改文章

如果有任何疑问或错误,欢迎在 issues 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

我的公众号

image
Last modified 2yr ago