WHY

日志概述

日志几乎是每个实际的软件项目从开发到最后实际运行过程中都必不可少的东西。它对于查看代码运行流程,记录发生的事情等方面都是很重要的。

一个好的日志系统应当能准确地记录需要记录的信息,同时兼具良好的性能,接下来本文将从0写一个Golang日志处理包。

通常Go应用程序多为并发模型应用,在并发处理实际应用的过程中也无法避免并发地调用日志方法。

通常来说,Go中除非声明方法是协程安全的,否则最好都视为协程不安全,Go中默认的日志方法为log,该方法协程安全,但是功能有限。

第三方Go日志包如ZAP、logrus等均为较为不错的日志库,也提供了较多的配置选项,但是对于高度定制需求的日志则有些不适用,从0开始自己写一个就较为适用。

设计概述

  • 按json方式输出到文件
  • 日志类型支持丰富
  • 有日志分级设定
  • 按天分隔日志文件
  • 异常退出能把未写入的日志写完

HOW

整体流程

怎么填充日志内容

要做到按json方式输出到文件,并且支持丰富的类型,第一个能想到的则是Golang中强大的interface

设计两种类型分别用于接收map型和string型日志,再根据日志分级设定分别暴露出对外接口:

  1. func Debug(msg map[string]interface{}) {
  2. writeLog("DEBUG", msg)
  3. }
  4. func DebugMsg(msg interface{}) {
  5. writeLog("DEBUG", map[string]interface{}{"msg": msg})
  6. }
  7. func Info(msg map[string]interface{}) {
  8. writeLog("INFO", msg)
  9. }
  10. func InfoMsg(msg interface{}) {
  11. writeLog("INFO", map[string]interface{}{"msg": msg})
  12. }
  13. func Warn(msg map[string]interface{}) {
  14. writeLog("WARN", msg)
  15. }
  16. func WarnMsg(msg interface{}) {
  17. writeLog("WARN", map[string]interface{}{"msg": msg})
  18. }
  19. func Error(msg map[string]interface{}) {
  20. writeLog("ERROR", msg)
  21. }
  22. func ErrorMsg(msg interface{}) {
  23. writeLog("ERROR", map[string]interface{}{"msg": msg})
  24. }

最终都是使用writeLog进行日志的处理,writeLog方法定义如下:

  1. func writeLog(level string, msg map[string]interface{})

用哪种方式写入文件

Golang对于文件的写入方式多种多样,通常来讲最后都是使用操作系统的磁盘IO方法把数据写入文件

在选型上这块使用bufio方式来构建,使用默认的4096长度,如果收集的日志长度超过了缓冲区长度则自动将内容写入到文件,同时增加一组定时器,每秒将缓冲区内容写入到文件中,这样在整体性能上较为不错

处理协程抢占问题

针对多协程抢占的问题,Golang提供有两个比较标准的应对方式:使用channel或者加锁

在这里采用读写锁的方式来进行处理bufio,bufio的WriteString方法需串行处理,要不然会导致错误,而Flush方法可以多协程同时操作

基于这些特性,我们在使用WriteString方法的时候使用写锁,使用Flush方法时采用读锁:

  1. fileWriter.Mu.Lock()
  2. fileWriter.Writer.WriteString(a)
  3. fileWriter.Mu.Unlock()
  1. fileWriter.Mu.RLock()
  2. err = fileWriter.Writer.Flush()
  3. if err != nil {
  4. log.Println("flush log file err", err)
  5. }
  6. fileWriter.Mu.RUnlock()

跨天的日志文件处理

首先明确一个问题,在每日结束次日开始时将打开一个新的日志文件,那么还在缓冲区未完成刷新的数据怎么处理呢?

bufio提供了Reset方法,但是该方法注释说明将丢弃未刷新的数据而直接重新指向新的io writer,因此我们不能直接使用该方法,否则这个时间节点附近的数据将会丢掉

实际测试证明如果先关闭原IO,再重新创建新的文件描述符,最后调用Reset方法指向新的描述符过后这段时间将会丢掉达约20ms的数据

基于此,我们使用了二级指针:

1.判断当前日期是否就是今天,如果是则等待下个判断周期,如果不是则开始准备指针重定向操作

2.判断哪一个文件描述符为空,如果为空则为其创建新的描述符,并指定配对的filebuffer,如果不为空则说明它就是当前正在操作的文件

3.将filewriter指向新的filebuffer

4.对老的filebuffer进行Flush操作,之后将filebuffer和file都置为空

经过这样的操作过后,跨天的日志文件处理就不会有数据丢失的情况了

  1. if today != time.Now().Day() {
  2. today = time.Now().Day()
  3. if file[0] == nil {
  4. file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  5. if err != nil {
  6. log.Fatal("open log file err: ", err)
  7. }
  8. fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
  9. fileWriter.Writer = fileBuffer[0]
  10. if fileBuffer[1].Buffered() > 0 {
  11. fileBuffer[1].Flush()
  12. }
  13. fileBuffer[1] = nil
  14. file[1].Close()
  15. file[1] = nil
  16. } else if file[1] == nil {
  17. file[1], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  18. if err != nil {
  19. log.Fatal("open log file err: ", err)
  20. }
  21. fileBuffer[1] = bufio.NewWriterSize(file[1], 4096)
  22. fileWriter.Writer = fileBuffer[1]
  23. if fileBuffer[0].Buffered() > 0 {
  24. fileBuffer[0].Flush()
  25. }
  26. fileBuffer[0] = nil
  27. file[0].Close()
  28. file[0] = nil
  29. }
  30. }

异常退出的处理

一个基本的概念就是程序在操作系统退出的时候通常都会得到系统信号,比如linux的kill操作就是给应用程序发送系统信号

信号分很多种,比如常见的ctrl+c对应的信号则是Interrupt信号,这块去搜索“posix信号”也有详细的解释说明

基于此,常规的异常处理我们可以捕获系统信号然后做一些结束前的处理操作,这样的信号可以在多个包同时使用,均会收到信号,不用担心信号强占的问题

比如这个包在接收到退出信号时则刷新所有缓存数据并关闭所有文件描述符:

  1. func exitHandle() {
  2. <-exitChan
  3. if file != nil {
  4. if fileWriter.Writer.Buffered() > 0 {
  5. fileWriter.Writer.Flush()
  6. }
  7. //及时关闭file句柄
  8. if file[0] != nil {
  9. file[0].Close()
  10. }
  11. if file[1] != nil {
  12. file[1].Close()
  13. }
  14. }
  15. os.Exit(1) //使用os.Exit强行关掉
  16. }

完整源码

  1. package logger
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "log"
  8. "os"
  9. "os/signal"
  10. "xxx/config"
  11. "sync"
  12. "syscall"
  13. "time"
  14. )
  15. var file []*os.File
  16. var err error
  17. var fileBuffer []*bufio.Writer
  18. var exitChan chan os.Signal
  19. type fileWriterS struct {
  20. Writer *bufio.Writer
  21. Mu sync.RWMutex
  22. }
  23. var fileWriter fileWriterS
  24. var today int
  25. func LoggerInit() {
  26. filePath := config.Get("app", "log_path") //config处可以直接换成自己的config甚至直接写死
  27. _, err := os.Stat(filePath)
  28. if err != nil {
  29. if os.IsNotExist(err) {
  30. os.MkdirAll(filePath, os.ModePerm)
  31. } else {
  32. log.Fatal("log path err:", err)
  33. }
  34. }
  35. file = make([]*os.File, 2)
  36. file[0] = nil
  37. file[1] = nil
  38. fileBuffer = make([]*bufio.Writer, 2)
  39. fileBuffer[0] = nil
  40. fileBuffer[1] = nil
  41. today = time.Now().Day()
  42. file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  43. if err != nil {
  44. log.Fatal("open log file err: ", err)
  45. }
  46. fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
  47. fileWriter.Writer = fileBuffer[0]
  48. exitChan = make(chan os.Signal)
  49. signal.Notify(exitChan, os.Interrupt, os.Kill, syscall.SIGTERM)
  50. go exitHandle()
  51. go func() {
  52. time.Sleep(1 * time.Second)
  53. for {
  54. if fileWriter.Writer.Buffered() > 0 {
  55. fileWriter.Mu.RLock()
  56. err = fileWriter.Writer.Flush()
  57. if err != nil {
  58. log.Println("flush log file err", err)
  59. }
  60. fileWriter.Mu.RUnlock()
  61. }
  62. time.Sleep(1 * time.Second)
  63. if today != time.Now().Day() {
  64. today = time.Now().Day()
  65. if file[0] == nil {
  66. file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  67. if err != nil {
  68. log.Fatal("open log file err: ", err)
  69. }
  70. fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
  71. fileWriter.Writer = fileBuffer[0]
  72. if fileBuffer[1].Buffered() > 0 {
  73. fileBuffer[1].Flush()
  74. }
  75. fileBuffer[1] = nil
  76. file[1].Close()
  77. file[1] = nil
  78. } else if file[1] == nil {
  79. file[1], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  80. if err != nil {
  81. log.Fatal("open log file err: ", err)
  82. }
  83. fileBuffer[1] = bufio.NewWriterSize(file[1], 4096)
  84. fileWriter.Writer = fileBuffer[1]
  85. if fileBuffer[0].Buffered() > 0 {
  86. fileBuffer[0].Flush()
  87. }
  88. fileBuffer[0] = nil
  89. file[0].Close()
  90. file[0] = nil
  91. }
  92. }
  93. }
  94. }()
  95. }
  96. func exitHandle() {
  97. <-exitChan
  98. if file != nil {
  99. if fileWriter.Writer.Buffered() > 0 {
  100. fileWriter.Writer.Flush()
  101. }
  102. if file[0] != nil {
  103. file[0].Close()
  104. }
  105. if file[1] != nil {
  106. file[1].Close()
  107. }
  108. }
  109. os.Exit(1)
  110. }
  111. func Debug(msg map[string]interface{}) {
  112. writeLog("DEBUG", msg)
  113. }
  114. func DebugMsg(msg interface{}) {
  115. writeLog("DEBUG", map[string]interface{}{"msg": msg})
  116. }
  117. func Info(msg map[string]interface{}) {
  118. writeLog("INFO", msg)
  119. }
  120. func InfoMsg(msg interface{}) {
  121. writeLog("INFO", map[string]interface{}{"msg": msg})
  122. }
  123. func Warn(msg map[string]interface{}) {
  124. writeLog("WARN", msg)
  125. }
  126. func WarnMsg(msg interface{}) {
  127. writeLog("WARN", map[string]interface{}{"msg": msg})
  128. }
  129. func Error(msg map[string]interface{}) {
  130. writeLog("ERROR", msg)
  131. }
  132. func ErrorMsg(msg interface{}) {
  133. writeLog("ERROR", map[string]interface{}{"msg": msg})
  134. }
  135. func writeLog(level string, msg map[string]interface{}) {
  136. will_write_map := make(map[string]interface{})
  137. t := time.Now()
  138. will_write_map["@timestamp"] = fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.%03d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
  139. will_write_map["level"] = level
  140. for p, v := range msg {
  141. will_write_map[p] = v
  142. }
  143. //buffer满了会自动flush
  144. bf := bytes.NewBuffer([]byte{})
  145. jsonEncoder := json.NewEncoder(bf)
  146. jsonEncoder.SetEscapeHTML(false)
  147. jsonEncoder.Encode(will_write_map)
  148. a := bf.String()
  149. // fmt.Println(a)
  150. fileWriter.Mu.Lock()
  151. fileWriter.Writer.WriteString(a)
  152. fileWriter.Mu.Unlock()
  153. // fileWriter.WriteString("\n")
  154. }

从0写一个Golang日志处理包的更多相关文章

  1. 用extjs6.0写一个点击新建窗口的功能

    一.写一个按钮 注意id { id: 'ListEdit', text:'编辑', iconCls:'x-fa fa-edit' } 二.写新建的页面 下面我新建的是表单,有几点需要注意的: ① 因为 ...

  2. Extjs6(二)——用extjs6.0写一个系统登录及注销

    本文基于ext-6.0.0 一.写login页 1.在view文件夹中创建login文件夹,在login中创建文件login.js和loginController.js(login.js放在class ...

  3. Extjs6(三)——用extjs6.0写一个简单页面

    本文基于ext-6.0.0 一.关于border布局 在用ext做项目的过程中,最常用到的一种布局就是border布局,现在要写的这个简单页面也是运用border布局来做.border布局将页面分为五 ...

  4. 如何自己写一个公用的NPM包

    以markdown-clear,创建过程为例,讲解整个NPM包创建和发布流程 1 如何创建一个包 1.1 创建并使用一个工程 在GitHub上新建一个仓库,其名markdown-clear clone ...

  5. 07 python学习笔记-写一个清理日志的小程序(七)

    #删掉三天前的日志 #1.获取到所有的日志文件, os.walk #2.获取文件时间 android 2019-09-27 log,并转成时间戳 #3.获取3天前的时间 time.time() - 6 ...

  6. 使用TypeScript给Vue 3.0写一个指令实现组件拖拽

    最近在用vue3重构后台的一个功能.一个弹窗组件,弹出一个表单.然后点击提交. 早上运维突然跑过来问我,为啥弹窗挡住了下边的表格的数据,我添加的时候,都没法对照表格来看了.你必须给我解决一下. 我参考 ...

  7. 写一个Windows上的守护进程(4)日志其余

    写一个Windows上的守护进程(4)日志其余 这次把和日志相关的其他东西一并说了. 一.vaformat C++日志接口通常有两种形式:流输入形式,printf形式. 我采用printf形式,因为流 ...

  8. 用weexplus从0到1写一个app

    说明 基于wexplus开发app是来新公司才接触的,之前只是用过weex体验过写demo,当时就被用vue技术栈来开发app的开发体验惊艳到了,这个开发体验比react native要好很多,对于我 ...

  9. 基于WebQQ3.0协议写一个QQ机器人

    最近公司需要做个qq机器人获取qq好友列表,并且能够自动向选定的qq好友定时发送消息.没有头绪,硬着头皮上 甘甜的心情瞬间变得苦涩了 哇 多捞吆 1.WEBQQ3.0登陆协议 进入WEBQQ, htt ...

随机推荐

  1. 测试人员应该掌握的oracle知识体系

    闲来无事,总结了一下,软件测试人员应该掌握的基本的oracle数据库知识体系 1.安装 1.1 oracle安装 1.2 oracle升级 1.3 oracle补丁 2.管理 2.1数据库创建(dbc ...

  2. python 批量重命名文件名字

    import os print(os.path) img_name = os.listdir('./img') for index, temp_name in enumerate(img_name): ...

  3. ThreadLocal 原理

    ThreadLocal是什么 ThreadLocal是一个本地线程副本变量工具类.主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用, ...

  4. hostapd阅读(openwrt)-3

    从官网下载相对而言比较干净的源码版本http://w1.fi/hostapd/,然后将其移植到openwrt下,方便在源码阅读时候进行调试编译,移植的过程总结如下心得. 1. openwrt编译与cl ...

  5. laravel 资源控制器方法列表

    以 PostController 控制器的每个方法都有对应的请求方式.路由命名.URL.方法名和业务逻辑约定. HTTP请求方式 URL 控制器方法 路由命名 业务逻辑描述 GET post inde ...

  6. SpringCloud系列使用Eureka进行服务治理

    1. 什么是微服务? "微服务"一词来自国外的一篇博文,网站:https://martinfowler.com/articles/microservices.html 如果您不能看 ...

  7. Python os.fdatasync() 方法

    概述 os.fdatasync() 方法用于强制将文件写入磁盘,该文件由文件描述符fd指定,但是不强制更新文件的状态信息.高佣联盟 www.cgewang.com 如果你需要刷新缓冲区可以使用该方法. ...

  8. luogu P4948 数列求和 推式子 简单数学推导 二项式 拉格朗日插值

    LINK:数列求和 每次遇到这种题目都不太会写.但是做法很简单. 终有一天我会成功的. 考虑类似等比数列求和的东西 帽子戏法一下. 设\(f(k)=\sum_{i=1}^ni^ka^i\) 考虑\(a ...

  9. 5.22 noip模拟赛

    本来我是不想写的,无奈不会写.蒟蒻 考场就是想不出来 今天得到了100分额外水过了100分我是真的失败.还有一个根本不会check 感觉自己非常之菜. 这道题是这样的 还行吧比较有意思 首先确立一个真 ...

  10. springboot多数据源启动报错:required a single bean, but 6 were found:

    技术群: 816227112 参考:https://stackoverflow.com/questions/43455869/could-not-autowire-there-is-more-than ...