编写一个简单的文件日志

涉及知识点

  • 自定义 log。

本文目标

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

新建logging

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

编写file文件

1、 file.go:

  1. package logging
  2. import (
  3. "os"
  4. "time"
  5. "fmt"
  6. "log"
  7. )
  8. var (
  9. LogSavePath = "runtime/logs/"
  10. LogSaveName = "log"
  11. LogFileExt = "log"
  12. TimeFormat = "20060102"
  13. )
  14. func getLogFilePath() string {
  15. return fmt.Sprintf("%s", LogSavePath)
  16. }
  17. func getLogFileFullPath() string {
  18. prefixPath := getLogFilePath()
  19. suffixPath := fmt.Sprintf("%s%s.%s", LogSaveName, time.Now().Format(TimeFormat), LogFileExt)
  20. return fmt.Sprintf("%s%s", prefixPath, suffixPath)
  21. }
  22. func openLogFile(filePath string) *os.File {
  23. _, err := os.Stat(filePath)
  24. switch {
  25. case os.IsNotExist(err):
  26. mkDir()
  27. case os.IsPermission(err):
  28. log.Fatalf("Permission :%v", err)
  29. }
  30. handle, err := os.OpenFile(filePath, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)
  31. if err != nil {
  32. log.Fatalf("Fail to OpenFile :%v", err)
  33. }
  34. return handle
  35. }
  36. func mkDir() {
  37. dir, _ := os.Getwd()
  38. err := os.MkdirAll(dir + "/" + getLogFilePath(), os.ModePerm)
  39. if err != nil {
  40. panic(err)
  41. }
  42. }
  • os.Stat:返回文件信息结构描述文件。如果出现错误,会返回*PathError
  1. type PathError struct {
  2. Op string
  3. Path string
  4. Err error
  5. }
  • 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. )
  • os.Getwd:返回与当前目录对应的根路径名
  • os.MkdirAll:创建对应的目录以及所需的子目录,若成功则返回nil,否则返回error
  • os.ModePermconst定义ModePerm FileMode = 0777

编写log文件

2、log.go

  1. package logging
  2. import (
  3. "log"
  4. "os"
  5. "runtime"
  6. "path/filepath"
  7. "fmt"
  8. )
  9. type Level int
  10. var (
  11. F *os.File
  12. DefaultPrefix = ""
  13. DefaultCallerDepth = 2
  14. logger *log.Logger
  15. logPrefix = ""
  16. levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
  17. )
  18. const (
  19. DEBUG Level = iota
  20. INFO
  21. WARNING
  22. ERROR
  23. FATAL
  24. )
  25. func init() {
  26. filePath := getLogFileFullPath()
  27. F = openLogFile(filePath)
  28. logger = log.New(F, DefaultPrefix, log.LstdFlags)
  29. }
  30. func Debug(v ...interface{}) {
  31. setPrefix(DEBUG)
  32. logger.Println(v)
  33. }
  34. func Info(v ...interface{}) {
  35. setPrefix(INFO)
  36. logger.Println(v)
  37. }
  38. func Warn(v ...interface{}) {
  39. setPrefix(WARNING)
  40. logger.Println(v)
  41. }
  42. func Error(v ...interface{}) {
  43. setPrefix(ERROR)
  44. logger.Println(v)
  45. }
  46. func Fatal(v ...interface{}) {
  47. setPrefix(FATAL)
  48. logger.Fatalln(v)
  49. }
  50. func setPrefix(level Level) {
  51. _, file, line, ok := runtime.Caller(DefaultCallerDepth)
  52. if ok {
  53. logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
  54. } else {
  55. logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
  56. }
  57. logger.SetPrefix(logPrefix)
  58. }
  • 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. }
  • 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. )

当前目录结构:

  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

我们自定义的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. import (
  3. "net/http"
  4. "github.com/gin-gonic/gin"
  5. "github.com/astaxie/beego/validation"
  6. "github.com/EDDYCJY/go-gin-example/pkg/e"
  7. "github.com/EDDYCJY/go-gin-example/pkg/util"
  8. "github.com/EDDYCJY/go-gin-example/models"
  9. "github.com/EDDYCJY/go-gin-example/pkg/logging"
  10. )
  11. ...
  12. func GetAuth(c *gin.Context) {
  13. ...
  14. code := e.INVALID_PARAMS
  15. if ok {
  16. ...
  17. } else {
  18. for _, err := range valid.Errors {
  19. logging.Info(err.Key, err.Message)
  20. }
  21. }
  22. c.JSON(http.StatusOK, gin.H{
  23. "code" : code,
  24. "msg" : e.GetMsg(code),
  25. "data" : data,
  26. })
  27. }

验证功能

修改文件后,重启服务,我们来试试吧!

获取到 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 状态只允许01]
  3. [INFO][article.go:79]2018/02/16 18:33:42 [state 状态只允许01]
  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 状态只允许01]
  6. [INFO][article.go:79]2018/02/16 18:38:39 [tag_id 标签ID必须大于0]

日志结构一切正常,我们的记录模式都为Info,因此前缀是对的,并且我们是入参有问题,也把错误记录下来了,这样排错就很方便了!

至此,本节就完成了,这只是一个简单的扩展,实际上我们线上项目要使用的文件日志,是更复杂一些,开动你的大脑 举一反三吧!

参考

本系列示例代码

关于

修改记录

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

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

我的公众号

image