Go实现日志2——支持结构化和hook

代码保存在:https://github.com/liwook/Go-projects/tree/main/log/sulogV2​​​​​​​

1.日志结构化

日志记录的事件以结构化格式(键值对,或通常是 JSON)表示,随后可以通过编程方式对其进行解析,便于对日志进行监控、警报、审计等其他形式的分析。

结构化的格式例子。

time=2023-10-08T21:09:03.912+08:00 level=INFO msg=TextHandler 姓名=陈

那这里就要回顾下(entry).log方法。

func (e *Entry) log(level Level, msg string) {e.Message = msg,e.Time = time.Now(),e.Level = levele.File, e.Line, e.Func = file, line, runtime.FuncForPC(pc).Name()e.Func = e.Func[strings.LastIndex(e.Func, "/")+1:]e.write()
}func (e *Entry) write() {e.logger.mu.Lock()defer e.logger.mu.Unlock()e.logger.opt.formatter.Format(e)e.logger.opt.output.Write(e.Buffer.Bytes())
}

在e.write()中,其主要是两步:按照规定的格式化数据后,把信息存在e.Buffer中;然后经由e.write()写入。重点在格式化数据那步,所以我们要看e.format方法。

那就回到Formatter接口。而JSON类型的是已经结构化的,重点来看看TEXT文本格式的。

文本格式结构化

在formatter.go文件中添加些常量:

//formatter.go
const (defaultTimestampFormat = time.RFC3339KeyMsg                 = "msg"KeyLevel               = "level"KeyTime                = "time"KeyFunc                = "func"KeyFile                = "file"
)

 在formatter_text.go中进行修改添加:

//formatter_text.go
// 格式是: 时间 日志等级 文件:所在行号 函数名称 日志内容
func (f *TextFormatter) Format(e *Entry) error {if !f.DisableTimestamp {if f.TimestampFormat == "" {f.TimestampFormat = time.RFC3339}f.appendKeyValue(e.Buffer, KeyTime, e.Time.Format(f.TimestampFormat))}f.appendKeyValue(e.Buffer, KeyLevel, LevelName[e.Level])    //添加日志等级if !e.logger.opt.disableCaller {if e.File != "" {short := e.Filefor i := len(e.File) - 1; i > 0; i-- {if e.File[i] == '/' {short = e.File[i+1:]break}}//添加函数名和文件名f.appendKeyValue(e.Buffer, KeyFunc, short)f.appendKeyValue(e.Buffer, KeyFile, e.File+":"+strconv.Itoa(e.Line))}}f.appendKeyValue(e.Buffer, KeyMsg, e.Message)e.Buffer.WriteString("\n")return nil
}//上一节的写法,没有结构化的
func (f *TextFormatter) Format(e *Entry) error {if !f.DisableTimestamp {if f.TimestampFormat == "" {f.TimestampFormat = time.RFC3339}e.Buffer.WriteString(fmt.Sprintf("%s %s", e.Time.Format(f.TimestampFormat), LevelNameMapping[e.Level]))} else {e.Buffer.WriteString(LevelNameMapping[e.Level])}if e.File != "" {short := e.Filefor i := len(e.File) - 1; i > 0; i-- {if e.File[i] == '/' {short = e.File[i+1:]break}}e.Buffer.WriteString(fmt.Sprintf(" %s:%d %s", short, e.Line, e.Func))}e.Buffer.WriteString(" ")e.Buffer.WriteString(e.Message)e.Buffer.WriteByte('\n')return nil
}

 新的Format方法也好理解,主要是增添了调用appendKeyValue方法,该方法是进行键值对格式书写的。

func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value any) {if b.Len() > 0 {b.WriteByte(' ')}b.WriteString(key)b.WriteByte('=')f.appendValue(b, value)
}func (f *TextFormatter) appendValue(b *bytes.Buffer, value any) {stringVal, ok := value.(string)if !ok {stringVal = fmt.Sprint(value)b.WriteString(stringVal)} else {b.WriteString(fmt.Sprintf("%q", stringVal)) //这样就是加""符号的}
}

到现在TEXT文本格式的也可以进行结构化输出了。

sulog.Info("hello info")//输出结果
time="2024-01-29T11:45:33+08:00" level="INFO" func="main.go" file="D:/code/gWork/log/sulogV2/cmd/main.go:42" msg="hello info"

增添WithFieldWithFields

做到这一步还不够,还有这种需求,想额外输出name="li" age=13这种的。就不单单是输出msg=...。

level="INFO" func="main.go" file="D:/code/gWork/log/sulogV2/cmd/main.go:42" msg="hello info" name="li" age=13

代码使用例子:两个方法WithFieldWithFields

	sulog.WithField("age", "11").Info("ok withField")sulog.WithFields(sulog.Fields{"name":  "li","age":   32,}).Info("ok withFields")

调用WithField后,还可以继续调用,说明其返回值应该是logger或者entry。

在logger.go中添加这两个方法,该方法返回值是*Entry。这两个方法都需要从entryPool中获取一个entry。

//optins.go
// Fields type, used to pass to `WithFields`.
type Fields map[string]any//logger.go
func (l *logger) WithField(key string, value any) *Entry {entry := l.entry()defer l.releaseEntry(entry)return entry.WithField(key, value)
}func (l *logger) WithFields(fields Fields) *Entry {entry := l.entry()defer l.releaseEntry(entry)return entry.WithFields(fields)
}//全局的 std
func WithField(key string, value any) *Entry {return std.WithField(key, value)
}func WithFields(fields Fields) *Entry {return std.WithFields(fields)
}

 那接着就到了结构体Entry中了。(Entry).WithField也是调用WithFields。

而在WithFields方法的参数中,fields的键值对的值是any类型,所以需要用到reflcect来进行类型判别筛选。WithFields返回值是*Entry,该方法中是返回了一个新Entry,所以需要拷贝*logger,data属性。

//entry.go
type Entry struct {//该结构体其他的没有改变,只是DataMap不再使用,改为使用Data,基本是一样的意思的// DataMap map[string]any //为了日志是json格式使用的Data    Fields //保存WithField中的数据.................
}// Add a single field to the Entry.
func (entry *Entry) WithField(key string, value any) *Entry {return entry.WithFields(Fields{key: value})
}// Add a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {data := make(Fields, len(entry.Data)+len(fields))//为了可以这样使用sulog.WithField("name","li").WithField("addr","zhong").Info(1)for k, v := range entry.Data {data[k] = v}for k, v := range fields {isErrField := falseif t := reflect.TypeOf(v); t != nil {//如果value类型是函数类型,是不符合要求的if t.Kind() == reflect.Func || (t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Func) {isErrField = true}}if isErrField {tmp := fmt.Sprintf("can not add field %q", k)fmt.Println(tmp)} else {data[k] = v}}return &Entry{logger: entry.logger, Data: data}
}

WithFields返回*Entry后可以继续打印日志,说明Entry也需要实现Debug这些方法。

// entry打印方法
func (e *Entry) Debug(args ...any) {e.Log(3, DebugLevel, args...)        //3是调用 runtime.Caller() 时,传入的栈深度
}// 自定义格式
func (e *Entry) Debugf(format string, args ...any) {e.Logf(3, DebugLevel, format, args...)
}

在sulog.WithField("age", "11").Info("ok withField")代码中,WithField后会进行releseEntry,即entry.Buffer也被重置了。那要获取到之前的Buffer的话,那就需要在WithFields中拷贝entry.Buffer(或者重新new),这可能会浪费些性能。

func (entry *Entry) WithFields(fields Fields) *Entry {data := make(Fields, len(entry.Data)+len(fields)).........................//return &Entry{logger: entry.logger, Data: data}return &Entry{logger: entry.logger, Data: data,Buffer:entry.Buffer}//拷贝Buffer
}

 再看回到使用entry.Buffer的地方(entry).log方法中,这里是没有对entry.Buffer进行new的,直接使用的话,是会出问题的。

所以我们也可以仿照entryPool那样,也使用sync.Pool类型来做一个Buffer池。

在logger中添加sync.Pool类型的Buffer池

在logger结构体中添加如下

type logger struct {.............entryPool  *sync.PoolbufferPool *sync.Pool    //添加Buffer池
}func New(opts ...Option) *logger {logger := &logger{opt: initOptions(opts...), Hooks: make(LevelHooks)}logger.entryPool = &sync.Pool{New: func() any { return entry(logger) }}//初始化bufferPoollogger.bufferPool = &sync.Pool{New: func() any { return new(bytes.Buffer) }}return logger
}//entry.go
//创建entry  的方法就有了改变,不再需要new(bytes.Buffer)
func entry(logger *logger) *Entry {return &Entry{logger: logger,// Buffer:  new(bytes.Buffer),// DataMap: make(map[string]any, 5)Data: make(map[string]any, 5),}
}

 之后修改(Entry).log方法,在其中通过bufferPool获取Buffer。

func (e *Entry) log(depth int, level Level, msg string) {e.Time = time.Now()e.Level = levele.Message = msgif !e.logger.opt.disableCaller {if pc, file, line, ok := runtime.Caller(depth); !ok {e.File = "???"e.Func = "???"} else {e.File, e.Line, e.Func = file, line, runtime.FuncForPC(pc).Name()e.Func = e.Func[strings.LastIndex(e.Func, "/")+1:]}}bufPool := e.logger.bufferPoolbuffer := bufPool.Get().(*bytes.Buffer)//获取buffere.Buffer = bufferdefer func() {    //使用完后,放回bufferPool池中e.Buffer = nilbuffer.Reset()bufPool.Put(buffer)}()e.write()
}

而且这样写,使用sulog.Info("info")也正常了,因为创建entry时候是没有new(bytes.Buffer)。

接着来看看测试例子:

sulog.WithField("name", "11").Info("ok withField")
sulog.WithFields(sulog.Fields{"name": "li","age":  32,}).Info("ok withFields")//打印结果
time="2024-01-29T13:52:42+08:00" level="INFO" func="main.go" file="D:/code/gWork/log/sulogV2/cmd/main.go:46" msg="ok withField" name="11"
time="2024-01-29T13:52:42+08:00" level="INFO" func="main.go" file="D:/code/gWork/log/sulogV2/cmd/main.go:50" msg="ok withFields" age=32 name="li"

区分WithField中的键"level" 和 sulog.Info()中的"level"

到这里是基本完成了,但还是可能会有问题,比如:

sulog.WithField("level", "debug").Info("ok withField")

这种日志等级应该是info,但是在WithField中level写到是debug,那就会有冲突了。时间和函数名那些要是也这样写的话,也会有冲突的。那该怎么做呢,那就做个区分,只要是在WithField中的,一致使用field.*来表示,level就用field.level来表示。

在formatter.go文件中添加如下代码:

该函数功能是查看WithField中要是有"level","file"这些,就把key修改成"fields.levle"等等。

//data是WithField方法中的数据
func prefixFieldClashes(data Fields) {for k, v := range data {switch k {case KeyMsg:data["fields."+KeyMsg] = vdelete(data, KeyMsg)case KeyLevel:data["fields."+KeyLevel] = vdelete(data, KeyLevel)case KeyFunc:data["fields."+KeyFunc] = vdelete(data, KeyFunc)case KeyTime:data["fields."+KeyTime] = vdelete(data, KeyTime)case KeyFile:data["fields."+KeyFile] = vdelete(data, KeyFile)}}
}

那么在JSON格式和TEXT格式的Format方法中使用。

func (f *JsonFormatter) Format(e *Entry) error {data := make(Fields, len(e.Data)+5)prefixFieldClashes(e.Data)for k, v := range e.Data {data[k] = v}if !f.DisableTimestamp {if f.TimestampFormat == "" {f.TimestampFormat = time.RFC3339}data[KeyTime] = e.Time.Format(f.TimestampFormat)}data[KeyLevel] = LevelName[e.Level]if e.File != "" {data[KeyFile] = e.File + ":" + strconv.Itoa(e.Line)data[KeyFunc] = e.Func}data[KeyMsg] = e.Messagereturn sonic.ConfigDefault.NewEncoder(e.Buffer).Encode(data)
}//TEXT文本格式的
func (f *TextFormatter) Format(e *Entry) error {prefixFieldClashes(e.Data)if !f.DisableTimestamp {................}...............f.appendKeyValue(e.Buffer, KeyMsg, e.Message)//加上WithField()中的for k, v := range e.Data {f.appendKeyValue(e.Buffer, k, v)}e.Buffer.WriteString("\n")return nil
}

来看看测试结果

	sulog.SetOptions(sulog.WithDisableCaller(true))sulog.SetOptions(sulog.WithFormatter(&sulog.JsonFormatter{DisableTimestamp: true}))sulog.WithFields(sulog.Fields{"level": "info","name":  "lihai","msg":   "this field message",}).Info("ok withField")//打印结果
{"fields.msg":"this field message","name":"lihai","fields.level":"info","level":"INFO","msg":"我们ok withField"}

在使用该日志时,鼓励用sulog.WithFields(log.Fields{}).Infof() 这种方式替代sulog.Infof("Failed to send event %s to topic %s"), 也就是不是用 %s,%d 这种方式格式化,而是直接传入变量 event,topic 给 log.Fields ,这样就显得结构化日志输出,很人性化美观。

2.支持hook

钩子(Hooks),可以理解成函数,可以让我们在日志输出前或输出后进行一些额外的处理。常见的使用场景包括发送邮件、写入数据库、上传到云端等。

一个日志,是有很多日志等级的,那就可能需要筛选出符合条件的等级才能触发hook。

在logger结构体中添加变量Hooks:

即是可以添加多个日志等级的hook,一个日志等级可以有多个hook。

//新添加hooks.go文件,添加该类型
type LevelHooks map[Level][]Hook//logger.go
type logger struct {...................Hooks LevelHooks //多个日志等级的多个hook
}

定义Hook

其肯定是有对应的可执行的日志等级列表和执行的函数这两个的。

给用户使用,那我们可以定义成接口,命名为Hook。

用户实现Hook接口即可。

type Hook interface {Levels() []LevelFire(*Entry) error
}

所以在Hook中用户需要做的就是在Fire方法中定义想如何操作这一条日志的方法,在Levels方法中定义想展示的日志级别

给LevelHooks添加方法

那肯定是有添加hook(Add)执行所有hook(Fire)的方法。

func (hooks LevelHooks) Add(hook Hook) {for _, level := range hook.Levels() {hooks[level] = append(hooks[level], hook)}
}//执行日志等级列表中全部hook
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {for _, hook := range hooks[level] {if err := hook.Fire(entry); err != nil {return err}}return nil
}

那在logger结构体中增加AddHook方法。

// 添加hook
func (l *logger) AddHook(hook Hook) {//用锁,保证了执行hook时候就不能添加hookl.mu.Lock()defer l.mu.Unlock()l.Hooks.Add(hook)
}func AddHook(hook Hook) {std.AddHook(hook)
}

很明显,(LevelHooks).Add在(logger).AddHook中调用了,而Fire没有使用,那这样的话,钩子函数就没有能执行。

执行hook

那其应该是在哪被调用呢?应该在(Entry).log中。

func (e *Entry) log(depth int, level Level, msg string) {............if !e.logger.opt.disableCaller {.....................}e.fireHooks()    //执行hook, 就是在这添加这句bufPool := e.logger.bufferPool...............e.write()
}//这种写法不好,执行hook时候一直霸占锁,不妥
func (entry *Entry) fireHooks() {entry.logger.mu.Lock()err := entry.logger.Hooks.Fire(entry.Level, entry)if err != nil {fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)}entry.logger.mu.Unlock()
}

一般是很容易想到上面的fireHooks的实现,但这种实现不好,要是hook是比较耗时的操作,那就会一直霸占锁,可以会引起阻塞。

解决办法:我们可以用个临时变量把所有的hook拷贝过来,之后就可以释放锁,然后就可以执行hook。这样就不会长时间霸占锁。

func (entry *Entry) fireHooks() {var tmpHooks LevelHooks    //临时变量entry.logger.mu.Lock()tmpHooks = make(LevelHooks, len(entry.logger.Hooks))//进行拷贝for k, v := range entry.logger.Hooks {tmpHooks[k] = v}entry.logger.mu.Unlock()err := tmpHooks.Fire(entry.Level, entry) //执行hookif err != nil {fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)}
}

测试:

实现Hook接口。

type hook struct{}func (h *hook) Levels() []sulog.Level {// return sulog.AllLevelsreturn []sulog.Level{sulog.InfoLevel, sulog.DebugLevel}//只有info和debug两种等级
}func (h *hook) Fire(entry *sulog.Entry) error {fmt.Printf("send emial. this is a hook func:%v\n", entry.Data)return nil
}func main() {sulog.SetOptions(sulog.WithDisableCaller(true))sulog.SetOptions(sulog.WithFormatter(&sulog.TextFormatter{DisableTimestamp: true}))sulog.AddHook(&hook{})sulog.WithField("name", "11").Info("ok withField")fmt.Println()sulog.WithField("country", "China").Error("ok withField")
}

效果:

this is a hook func:map[name:11]
level="INFO" msg="ok withField" name="11"level="ERROR" msg="ok withField" country="China"

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/735418.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Googlenet网络架构

原文链接:[1409.4842v1] Going Deeper with Convolutions (arxiv.org) 图源:深入解读GoogLeNet网络结构(附代码实现)-CSDN博客 表截自原文 以下📒来自博客深入解读GoogLeNet网络结构(附代码实现&#xff0…

【顶刊|修正】多区域综合能源系统热网建模及系统运行优化【复现+延伸】

目录 主要内容 部分代码 结果一览 下载链接 主要内容 该程序复现《多区域综合能源系统热网建模及系统运行优化》模型并进一步延伸,基于传热学的基本原理建立了区域热网能量传输通用模型,对热网热损方程线性化实现热网能量流建模&#xff0…

使用docker-compose编排ruoyi项目

目录 一、开始部署 1.拉取ruoyi代码 2.拉取node镜像 3.拉取maven镜像 4.在/root/ruoyi/java下写一个Dockerfile用于后端Java环境 5.拉取MySQL,Redis,Nginx镜像 6.在/root/java目录下写一个nginx.conf 7.在/root/ruoyi目录下写docker-compose.yml文…

Idea导入Maven项目

方法一:使用Maven面板 方法二:在项目结构中设置,在最后一步中选择pom.xml。

js【详解】bind()、call()、apply()( 含手写 bind,手写 call,手写 apply )

必备知识点:js 【详解】函数中的 this 指向_js function this-CSDN博客 https://blog.csdn.net/weixin_41192489/article/details/123093256 bind、call、apply 的相同点 都是Function原型上的方法用途都是改变 this 的指向第一个参数都是新的 this bind、call、app…

前端学习之列表标签

目录 有序列表 结果 无序标签 结果 数据标签 结果 有序列表 &#xff08;注&#xff1a;注释是解释&#xff09; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Document</title> </…

SpringBoot实现 PDF 添加水印

方案一&#xff1a;使用 Apache PDFBox 库 ①、依赖 <dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.24</version> </dependency>②、添加水印 public class PdfoxWaterma…

蓝桥集训之日期差值

蓝桥集训之日期差值 模版&#xff1a;判断闰年 总天数 月份天数 #include <iostream>#include <cstring>#include <algorithm>using namespace std;const int months[]{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};int is_leap(int y){if(y % 10…

【JavaEE初阶系列】——计算机是如何工作的

目录 &#x1f388;冯诺依曼体系 ❗外存和内存的概念 ❗CPU中央处理器—人类当今科技领域巅峰之作之一 &#x1f6a9;如何衡量cpu &#x1f6a9;指令&#xff08;Instruction&#xff09; &#x1f388;操作系统&#xff08;Operating System&#xff09; &#x1f388;…

关于GPU显卡的介绍

一.关于英伟达历代产品架构 显卡是一种计算机硬件设备,也被称为显示适配器或图形处理器。目前的硬件部分主要由主板、芯片、存储器、散热器&#xff08;散热片、风扇&#xff09;等部分。显卡的主要芯片是显卡的主要处理单元。显卡上也有和计算机存储器相似的存储器&#xff0…

聊聊.NET中的连接池

在.NET中&#xff0c;连接池被广泛用于管理和优化不同类型资源的连接。连接池可以减少建立和关闭连接所需的时间和资源消耗&#xff0c;从而提高了应用程序的性能和响应能力。 HttpClient中的连接池 System.Net.Http.HttpClient 类用于发送 HTTP 请求以及从 URI 所标识的资源…

安全测试报告-模板内容

1. 概述 为检验XXXX平台 系统的安全性&#xff0c;于 XXXX年 XX 月 XX 日至 XXXX年 XX 月 XX日对目标系统进行了安全测试。在此期间测试人员将使用各 种非破坏性质的攻击手段&#xff0c;对目标系统做深入的探测分析&#xff0c;进而挖掘系统中的安 全漏洞和风险隐患。研发团队…

代码讲解:如何把3D数据转换成旋转的视频?

目录 3D数据集下载 读取binvox文件 使用matplotlib创建图 动画效果 完整代码 3D数据集下载 这里以shapenet数据集为例&#xff0c;可以访问外网的可以去直接申请下载&#xff1b;我也准备了一个备份在百度网盘的数据集&#xff0c;可以参考&#xff1a; ShapeNet简介和下…

Java适配器模式源码剖析及使用场景

文章目录 一、适配器模式介绍二、大白话理解三、 项目案例四、Java源码 一、适配器模式介绍 适配器模式(Adapter Pattern)是一种结构型设计模式,它作用于将一个类的接口转换成客户端所期望的另一种接口,从而使原本由于接口不兼容而无法一起工作的那些类可以在一起工作。它属于…

Vue3中Vue Router的使用区别

在 Vue 3 中&#xff0c;useRouter 和 useRoute 是两个用于 Vue Router 的 Composition API 函数&#xff0c;它们的用途和返回的对象不同&#xff0c;接下来详细了解一下它们的区别以及如何正确使用它们。 useRouter useRouter 用于获取 router 实例&#xff0c;这个实例提供…

macOS14.4安装FFmpeg及编译FFmpeg源码

下载二进制及源码包 二进制 使用brew安装ffmpeg : brew install ffmpeg 成功更新到ffmpeg6.1 下载FFmpeg源码

MIT 6.858 计算机系统安全讲义 2014 秋季(三)

译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 SSL/TLS 和 HTTPS **注意&#xff1a;**这些讲座笔记略有修改自 2014 年 6.858 课程网站上发布的笔记。 这节课涉及两个相关主题&#xff1a; 如何在比 Kerberos 更大规模上加密保护网络通信&#xff1f; 技术&#xf…

LVS (Linux Virtual server)集群介绍

一 集群和分布式 &#xff08;一&#xff09;系统性能扩展方式&#xff1a; Scale UP&#xff1a;垂直扩展&#xff0c;向上扩展,增强&#xff0c;性能更强的计算机运行同样的服务 &#xff08;即升级单机的硬件设备&#xff09; Scale Out&#xff1a;水平扩展&#xff0…

Anaconda prompt运行打开jupyter notebook 指令出错解决方案

一、打不开jupyter notebook网页 报错如下&#xff1a; Traceback (most recent call last): File “D:\anaconda3\lib\site-packages\notebook\traittypes.py”, line 235, in _resolve_classes klass self._resolve_string(klass) File “C:\Users\DELL\AppData\Roaming\Py…

单文件组件SFC及Vue CLI脚手架的安装使用

单文件组件SFC及Vue CLI脚手架的安装使用 Vue 单文件组件&#xff08;又名 *.vue 文件&#xff0c;缩写为 SFC&#xff09;是一种特殊的文件格式&#xff0c;它允许将 Vue 组件的模板、逻辑 与 样式封装在单个文件中。 为什么要使用 SFC 使用 SFC 必须使用构建工具&#xff…