从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 ...
随机推荐
- 图文详解压力测试工具JMeter的安装与使用
压力测试是目前大型网站系统的设计和开发中不可或缺的环节,通常会和容量预估等工作结合在一起,穿插在系统开发的不同方案.压力测试可以帮助我们及时发现系统的性能短板和瓶颈问题,在这个基础在上再进行针对性的性 ...
- python-在python3中使用容联云通讯发送短信验证码
容联云通讯是第三方平台,能够提供短信验证码和语音通信等功能,这里只测试使用短信验证码的功能,因此只需完成注册登录(无需实名认证等)即可使用其短信验证码免费测试服务,不过免费测试服务只能给控制台中指定的 ...
- async基本使用
async函数在使用上很简单,我们来看一下下面的例子 async function add(a,b){ return a+b } add(1,2).then((res) =>{ consoel. ...
- python基础--14大内置模块(上)
python的内置模块(重点掌握以下模块) 什么是模块 常见的场景:一个模块就是一个包含了python定义和声明的文件,文件名就是模块名字加上.py的后缀. 但其实import加载的模块分为四个通用类 ...
- 构建一个基于事件分发驱动的EventLoop线程模型
在之前的文章中我们详细介绍过Netty中的NioEventLoop,NioEventLoop从本质上讲是一个事件循环执行器,每个NioEventLoop都会绑定一个对应的线程通过一个for(;;)循环 ...
- python xpath的基本用法
XPath是一种在XML文档中查找信息的语言,使用路径表达式在XML文档中进行导航.学习XPath需要对XML和HTML有基本的了解. 在XPath中,有七种类型的节点:文档(根)节点.元素.属性.文 ...
- 萌新学渗透系列之Hack The Box_Devel
我将我的walkthrough过程用视频解说的形式记载 视频地址https://www.bilibili.com/video/BV1Ck4y1B7DB 一是因为看我视频的后来者应该都是刚入门的新手,视 ...
- php iamp 接收邮件,收取邮件,获取邮件列表
每次想写的时候吧,提笔忘字.等到再次使用,又得想半天,,,,,好尴尬. 这次一边做一边写. 心得,程序员从菜鸟往老鸟转变的重要一步,学英语,看文档,在此我万分感谢鸟哥,,,,没他php哪有官方的中文注 ...
- MVC + EFCore 项目实战 - 数仓管理系统8 - 数据源管理下--数据源预览
上篇我们完成了数据源保存功能,并顺便看了保存后的数据源列表展示功能. 本篇我们开始开发预览功能,用户预览主要步骤: 1.点击数据源卡片预览按钮 2.查看数据源包含的表 3.点击表名,预览表中数据 ...
- 微服务迁移记(五):WEB层搭建(4)-简单的权限管理
一.redis搭建 二.WEB层主要依赖包 三.FeignClient通用接口 以上三项,参考<微服务迁移记(五):WEB层搭建(1)> 四.SpringSecurity集成 参考:< ...