从0写一个Golang日志处理包
WHY
日志概述
日志几乎是每个实际的软件项目从开发到最后实际运行过程中都必不可少的东西。它对于查看代码运行流程,记录发生的事情等方面都是很重要的。
一个好的日志系统应当能准确地记录需要记录的信息,同时兼具良好的性能,接下来本文将从0写一个Golang日志处理包。
通常Go应用程序多为并发模型应用,在并发处理实际应用的过程中也无法避免并发地调用日志方法。
通常来说,Go中除非声明方法是协程安全的,否则最好都视为协程不安全,Go中默认的日志方法为log,该方法协程安全,但是功能有限。
第三方Go日志包如ZAP、logrus等均为较为不错的日志库,也提供了较多的配置选项,但是对于高度定制需求的日志则有些不适用,从0开始自己写一个就较为适用。
设计概述
- 按json方式输出到文件
- 日志类型支持丰富
- 有日志分级设定
- 按天分隔日志文件
- 异常退出能把未写入的日志写完
HOW
整体流程

怎么填充日志内容
要做到按json方式输出到文件,并且支持丰富的类型,第一个能想到的则是Golang中强大的interface
设计两种类型分别用于接收map型和string型日志,再根据日志分级设定分别暴露出对外接口:
func Debug(msg map[string]interface{}) {
writeLog("DEBUG", msg)
}
func DebugMsg(msg interface{}) {
writeLog("DEBUG", map[string]interface{}{"msg": msg})
}
func Info(msg map[string]interface{}) {
writeLog("INFO", msg)
}
func InfoMsg(msg interface{}) {
writeLog("INFO", map[string]interface{}{"msg": msg})
}
func Warn(msg map[string]interface{}) {
writeLog("WARN", msg)
}
func WarnMsg(msg interface{}) {
writeLog("WARN", map[string]interface{}{"msg": msg})
}
func Error(msg map[string]interface{}) {
writeLog("ERROR", msg)
}
func ErrorMsg(msg interface{}) {
writeLog("ERROR", map[string]interface{}{"msg": msg})
}
最终都是使用writeLog进行日志的处理,writeLog方法定义如下:
func writeLog(level string, msg map[string]interface{})
用哪种方式写入文件
Golang对于文件的写入方式多种多样,通常来讲最后都是使用操作系统的磁盘IO方法把数据写入文件
在选型上这块使用bufio方式来构建,使用默认的4096长度,如果收集的日志长度超过了缓冲区长度则自动将内容写入到文件,同时增加一组定时器,每秒将缓冲区内容写入到文件中,这样在整体性能上较为不错
处理协程抢占问题
针对多协程抢占的问题,Golang提供有两个比较标准的应对方式:使用channel或者加锁
在这里采用读写锁的方式来进行处理bufio,bufio的WriteString方法需串行处理,要不然会导致错误,而Flush方法可以多协程同时操作
基于这些特性,我们在使用WriteString方法的时候使用写锁,使用Flush方法时采用读锁:
fileWriter.Mu.Lock()
fileWriter.Writer.WriteString(a)
fileWriter.Mu.Unlock()
fileWriter.Mu.RLock()
err = fileWriter.Writer.Flush()
if err != nil {
log.Println("flush log file err", err)
}
fileWriter.Mu.RUnlock()
跨天的日志文件处理
首先明确一个问题,在每日结束次日开始时将打开一个新的日志文件,那么还在缓冲区未完成刷新的数据怎么处理呢?
bufio提供了Reset方法,但是该方法注释说明将丢弃未刷新的数据而直接重新指向新的io writer,因此我们不能直接使用该方法,否则这个时间节点附近的数据将会丢掉
实际测试证明如果先关闭原IO,再重新创建新的文件描述符,最后调用Reset方法指向新的描述符过后这段时间将会丢掉达约20ms的数据
基于此,我们使用了二级指针:

1.判断当前日期是否就是今天,如果是则等待下个判断周期,如果不是则开始准备指针重定向操作
2.判断哪一个文件描述符为空,如果为空则为其创建新的描述符,并指定配对的filebuffer,如果不为空则说明它就是当前正在操作的文件
3.将filewriter指向新的filebuffer
4.对老的filebuffer进行Flush操作,之后将filebuffer和file都置为空
经过这样的操作过后,跨天的日志文件处理就不会有数据丢失的情况了
if today != time.Now().Day() {
today = time.Now().Day()
if file[0] == nil {
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)
if err != nil {
log.Fatal("open log file err: ", err)
}
fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
fileWriter.Writer = fileBuffer[0]
if fileBuffer[1].Buffered() > 0 {
fileBuffer[1].Flush()
}
fileBuffer[1] = nil
file[1].Close()
file[1] = nil
} else if file[1] == nil {
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)
if err != nil {
log.Fatal("open log file err: ", err)
}
fileBuffer[1] = bufio.NewWriterSize(file[1], 4096)
fileWriter.Writer = fileBuffer[1]
if fileBuffer[0].Buffered() > 0 {
fileBuffer[0].Flush()
}
fileBuffer[0] = nil
file[0].Close()
file[0] = nil
}
}
异常退出的处理
一个基本的概念就是程序在操作系统退出的时候通常都会得到系统信号,比如linux的kill操作就是给应用程序发送系统信号
信号分很多种,比如常见的ctrl+c对应的信号则是Interrupt信号,这块去搜索“posix信号”也有详细的解释说明
基于此,常规的异常处理我们可以捕获系统信号然后做一些结束前的处理操作,这样的信号可以在多个包同时使用,均会收到信号,不用担心信号强占的问题
比如这个包在接收到退出信号时则刷新所有缓存数据并关闭所有文件描述符:
func exitHandle() {
<-exitChan
if file != nil {
if fileWriter.Writer.Buffered() > 0 {
fileWriter.Writer.Flush()
}
//及时关闭file句柄
if file[0] != nil {
file[0].Close()
}
if file[1] != nil {
file[1].Close()
}
}
os.Exit(1) //使用os.Exit强行关掉
}
完整源码
package logger
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"os/signal"
"xxx/config"
"sync"
"syscall"
"time"
)
var file []*os.File
var err error
var fileBuffer []*bufio.Writer
var exitChan chan os.Signal
type fileWriterS struct {
Writer *bufio.Writer
Mu sync.RWMutex
}
var fileWriter fileWriterS
var today int
func LoggerInit() {
filePath := config.Get("app", "log_path") //config处可以直接换成自己的config甚至直接写死
_, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
os.MkdirAll(filePath, os.ModePerm)
} else {
log.Fatal("log path err:", err)
}
}
file = make([]*os.File, 2)
file[0] = nil
file[1] = nil
fileBuffer = make([]*bufio.Writer, 2)
fileBuffer[0] = nil
fileBuffer[1] = nil
today = time.Now().Day()
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)
if err != nil {
log.Fatal("open log file err: ", err)
}
fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
fileWriter.Writer = fileBuffer[0]
exitChan = make(chan os.Signal)
signal.Notify(exitChan, os.Interrupt, os.Kill, syscall.SIGTERM)
go exitHandle()
go func() {
time.Sleep(1 * time.Second)
for {
if fileWriter.Writer.Buffered() > 0 {
fileWriter.Mu.RLock()
err = fileWriter.Writer.Flush()
if err != nil {
log.Println("flush log file err", err)
}
fileWriter.Mu.RUnlock()
}
time.Sleep(1 * time.Second)
if today != time.Now().Day() {
today = time.Now().Day()
if file[0] == nil {
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)
if err != nil {
log.Fatal("open log file err: ", err)
}
fileBuffer[0] = bufio.NewWriterSize(file[0], 4096)
fileWriter.Writer = fileBuffer[0]
if fileBuffer[1].Buffered() > 0 {
fileBuffer[1].Flush()
}
fileBuffer[1] = nil
file[1].Close()
file[1] = nil
} else if file[1] == nil {
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)
if err != nil {
log.Fatal("open log file err: ", err)
}
fileBuffer[1] = bufio.NewWriterSize(file[1], 4096)
fileWriter.Writer = fileBuffer[1]
if fileBuffer[0].Buffered() > 0 {
fileBuffer[0].Flush()
}
fileBuffer[0] = nil
file[0].Close()
file[0] = nil
}
}
}
}()
}
func exitHandle() {
<-exitChan
if file != nil {
if fileWriter.Writer.Buffered() > 0 {
fileWriter.Writer.Flush()
}
if file[0] != nil {
file[0].Close()
}
if file[1] != nil {
file[1].Close()
}
}
os.Exit(1)
}
func Debug(msg map[string]interface{}) {
writeLog("DEBUG", msg)
}
func DebugMsg(msg interface{}) {
writeLog("DEBUG", map[string]interface{}{"msg": msg})
}
func Info(msg map[string]interface{}) {
writeLog("INFO", msg)
}
func InfoMsg(msg interface{}) {
writeLog("INFO", map[string]interface{}{"msg": msg})
}
func Warn(msg map[string]interface{}) {
writeLog("WARN", msg)
}
func WarnMsg(msg interface{}) {
writeLog("WARN", map[string]interface{}{"msg": msg})
}
func Error(msg map[string]interface{}) {
writeLog("ERROR", msg)
}
func ErrorMsg(msg interface{}) {
writeLog("ERROR", map[string]interface{}{"msg": msg})
}
func writeLog(level string, msg map[string]interface{}) {
will_write_map := make(map[string]interface{})
t := time.Now()
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)
will_write_map["level"] = level
for p, v := range msg {
will_write_map[p] = v
}
//buffer满了会自动flush
bf := bytes.NewBuffer([]byte{})
jsonEncoder := json.NewEncoder(bf)
jsonEncoder.SetEscapeHTML(false)
jsonEncoder.Encode(will_write_map)
a := bf.String()
// fmt.Println(a)
fileWriter.Mu.Lock()
fileWriter.Writer.WriteString(a)
fileWriter.Mu.Unlock()
// fileWriter.WriteString("\n")
}
从0写一个Golang日志处理包的更多相关文章
- 用extjs6.0写一个点击新建窗口的功能
一.写一个按钮 注意id { id: 'ListEdit', text:'编辑', iconCls:'x-fa fa-edit' } 二.写新建的页面 下面我新建的是表单,有几点需要注意的: ① 因为 ...
- Extjs6(二)——用extjs6.0写一个系统登录及注销
本文基于ext-6.0.0 一.写login页 1.在view文件夹中创建login文件夹,在login中创建文件login.js和loginController.js(login.js放在class ...
- Extjs6(三)——用extjs6.0写一个简单页面
本文基于ext-6.0.0 一.关于border布局 在用ext做项目的过程中,最常用到的一种布局就是border布局,现在要写的这个简单页面也是运用border布局来做.border布局将页面分为五 ...
- 如何自己写一个公用的NPM包
以markdown-clear,创建过程为例,讲解整个NPM包创建和发布流程 1 如何创建一个包 1.1 创建并使用一个工程 在GitHub上新建一个仓库,其名markdown-clear clone ...
- 07 python学习笔记-写一个清理日志的小程序(七)
#删掉三天前的日志 #1.获取到所有的日志文件, os.walk #2.获取文件时间 android 2019-09-27 log,并转成时间戳 #3.获取3天前的时间 time.time() - 6 ...
- 使用TypeScript给Vue 3.0写一个指令实现组件拖拽
最近在用vue3重构后台的一个功能.一个弹窗组件,弹出一个表单.然后点击提交. 早上运维突然跑过来问我,为啥弹窗挡住了下边的表格的数据,我添加的时候,都没法对照表格来看了.你必须给我解决一下. 我参考 ...
- 写一个Windows上的守护进程(4)日志其余
写一个Windows上的守护进程(4)日志其余 这次把和日志相关的其他东西一并说了. 一.vaformat C++日志接口通常有两种形式:流输入形式,printf形式. 我采用printf形式,因为流 ...
- 用weexplus从0到1写一个app
说明 基于wexplus开发app是来新公司才接触的,之前只是用过weex体验过写demo,当时就被用vue技术栈来开发app的开发体验惊艳到了,这个开发体验比react native要好很多,对于我 ...
- 基于WebQQ3.0协议写一个QQ机器人
最近公司需要做个qq机器人获取qq好友列表,并且能够自动向选定的qq好友定时发送消息.没有头绪,硬着头皮上 甘甜的心情瞬间变得苦涩了 哇 多捞吆 1.WEBQQ3.0登陆协议 进入WEBQQ, htt ...
随机推荐
- CPU核数
今天想看CPU核数,又忘记怎么看了QAQ. CPU的基本信息都被记录在/proc/cpuinfo中,一般直接cat /proc/cpuinfo就可以了. 主要是学习一下物理cpu核数/逻辑cpu核数的 ...
- accpet和connect设置超时
三次握手 TCP连接建立的开始是三次握手,通过三次交互确认连接成功,在客户端调用connect时,客户端发送sync消息给服务端,服务端收到sync消息后,返回一个ack+sync,并等待ack,客户 ...
- Unable to find a constructor that takes a String param or a valueOf() or fromString() method
Unable to find a constructor that takes a String param or a valueOf() or fromString() method 最近在做服务的 ...
- Java数组(基本+内存分析)
一.数组概念 数组即为多个相同数据类型数据的数据按一定顺序排列的集合. 二.数组的特点 1.数组有数组名.索引.元素.素组长度: 2.数组的元素可以是基本数据类型也可以是引用数据类型: ...
- Java复习总结(二)Java SE 面试题
Java SE基础知识 目录 Java SE 1. 请你谈谈Java中是如何支持正则表达式操作的? 2. 请你简单描述一下正则表达式及其用途. 3. 请你比较一下Java和JavaSciprt? 4. ...
- 微服务迁移记(五):WEB层搭建(1)
WEB层是最终表现层,注册至注册中心,引用接口层(不需要引用实现层).公共服务层.用户登录使用SpringSecurity,Session保存在redis中,权限管理没有用SpringSecurity ...
- Python大礼包-安装视频+pycharm编译器|Mac版本+64位+32位版本pycharm安装包+python安装|内附网盘链接带提取码
pycharm安装包+环境安装打包带走,附带视频教程与pdf教程. (下载链接在本文最下方) 多的不说,直接上图: Python大礼包-安装视频+pycharm编译器详细文件: 点击此处进入下载地址 ...
- 老男孩武老师的Django笔记
武老师的 Django 博客笔记 基础篇 https://www.cnblogs.com/wupeiqi/articles/5237704.html 进阶篇 https://www.cnblogs.c ...
- PHP filesize() 函数
定义和用法 filesize() 函数返回指定文件的大小. 如果成功,该函数返回文件大小的字节数.如果失败,则返回 FALSE. 语法 filesize(filename) 参数 描述 filenam ...
- PHP is_string() 函数
is_string() 函数用于检测变量是否是字符串. PHP 版本要求: PHP 4, PHP 5, PHP 7高佣联盟 www.cgewang.com 语法 bool is_string ( mi ...