gorm log with traceId 打印带有traceId信息的日志,通过context实现

背景

无论是单体项目,还是分布式项目,一个请求进来总会有一定的链路,单体项目中会调用各种方法,分布式服务中更麻烦一点,跨服务调用。于是乎,我们就希望有一个全局的traceId可以把一个请求过程中经过的所有链路的关键信息串联起来,这样的话在检索日志的时候可以带来极大的方便,根据traceId把整个链路上的日志全部打印出来。

在golang项目中,通用的写法是通过context实现traceId信息传递。那么gorm如何通过context把traceId传进去,以实现打印日志带上traceId信息呢?

我们得通过阅读源码来寻找这个问题的解决方案。

gorm源码解读

我们首先需要了解gorm日志打印是如何实现的,任意找一个sql执行方法进去,比如,查询的方法。

func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {tx = db.getInstance()if len(conds) > 0 {if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {tx.Statement.AddClause(clause.Where{Exprs: exprs})}}tx.Statement.Dest = destreturn tx.callbacks.Query().Execute(tx)
}

进一步寻找打印日志的逻辑,定位到Execute方法。在Execute方法中找到了打印日志的逻辑。

if stmt.SQL.Len() > 0 {db.Logger.Trace(stmt.Context, curTime, func() (string, int64) {sql, vars := stmt.SQL.String(), stmt.Varsif filter, ok := db.Logger.(ParamsFilter); ok {sql, vars = filter.ParamsFilter(stmt.Context, stmt.SQL.String(), stmt.Vars...)}return db.Dialector.Explain(sql, vars...), db.RowsAffected}, db.Error)}

到了这边,我们发现,日志打印调用的Trace方法的第一个传参是Context。所以,我们继续顺腾摸瓜看这个Context是通过什么方式传进来的。Context从db.Statement中获取的。所以,我们需要寻找给db.Statement赋值的方法。

func (db *DB) getInstance() *DB {if db.clone > 0 {tx := &DB{Config: db.Config, Error: db.Error}if db.clone == 1 {// clone with new statementtx.Statement = &Statement{DB:        tx,ConnPool:  db.Statement.ConnPool,Context:   db.Statement.Context,Clauses:   map[string]clause.Clause{},Vars:      make([]interface{}, 0, 8),SkipHooks: db.Statement.SkipHooks,}} else {// with clone statementtx.Statement = db.Statement.clone()tx.Statement.DB = tx}return tx}return db
}

然后,我们就在WithContext的方法中找到了把context传递进来的入口。

func (db *DB) WithContext(ctx context.Context) *DB {return db.Session(&Session{Context: ctx})
}

传Context的入口找到了,那么,gorm中如何根据context中自定义值打印日志呢?比如,Context中塞了自定义的traceId的key,value值?

我们回到前面打印日志的地方,看打印日志的方法,打印日志的Trace方法是这个接口下的一个方法。

type Interface interface {LogMode(LogLevel) InterfaceInfo(context.Context, string, ...interface{})Warn(context.Context, string, ...interface{})Error(context.Context, string, ...interface{})Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
}
func (l *logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {if l.LogLevel <= Silent {return}elapsed := time.Since(begin)switch {case err != nil && l.LogLevel >= Error && (!errors.Is(err, ErrRecordNotFound) || !l.IgnoreRecordNotFoundError):sql, rows := fc()if rows == -1 {l.Printf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-", sql)} else {l.Printf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql)}case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= Warn:sql, rows := fc()slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)if rows == -1 {l.Printf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql)} else {l.Printf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql)}case l.LogLevel == Info:sql, rows := fc()if rows == -1 {l.Printf(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-", sql)} else {l.Printf(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql)}}
}

定位到trace方法中,我们发现并没有处理Context,其实很正常。

所以,我们需要重写这个Trace方法,自定义一个log对象,实现gorm的log接口。

解决方案

直接上代码。

package mainimport ("context""encoding/json""errors""fmt""time""go.uber.org/zap""gorm.io/gorm""gorm.io/gorm/logger""gorm.io/gorm/utils""gorm.io/driver/mysql"
)func main() {zapL, err := zap.NewProduction()if err != nil {panic(err)}log := New(zapL,WithCustomFields(String("timeStamp", time.Now().Format("2006-01-02 15:04:05")),func(ctx context.Context) zap.Field {v := ctx.Value("requestId")if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String("trace", vv)}return zap.Skip()},func(ctx context.Context) zap.Field {v := ctx.Value("method")if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String("method", vv)}return zap.Skip()},),WithConfig(logger.Config{SlowThreshold:             200 * time.Millisecond,Colorful:                  false,IgnoreRecordNotFoundError: false,LogLevel:                  logger.Info,}),)mysqlConfig := mysql.Config{DSN:                       "*******", // DSN data source nameDefaultStringSize:         191,                                                                                                // string 类型字段的默认长度SkipInitializeWithVersion: false,                                                                                              // 根据版本自动配置}// your dialectordb, _ := gorm.Open(mysql.New(mysqlConfig), &gorm.Config{Logger: log})// do your thingsresult := make(map[string]interface{})ctx := context.WithValue(context.Background(), "method", "method")db.WithContext(context.WithValue(ctx, "requestId", "requestId123456")).Table("privacy_detail").Find(&result)db.WithContext(context.WithValue(context.Background(), "requestId", "requestId123457")).Table("privacy_detail").Find(&result)db.WithContext(context.WithValue(context.Background(), "requestId", "requestId123458")).Table("privacy_detail").Create(&result)log.Info(context.WithValue(context.Background(), "requestId", "requestId123456"), "msg", "args")
}// Logger logger for gorm2
type Logger struct {log *zap.Loggerlogger.ConfigcustomFields []func(ctx context.Context) zap.Field
}// Option logger/recover option
type Option func(l *Logger)// WithCustomFields optional custom field
func WithCustomFields(fields ...func(ctx context.Context) zap.Field) Option {return func(l *Logger) {l.customFields = fields}
}// WithConfig optional custom logger.Config
func WithConfig(cfg logger.Config) Option {return func(l *Logger) {l.Config = cfg}
}// SetGormDBLogger set db logger
func SetGormDBLogger(db *gorm.DB, l logger.Interface) {db.Logger = l
}// New logger form gorm2
func New(zapLogger *zap.Logger, opts ...Option) logger.Interface {l := &Logger{log: zapLogger,Config: logger.Config{SlowThreshold:             200 * time.Millisecond,Colorful:                  false,IgnoreRecordNotFoundError: false,LogLevel:                  logger.Warn,},}for _, opt := range opts {opt(l)}return l
}// LogMode log mode
func (l *Logger) LogMode(level logger.LogLevel) logger.Interface {newLogger := *lnewLogger.LogLevel = levelreturn &newLogger
}// Info print info
func (l Logger) Info(ctx context.Context, msg string, args ...interface{}) {if l.LogLevel >= logger.Info {//预留10个字段位置fields := make([]zap.Field, 0, 10+len(l.customFields))fields = append(fields, zap.String("file", utils.FileWithLineNum()))for _, customField := range l.customFields {fields = append(fields, customField(ctx))}for _, arg := range args {if vv, ok := arg.(zapcore.Field); ok {if len(vv.String) > 0 {fields = append(fields, zap.String(vv.Key, vv.String))} else if vv.Integer > 0 {fields = append(fields, zap.Int64(vv.Key, vv.Integer))} else {fields = append(fields, zap.Any(vv.Key, vv.Interface))}}}l.log.Info(msg, fields...)}
}// Warn print warn messages
func (l Logger) Warn(ctx context.Context, msg string, args ...interface{}) {if l.LogLevel >= logger.Warn {//预留10个字段位置fields := make([]zap.Field, 0, 10+len(l.customFields))fields = append(fields, zap.String("file", utils.FileWithLineNum()))for _, customField := range l.customFields {fields = append(fields, customField(ctx))}for _, arg := range args {if vv, ok := arg.(zapcore.Field); ok {if len(vv.String) > 0 {fields = append(fields, zap.String(vv.Key, vv.String))} else if vv.Integer > 0 {fields = append(fields, zap.Int64(vv.Key, vv.Integer))} else {fields = append(fields, zap.Any(vv.Key, vv.Interface))}}}l.log.Warn(msg, fields...)}
}// Error print error messages
func (l Logger) Error(ctx context.Context, msg string, args ...interface{}) {if l.LogLevel >= logger.Error {//预留10个字段位置fields := make([]zap.Field, 0, 10+len(l.customFields))fields = append(fields, zap.String("file", utils.FileWithLineNum()))for _, customField := range l.customFields {fields = append(fields, customField(ctx))}for _, arg := range args {if vv, ok := arg.(zapcore.Field); ok {if len(vv.String) > 0 {fields = append(fields, zap.String(vv.Key, vv.String))} else if vv.Integer > 0 {fields = append(fields, zap.Int64(vv.Key, vv.Integer))} else {fields = append(fields, zap.Any(vv.Key, vv.Interface))}}}l.log.Error(msg, fields...)}
}// Trace print sql message
func (l Logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {if l.LogLevel <= logger.Silent {return}fields := make([]zap.Field, 0, 6+len(l.customFields))elapsed := time.Since(begin)switch {case err != nil && l.LogLevel >= logger.Error && (!l.IgnoreRecordNotFoundError || !errors.Is(err, gorm.ErrRecordNotFound)):for _, customField := range l.customFields {fields = append(fields, customField(ctx))}fields = append(fields,zap.Error(err),zap.String("file", utils.FileWithLineNum()),zap.Duration("latency", elapsed),)sql, rows := fc()if rows == -1 {fields = append(fields, zap.String("rows", "-"))} else {fields = append(fields, zap.Int64("rows", rows))}fields = append(fields, zap.String("sql", sql))l.log.Error("", fields...)case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= logger.Warn:for _, customField := range l.customFields {fields = append(fields, customField(ctx))}fields = append(fields,zap.Error(err),zap.String("file", utils.FileWithLineNum()),zap.String("slow!!!", fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)),zap.Duration("latency", elapsed),)sql, rows := fc()if rows == -1 {fields = append(fields, zap.String("rows", "-"))} else {fields = append(fields, zap.Int64("rows", rows))}fields = append(fields, zap.String("sql", sql))l.log.Warn("", fields...)case l.LogLevel == logger.Info:for _, customField := range l.customFields {fields = append(fields, customField(ctx))}fields = append(fields,zap.Error(err),zap.String("file", utils.FileWithLineNum()),zap.Duration("latency", elapsed),)sql, rows := fc()if rows == -1 {fields = append(fields, zap.String("rows", "-"))} else {fields = append(fields, zap.Int64("rows", rows))}fields = append(fields, zap.String("sql", sql))l.log.Info("", fields...)}
}// Immutable custom immutable field
// Deprecated: use Any instead
func Immutable(key string, value interface{}) func(ctx context.Context) zap.Field {return Any(key, value)
}// Any custom immutable any field
func Any(key string, value interface{}) func(ctx context.Context) zap.Field {field := zap.Any(key, value)return func(ctx context.Context) zap.Field { return field }
}// String custom immutable string field
func String(key string, value string) func(ctx context.Context) zap.Field {field := zap.String(key, value)return func(ctx context.Context) zap.Field { return field }
}// Int64 custom immutable int64 field
func Int64(key string, value int64) func(ctx context.Context) zap.Field {field := zap.Int64(key, value)return func(ctx context.Context) zap.Field { return field }
}// Uint64 custom immutable uint64 field
func Uint64(key string, value uint64) func(ctx context.Context) zap.Field {field := zap.Uint64(key, value)return func(ctx context.Context) zap.Field { return field }
}// Float64 custom immutable float32 field
func Float64(key string, value float64) func(ctx context.Context) zap.Field {field := zap.Float64(key, value)return func(ctx context.Context) zap.Field { return field }
}

自定义结构体

// Logger logger for gorm2

type Logger struct {

    log *zap.Logger

    logger.Config

    customFields []func(ctx context.Context) zap.Field

}

关键在于 customFields定义了一个接受传Context参数的方法。在初始化日志的地方,传从Context中获取对应参数的函数,比如,从context中接受traceId。

由此,gorm log with traceId目的实现。

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

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

相关文章

2024OD机试卷-游戏分组 (java\python\c++)

题目:游戏分组 题目描述 部们准备举办一场 王者荣耀 表演赛,有 10 名游戏爱好者参与,分为两队,每队 5 人。 每位参与者都有一个评分,代表着他的游戏水平。为了表演赛尽可能精彩,我们需要把 10 名参赛者分为实力尽量相近的两队。 一队的实力可以表示为这一队 5 名队员的…

基于SSM的婚恋网站的设计与实现(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的婚恋网站的设计与实现&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spri…

std::remove-----std::remove_if

std::remove和std::remove_if 是 C11 标准库中的一个算法函数. std::remove 作用 遍历一遍容器&#xff0c;将容器中所有不是指定元素的元素往前复制。 总之就是一句话&#xff1a; 把不该删除的移动到前面&#xff0c;后面的就是应该删除的。 注意&#xff1a; 1&#…

Netty-面试题(上)(四十九)

为什么Netty适合做网络编程? Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架&#xff0c;用以快速开发高性能、高可靠性的网络 IO 程序。Netty 主要用来做网络通信&#xff0c;一般可以用来作RPC框架的通信工具、实现即时通讯…

函数递归练习

目录 1.分析下面选择题 2.实现求第n个斐波那契数 3.编写一个函数实现n的k次方&#xff0c;使用递归实现。 4.写一个递归函数DigitSum(n)&#xff0c;输入一个非负整数&#xff0c;返回组成它的数字之和 5.递归方式实现打印一个整数的每一位 6.实现求n的阶乘 1.分析下面选择…

算术平均数

算术平均数&#xff08;average&#xff09;是一组数据相加后除以数据的个数而得到的结果&#xff0c;是度量数据水平的常用统计量&#xff0c;在参数估计和假设检验中经常用到。比如&#xff1a;用职工平均工资来衡量职工工资的一般水平&#xff0c;用平均体重来观察某一人群体…

HTML中打开窗口的类型及使用方法

HTML中打开窗口是Web开发中常用的功能之一&#xff0c;可以通过不同的方式打开窗口&#xff0c;以满足不同的需求。本文将介绍HTML中打开窗口的类型及使用方法。 一、使用target属性打开窗口 target属性是HTML中打开窗口最常用的方式之一&#xff0c;可以通过设置target属性的…

基于LeNet5实现手写数字识别,可视化卷积层。

LeNet5 CNN卷积网络的发展史 1. LetNet5(1998) 2. AlexNet(2012) 3. ZFNet(2013) 4. VGGNet(2014) 5. GoogLeNet(2014) 6. ResNet(2015) 7. DenseNet(2017) 8. EfficientNet(2019) 9. Vision Transformers(2020) 10. 自适应卷积网络(2021) 上面列出了发展到现在CNN的一些经典…

Spring STOMP-用户的目的地

应用程序可以发送针对特定用户的消息&#xff0c;并且Spring的STOMP支持识别以/user/为前缀的destination。例如&#xff0c;客户端可能会订阅/user/queue/position-updates的destination。UserDestinationMessageHandler处理此destination&#xff0c;并将其转换为特定于用户会…

单位个人如何向期刊投稿发表文章?

在单位担任信息宣传员一职以来,我深感肩上的责任重大。每月的对外信息宣传投稿不仅是工作的核心,更是衡量我们部门成效的重要指标。起初,我满腔热血,以为只要勤勉努力,将精心撰写的稿件投至各大报社、报纸期刊的官方邮箱,就能顺利登上版面,赢得读者的青睐。然而,现实远比理想骨…

Java入门基础学习笔记23——For循环结构

1、for循环&#xff1a; 控制一段代码反复执行很多次。 2、For循环语句的基本结构&#xff1a; for(初始化表达式&#xff1b;判断表达式&#xff1b;递增&#xff08;递减&#xff09;表达式&#xff09; {循环体语句&#xff08;重复执行的代码&#xff09; } 例&#xff1…

【碎片知识】2024_05_15

char int long float double运算的时候是从低转到高的&#xff0c;表达式的类型会自动提升或者转 换为参与表达式求值的最上级类型. 关于代码的说法正确的是&#xff08; &#xff09; #include <stdio.h> int main() {int x -1;unsigned int y 2;if (x > y){printf…

论文合集整理推荐2024.5.15

‍2012年论文合集&#xff1a;论文入口 ‍2019年论文合集&#xff1a;论文入口 2022年论文合集&#xff1a;论文入口 2023年论文合集&#xff1a;论文入口 2024年论文合集&#xff1a;论文入口

RustGUI学习(iced)之小部件(十三):如何使用qrcode部件来生成和显示二维码?

前言 本专栏是学习Rust的GUI库iced的合集,将介绍iced涉及的各个小部件分别介绍,最后会汇总为一个总的程序。 iced是RustGUI中比较强大的一个,目前处于发展中(即版本可能会改变),本专栏基于版本0.12.1. 概述 这是本专栏的第十三篇,主要讲述qr_code二维码部件的使用,会结…

Linux-笔记 man手册命令

man 1&#xff1a;用户级别的命令。这些是用户可以直接在shell中执行的命令&#xff0c;例如ls、cp、rm等。 man 2&#xff1a;系统调用。这部分包含了操作系统提供的底层功能&#xff0c;通常是C语言的函数原型&#xff0c;由程序或库调用。 man 3&#xff1a;库函数。这部分…

基于单片机的智能安防系统设计(32+4G+WIFI版)-设计说明书

设计摘要&#xff1a; 本设计基于STM32单片机&#xff0c;旨在实现一个智能安防系统&#xff0c;主要包括烟雾和温度传感器、人体红外传感器、显示屏、按键、4G模块和WiFi模块等组件。通过这些组件的协作&#xff0c;实现了火灾检测、入侵监测、状态显示、用户交互和远程通信等…

Java中List不同实现类的对比

Java中List不同实现类的对比 在Java中&#xff0c;List接口是一个非常重要的集合接口&#xff0c;它表示一个有序的集合&#xff0c;可以包含重复的元素。List接口有很多不同的实现类&#xff0c;其中最常用的是ArrayList、LinkedList和Vector。这些实现类在性能、使用方式和适…

CSP认证刷题笔记(2)ISBN号码(13年CSP认证第二题)

文章目录 题目描述基本思路 题目描述 每一本正式出版的图书都有一个 ISBN 号码与之对应。ISBN 码包括9位数字、1位识别码和3位分隔符&#xff0c;其规定格式如x-xxx-xxxxx-x&#xff0c;其中符号- 是分隔符&#xff08;键盘上的减号&#xff09;&#xff0c;最后一位是识别码&…

OSG编程指南<二十三>:基于OSG+ImGui制作模型编辑器,实现三轴方向的实时平移、旋转和缩放变化

1、概述 在OSG的开发应用过程中&#xff0c;我们有时候总会纠结于使用MFC还是Qt来嵌入OSG窗口以便于后续的功能开发&#xff0c;毕竟选择一个合适的UI框架&#xff0c;对于后续的开发还是省去很多麻烦的。但对于初学者来说&#xff0c;可能对框架消息机制的不熟悉&#xff0c;尤…

项目8-头像的上传

js实现头像上传并且预览图片功能以及提交 - 掘金 (juejin.cn) 我们简单建立一个表 1.前端知识储备 1.1 addClass的使用 1.基本语法 addClass() 方法向被选元素添加一个或多个类。 该方法不会移除已存在的 class 属性&#xff0c;仅仅添加一个或多个 class 属性。 提示&…