golang:微服务架构下的日志追踪系统(二)

背景

在使用Gin框架进行服务开发时,我们遇到了一个日志记录的问题。由于Gin的上下文(*gin.Context)实现了context.Context接口,在调用日志记录器的InfoWarnError等方法时,直接传递Gin的上下文通常不会导致编译错误。会导致我们在《golang:微服务架构下的日志追踪系统》一文中定义的日志统计维度信息无法正确串联。

为了解决这一问题,我们之前的解决方案是让业务方在使用日志记录器时,将Gin的上下文手动转换为context.Context

但这种方案带来了一个明显的弊端:它依赖于开发人员的主动转换,而开发人员往往会忘记进行这一步操作,直接传递Gin的上下文,从而导致日志并未按照预期格式打印。这不仅增加了开发人员的负担,也降低了日志系统的可靠性和准确性。

解决方案

针对上述问题,我们提出了一种更为优雅的解决方案:在日志记录器的方法内部封装Gin上下文到context.Context的转换逻辑。这样一来,开发人员在使用日志记录器时无需再关心上下文的转换问题,只需直接传递Gin的上下文即可。

接下来,我们将这一转换逻辑集成到日志记录器的各个方法中,确保无论传入的是Gin的上下文还是context.Context,都能正确记录日志信息。

在封装的方法中实现gin的context的转换即可。

// gin context to context
func GinContextToContext(ctx context.Context) context.Context {if v, ok := ctx.(*gin.Context); ok {if v == nil || v.Request == nil {return context.Background()}return v.Request.Context()}return ctx
}

完整的逻辑。

package loggerimport ("context""errors""fmt""time""{{your module}}}/go-core/utils/constant""github.com/gin-gonic/gin""go.uber.org/zap""go.uber.org/zap/zapcore""gorm.io/gorm""gorm.io/gorm/logger""gorm.io/gorm/utils"
)// 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
}// NewDefault new default logger
// 初始化一个默认的 logger
func NewDefault(zapLogger *zap.Logger) logger.Interface {return New(zapLogger, WithCustomFields(func(ctx context.Context) zap.Field {v := ctx.Value("Request-Id")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()}, func(ctx context.Context) zap.Field {v := ctx.Value("path")if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String("path", vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {v := ctx.Value("version")if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String("version", vv)}return zap.Skip()}),WithConfig(logger.Config{SlowThreshold:             200 * time.Millisecond,Colorful:                  false,IgnoreRecordNotFoundError: false,LogLevel:                  logger.Info,}))
}// 用于支持微服务架构下的链路追踪
func NewTracingLogger(zapLogger *zap.Logger) logger.Interface {return New(zapLogger, WithCustomFields(// trace是链路追踪的唯一标识// span是当前请求的唯一标识// parent_span是父请求的唯一标识func(ctx context.Context) zap.Field {v := ctx.Value(constant.CONTEXT_KEY_TRACE)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_TRACE, vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {v := ctx.Value(constant.CONTEXT_KEY_SPAN)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_SPAN, vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {v := ctx.Value(constant.CONTEXT_KEY_PARENT_SPAN)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_PARENT_SPAN, vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {v := ctx.Value(constant.CONTEXT_KEY_METHOD)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_METHOD, vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {v := ctx.Value(constant.CONTEXT_KEY_PATH)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_PATH, vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {v := ctx.Value(constant.CONTEXT_KEY_VERSION)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_VERSION, vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {// 用于标识调用方服务名v := ctx.Value(constant.CONTEXT_KEY_CALLER_SERVICE_NAME)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_CALLER_SERVICE_NAME, vv)}return zap.Skip()}, func(ctx context.Context) zap.Field {// 用于标识调用方ipv := ctx.Value(constant.CONTEXT_KEY_CALLER_IP)if v == nil {return zap.Skip()}if vv, ok := v.(string); ok {return zap.String(constant.CONTEXT_KEY_CALLER_IP, vv)}return zap.Skip()}),WithConfig(logger.Config{SlowThreshold:             200 * time.Millisecond,Colorful:                  false,IgnoreRecordNotFoundError: false,LogLevel:                  logger.Info,}))
}// gin context to context
func GinContextToContext(ctx context.Context) context.Context {if v, ok := ctx.(*gin.Context); ok {if v == nil || v.Request == nil {return context.Background()}return v.Request.Context()}return ctx
}// 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 {ctx = GinContextToContext(ctx)//预留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))}now := time.Now().UnixMilli()// 从ctx中获取操作的开始时间if v := ctx.Value(constant.CONTEXT_KEY_EXECUTE_START_TIME); v != nil {if vv, ok := v.(int64); ok {// 计算操作的执行时间,以毫秒为单位duration := now - vv// 将操作的执行时间放入ctxfields = append(fields, zap.Int64(constant.CONTEXT_KEY_EXECUTE_DURATION, duration))}}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 {ctx = GinContextToContext(ctx)//预留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 {ctx = GinContextToContext(ctx)//预留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}ctx = GinContextToContext(ctx)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 }
}

测试用例

package loggerimport ("context""fmt""net/http""net/http/httptest""testing""time""{{your module}}/go-core/utils/constant""github.com/gin-gonic/gin""go.uber.org/zap"
)// Mock function to create a gin context
func createGinContext() *gin.Context {w := httptest.NewRecorder()// Create a mock request with an attached contextreq, _ := http.NewRequest(http.MethodPost, "/api/v1/users", nil)ctx := context.Background()// Here you can set values in the contextctx = context.WithValue(ctx, constant.CONTEXT_KEY_SPAN, "123456")ctx = context.WithValue(ctx, constant.CONTEXT_KEY_PARENT_SPAN, "parent_span_123456")ctx = context.WithValue(ctx, constant.CONTEXT_KEY_TRACE, "trace_id_123456")ctx = context.WithValue(ctx, constant.CONTEXT_KEY_METHOD, "POST")ctx = context.WithValue(ctx, constant.CONTEXT_KEY_PATH, "/api/v1/users")ctx = context.WithValue(ctx, constant.CONTEXT_KEY_VERSION, "v1.0.0")ctx = context.WithValue(ctx, constant.CONTEXT_KEY_CALLER_SERVICE_NAME, "user-service")ctx = context.WithValue(ctx, constant.CONTEXT_KEY_CALLER_IP, "172.0.0.3")req = req.WithContext(ctx)// Create the Gin contextc, _ := gin.CreateTestContext(w)c.Request = reqreturn c
}// 测试ginContextWithLogger
func TestGinContextWithLogger(t *testing.T) {// 创建一个 zap logger 实例zapLogger, _ := zap.NewProduction()defer zapLogger.Sync() // 确保日志被刷新// 创建一个带有自定义字段和配置的 Logger 实例customLogger := NewTracingLogger(zapLogger)// 创建一个 Gin contextc := createGinContext()// 测试 Info 方法customLogger.Info(c, "This is an info message")// 测试 Warn 方法customLogger.Warn(c, "This is a warning message")// 测试 Error 方法customLogger.Error(c, "This is an error message")// 测试 Trace 方法,模拟一个慢查询slowQueryBegin := time.Now()slowQueryFunc := func() (string, int64) {return "SELECT * FROM users", 100}time.Sleep(2 * time.Second) // 模拟一个慢查询customLogger.Trace(c, slowQueryBegin, slowQueryFunc, nil)// 测试 Trace 方法,模拟一个错误查询errorQueryBegin := time.Now()errorQueryFunc := func() (string, int64) {return "SELECT * FROM non_existent_table", 0}customLogger.Trace(c, errorQueryBegin, errorQueryFunc, fmt.Errorf("table not found"))// 由于日志是异步的,我们需要在测试结束时等待一段时间以确保所有日志都被输出time.Sleep(500 * time.Millisecond)
}

 

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

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

相关文章

美国宏观经济基础框架梳理

玩转币圈和美股&#xff0c;最关键的是理解美国宏观经济。以下是核心逻辑&#xff1a;美国经济数据→政策调整→资金流动→资产价格变化。掌握这些因素的关系&#xff0c;才能在市场中立于不败之地。 一、核心变量及其意义 1. GDP&#xff08;国内生产总值&#xff09; • …

Java Map集合、集合的嵌套

一. 概述 1. Map集合称为双列集合&#xff0c;格式&#xff1a;{key1value1, key2value2,.....},一次需要存一对数据作为一个元素。 2. Map集合的每个元素"keyvalue"称为一个键值对/键值对对象/一个Entry对象&#xff0c;Map集合也被称为"键值对集合"。 3.…

spring防止重复点击,两种注解实现(AOP)

第一种&#xff1a;EasyLock 简介 为了简化可复用注解&#xff0c;自己实现的注解&#xff0c;代码简单随拿随用 使用方式 1.创建一个注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) Documented public interface EasyLock {long waitTime() default …

微服务拆分的艺术:构建高效、灵活的系统架构

目录 一、微服务拆分的重要性 二、微服务拆分的策略 1. 按照业务领域拆分 2. 按照团队结构拆分 3. 按照业务边界拆分 4. 按照数据和数据库拆分 5. 按照用户界面或外部接口拆分 6. 按照功能模块或领域驱动设计拆分 7. 按照性能和可伸缩性需求拆分 三、微服务拆分的实践…

中建海龙:科技助力福城南产业片区绿色建筑发展

在快速发展的城市化进程中&#xff0c;绿色建筑以其环保、节能、可持续的特点日益受到重视。作为建筑工业化领域的领军企业&#xff0c;中建海龙科技有限公司&#xff08;简称“中建海龙”&#xff09;凭借其卓越的科技实力和创新举措&#xff0c;在推动绿色建筑发展方面做出了…

大模型数据采集和预处理:把所有数据格式,word、excel、ppt、jpg、pdf、表格等转为数据

大模型数据采集和预处理&#xff1a;把所有数据格式&#xff0c;word、excel、ppt、jpg、pdf、表格等转为数据 文本/图片/表格&#xff0c;分别提取处理工具选择不同格式文件&#xff0c;使用不同工具处理1. 确认目标2. 分析过程(目标-手段分析法)3. 实现步骤4. 代码封装效果展…

三甲医院等级评审八维数据分析应用(五)--数据集成与共享篇

一、引言 1.1 研究背景与意义 随着医疗卫生体制改革的不断深化以及信息技术的飞速发展,三甲医院评审作为衡量医院综合实力与服务水平的重要标准,对数据集成与共享提出了更为严苛的要求。在传统医疗模式下,医院内部各业务系统往往各自为政,形成诸多“信息孤岛”,使得数据…

论文泛读《LPFHE: Low-Complexity Polynomial CNNs for Secure Inference over FHE》

文章目录 1、摘要2、介绍3、文章结构4、总结 1、摘要 Machine learning as a service (MLaaS) 在客户中越来越受欢迎。为了解决 MLaaS 中的隐私问题&#xff0c;引入了 FHE 来保护客户端的数据。  然而&#xff0c;FHE 不能直接评估 卷积神经网络 (CNNs) 中的非算数激活函数。…

Redission红锁

目录 一、什么是红锁 二、Redission红锁的使用 一、什么是红锁 Redis 的作者 Salvatore Sanfilippo&#xff08;又名 antirez&#xff09;提出的一种基于多个 Redis 实例实现分布式锁的算法。红锁&#xff08;Redlock&#xff09;旨在解决单点故障问题&#xff0c;即当使用单…

VTK 鼠标+键盘重构

1、鼠标事件 如果有鼠标事件处理等相应的需求,可以重写该事件。 void OnMouseMove() override; //鼠标移动事件 void OnLeftButtonDown() override;//左键按下事件 void OnLeftButtonUp() override;//左键抬起事件 void OnMiddleButtonDown() override;//滚轮按下事件 …

ELK 使用教程采集系统日志 Elasticsearch、Logstash、Kibana

前言 你知道对于一个系统的上线考察&#xff0c;必备的几样东西是什么吗&#xff1f;其实这也是面试中考察求职者&#xff0c;是否真的做过系统开发和上线的必备问题。包括&#xff1a;服务治理(熔断/限流) (opens new window)、监控 (opens new window)和日志&#xff0c;如果…

node内置模块之---path 模块

path 模块的作用 path 模块是 Node.js 的核心模块之一&#xff0c;提供了用于处理和转换文件路径的功能。它能帮助你在不同操作系统间处理文件路径时避免平台差异&#xff08;如 Windows 和 Unix 系统使用不同的路径分隔符&#xff09; path 模块api介绍 使用场景&#xff1a; …

STM32G0B1 can Error_Handler 解决方法

问题现象 MCU上电&#xff0c;发送0x13帧数据固定进入 Error_Handler 硬件介绍 MCU :STM32G0B1 can:NSI1042 tx 接TX RX 接RX 折腾了一下午&#xff0c;无解&#xff0c;问题依旧&#xff1b; 对比测试 STM32G431 手头有块G431 官方评估版CAN 模块&#xff1b; 同样的…

在ros2 jazzy和gazebo harmonic下的建图导航(cartographer和navigation)实现(基本)

我的github分支&#xff01;&#xff01;&#xff01; 你可以在这里找到相对应的源码。 DWDROME的MOGI分支 来源于&#xff01;&#xff01; MOGI-ROS/Week-3-4-Gazebo-basics 学习分支整理日志 分支概述 这是一个用于个人学习的新分支&#xff0c;目的是扩展基本模型并添加…

Redis 实现分布式锁

文章目录 引言一、Redis的两种原子操作1.1 Redis 的原子性1.2 单命令1.3 Lua 脚本1.4 对比单命令与 Lua 脚本 二、Redis 实现分布式锁2.1 分布式锁的概念与需求2.1.1 什么是分布式锁&#xff1f;2.1.2 分布式锁的常见应用场景 2.2 基于 Redis 的分布式锁实现2.2.1 锁的获取与释…

区块链与微服务研究报告

摘要 区块链与微服务架构的结合为现代企业级应用提供了新的解决方案。区块链技术以其不可篡改性和透明性&#xff0c;解决了微服务在分布式环境下的数据一致性和安全性问题。本文将探讨区块链与微服务的基本概念、技术原理、应用案例以及未来研究方向。 1. 引言 随着信息技术…

SAP MM物料管理模块常见BAPI函数清单

【SAP系统研究】 #SAP #MM #物料管理 #函数 #BAPI 1、物料主数据 BAPI_MATERIAL_SAVEDATA 创建/更改物料主数据 BAPI_MATERIAL_SAVEREPLICA 物料主数据视图扩充 BAPI_MATERIAL_EXISTENCECHECK 检查物料主数据是否存在 BAPI_MATERIAL_GETLIST 显示物料主数据明细 BAPI_MATERIALG…

Stable Diffusion和Midjourney有什么区别?

Stable Diffusion 和 Midjourney 主要有以下区别&#xff1a; 目录 费用与可访问性 设备要求 安装与使用 学习成本 图像生成效果 可控性与定制性 私密性 费用与可访问性 Stable Diffusion&#xff1a;开源免费&#xff0c;任何人都可以免费下载并自行部署使用&#xf…

104周六复盘 (188)UI

1、早上继续看二手书的一个章节&#xff0c;程序开发流程、引擎、AI等内容&#xff0c; 内容很浅&#xff0c;基本上没啥用&#xff0c;算是复习。 最大感触就是N年前看同类书的里程碑、AI相关章节时&#xff0c;会感觉跟自己没啥关系&#xff0c; 而如今则密切相关&#xf…

Mono里运行C#脚本23—mono_jit_exec

前面已经分析一部分代码,可以打下基础知识,当然还有很多其它部分的内容,没有深入去了解。 接着下来,我们去探索一下怎么样找到执行应用程序的入口。 在这个嵌入脚本程序里,有一个这样的函数调用: main_function (domain, file, argc - 1, argv + 1); 这个函数的作用,…