清晰架构(Clean Architecture)的Go微服务: 日志管理

良好的日志记录可以提供丰富的日志数据,便于在调试时发现问题,从而大大提高编码效率。 记录器提供的自动化信息越多越好,日志信息也需要以简洁的方式呈现,便于找到重要的数据。

日志需求:
  1. 无需修改业务代码即可切换到其他日志库
  2. 不需直接依赖任何日志库
  3. 整个应用程序只有一个日志库的全局实例,因此你可以在一个位置更改日志配置并将其应用于整个程序。
  4. 可以在不修改代码的情况下轻松更改日志记录选项,例如,日志级别
  5. 能够在程序运行时动态更改日志级别
资源句柄:为什么日志记录与数据库不同

当应用程序需要处理外部资源时,例如数据库,文件系统,网络连接, SMTP服务器时,它通常需要一个资源句柄(Resource Handler)。在依赖注入中,容器创建一个资源句柄并将其注入每个业务函数,因此它可以使用资源句柄来访问底层资源。在此应用程序中,资源句柄是一个接口,因此业务层不会直接依赖于资源句柄的任何具体实现。数据库和gRPC链接都以这种方式处理。

但是,日志记录器稍有不同,因为几乎每个函数都需要它,但数据库不是。在Java中,我们为每个Java类初始化一个记录器(Logger)实例。 Java日志记录框架使用层次关系来管理不同的记录器,因此它们从父日志记录器继承相同的日志配置。在Go中,不同的记录器之间没有层次关系,因此你要么创建一个记录器,要么具有许多彼此不相关的不同记录器。为了获得一致的日志记录配置,最好创建一个全局记录器并将其注入每个函数。但者将需要做很多工作,所以我决定在一个中心位置创建一个全局记录器,每个函数可以直接引用它。

为了不将应用程序紧密绑定到特定的记录器,我创建了一个通用的记录器接口,因此应用程序对于具体的记录器透明的。以下是记录器(Logger)接口。

// Log is a package level variable, every program should access logging function through "Log"
var Log Logger

// Logger represent common interface for logging function
type Logger interface {
    Errorf(format string, args ...interface{})
    Fatalf(format string, args ...interface{})
    Fatal(args ...interface{})
    Infof(format string, args ...interface{})
    Info( args ...interface{})
    Warnf(format string, args ...interface{})
    Debugf(format string, args ...interface{})
    Debug(args ...interface{})
}

因为每个文件都依赖于日志记录,很容易产生循环依赖,所以我在“容器”包里面创建了一个单独的子包“logger”来避免这个问题。 它只有一个“Log”变量和“Logger”接口。 每个文件都通过这个变量和接口访问日志功能。

记录器封装

支持一个日志库的标准方法(例如ZAP¹或Logrus²) 是创建一个封装来实现已经创建的记录器接口。 这很简单,以下是代码。

type loggerWrapper struct {
    lw *zap.SugaredLogger
}
func (logger *loggerWrapper) Errorf(format string, args ...interface{}) {
    logger.lw.Errorf(format, args)
}
func (logger *loggerWrapper) Fatalf(format string, args ...interface{}) {
    logger.lw.Fatalf(format, args)
}
func (logger *loggerWrapper) Fatal(args ...interface{}) {
    logger.lw.Fatal(args)
}
func (logger *loggerWrapper) Infof(format string, args ...interface{}) {
    logger.lw.Infof(format, args)
}
func (logger *loggerWrapper) Warnf(format string, args ...interface{}) {
    logger.lw.Warnf(format, args)
}
func (logger *loggerWrapper) Debugf(format string, args ...interface{}) {
    logger.lw.Debugf(format, args)
}
func (logger *loggerWrapper) Printf(format string, args ...interface{}) {
    logger.lw.Infof(format, args)
}
func (logger *loggerWrapper) Println(args ...interface{}) {
    logger.lw.Info(args, "\n")
}

但是日志记录存在一个问题。日志记录的一个功能是在日志消息中打印记录者名字。在对接口封装之后,方法的调用者不是打印日志的程序,而是封装程序。要解决该问题,你可以直接更改日志库的源代码,但在升级日志库时会导致兼容性问题。最终的解决方案是要求日志记录库创建一个新功能,该功能可以根据方法是否使用封装来返回合适的调用方。

为了让代码现在能正常工作,我走了捷径。因为ZAP和Logrus之间的大多数函数签名是相似的,所以我提取了常用的签名并创建了一个共享接口,因为两个日志库都已经有了这些函数,它们自动实现这些接口。 Go接口设计的优点在于,你可以先创建具体实现,然后再创建接口,如果函数签名相互匹配,则自动实现接口。这有点作弊,但非常有效。如果要用的记录器不支持公共的接口,则还是要对它进行封装, 这样就只能暂时先牺牲调用者功能或修改源代码。

日志库比较:

不同的日志库提供不同的功能,其中一些功能对于调试很重要。

需要记录的重要信息(需要以下数据):

  1. 文件名和行号
  2. 方法名称和调用文件名
  3. 消息记录级别
  4. 时间戳
  5. 错误堆栈跟踪
  6. 自动记录每个函数调用包括参数和结果

我希望日志库自动提供这些数据,例如调用方法名称,而不编写显式代码来实现。对于上述6个功能,目前没有日志库提供#6,但它们都提供1到5个中的部分或全部。我尝试了两个非常流行的日志库Logrus和ZAP。 Logrus提供了所有功能,但是我的控制台上的格式不正确(它在我的Windows控制台上显示“ n t”而不是新行)并且输出格式不像ZAP那样干净。 ZAP不提供#2,但其他一切看起来都不错,所以我决定暂时使用它。

令人惊讶的是,本程序被证明是一个非常好的工具来测试不同的日志库,因为你可以切换到不同的日志库来比较输出结果,而只需要更改配置文件中的一行。这不是本程序的功能,而是一个好的副作用。

实际上,我最需要的功能是自动记录每个函数调用包括参数和结果(#6),但是还没有日志库提供该功能提供。我希望将来能够得到它。

错误(error)处理:

错误处理与日志记录直接相关,所以我也在这里讨论一下。以下是我在处理错误时遵循的规则。

1.使用堆栈跟踪创建错误

错误消息本身需要包含堆栈跟踪信息。如果错误源自你的程序,你可以导入“github.com/pkg/errors”库来创建错误以包含堆栈跟踪。但是如果它是从另一个库生成的并且该库没有使用“pkg/errors”,你需要用“errors.Wrap(err,message)”语句包装该错误,以获取堆栈跟踪信息。由于我们无法控制第三方库,因此最好的解决方案是在我们的程序中对所有错误进行包装。详情请见[这里](https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package)³。

2.使用堆栈跟踪打印错误
你需要使用“logger.Log.Errorf(”%+vn“,err)”或“fmt.Printf(”%+vn“,err)”以便打印堆栈跟踪信息,关键是“+v”选项(当然你必须已经使用#1)。

3.只有顶级函数才能处理错误
“处理”表示记录错误并将错误返回给调用者。因为只有顶级函数处理错误,所以错误只在程序中记录一次。顶层的调用者通常是面向用户的程序,它是用户界面程序(UI)或另一个微服务。你希望记录错误消息(因此你的程序中具有记录),然后将消息返回到UI或其他微服务,以便他们可以重试或对错误执行某些操作。

4.所有其他级别函数应只是将错误传播到较高级别
底层或中间层函数不要记录或处理错误,也不要丢弃错误。你可以向错误中添加更多数据,然后传播它。当出现错误时,你不希望停止整个应用程序。

恐慌(Panic):

除了在本地的“main.go”之外,我从未使用过恐慌(Panic)。它更像是一个bug而不是一个功能。在让我们谈谈日志⁴中,Dave Cheney写道“人们普遍认为应用库不应该使用恐慌”。另一个错误是log.Fatal,它具有与恐慌相同的效果,也应该被禁止。 “log.Fatal”更糟糕,它看起来像一个日志,但是在输出日志后它“恐慌”,这违反了单一责任规则。

恐慌有两个问题。首先,它与错误的处理方式不同,但它实际上是一个错误,一个错误的子类型。现在,错误处理代码需要处理错误和恐慌,例如事务处理代码⁵中的错误处理代码。其次,它会停止应用程序,这非常糟糕。只有顶级主控制程序才能决定如何处理错误,所有其他被调用的函数应该只将错误传播到上层。特别是现在,服务网格层(Service Mesh)可以提供重试等功能,恐慌使其更加复杂。

如果你正在调用第三方库并且它在代码中产生恐慌,那么为了防止代码停止,你需要截获恐慌并从中恢复。以下是代码示例,你需要为每个可能发生恐慌的顶级函数执行此操作(在每个函数中放置“defer catchPanic()”)。在下面的代码中,我们有一个函数“catchPanic”来捕获并从恐慌中恢复。函数“RegisterUser”在代码的第一行调用“defer catchPanic()”。有关恐慌的详细讨论,请参阅此处⁶。

func catchPanic() {
    if p := recover(); p != nil {
        logger.Log.Errorf("%+v\n", p)
    }
}

func (uss *UserService) RegisterUser(ctx context.Context, req *uspb.RegisterUserReq)
    (*uspb.RegisterUserResp, error) {
    
     defer catchPanic()
    ruci, err := getRegistrationUseCase(uss.container)
    if err != nil {
        logger.Log.Errorf("%+v\n", err)
        return nil, errors.Wrap(err, "")
    }
    mu, err := userclient.GrpcToUser(req.User)
...
}
结论:

良好的日志记录可以使程序员更有效。你希望使用堆栈跟踪记录错误。 只有顶级函数才能处理错误,所有其他级别函数只应将错误传播到上一级。 不要使用恐慌。

源程序:

完整的源程序链接 github: https://github.com/jfeng45/se...

索引:

[1] zap

[2] Logrus

[3]Stack traces and the errors package

[4]Let’s talk about logging

[5]database/sql Tx — detecting Commit or Rollback

[6]On the uses and misuses of panics in Go

不堆砌术语,不罗列架构,不迷信权威,不盲从流行,坚持独立思考

Image placeholder
余光
未设置
  63人点赞

没有讨论,发表一下自己的看法吧

推荐文章
SOA架构和微服务架构的区别是什么?

来源:rrd.me/fqdANSOA架构和微服务架构的区别首先SOA和微服务架构一个层面的东西,而对于ESB和微服务网关是一个层面的东西,一个谈到是架构风格和方法,一个谈的是实现工具或组件。1.SOA

基于gin的golang web开发:模型验证

推荐课程《GO开发工程师--学习猿地精品课程》 Gin除了模型绑定还提供了模型验证功能。你可以给字段指定特定的规则标签,如果一个字段用binding:"required"标签修饰,在绑定时该字段的值为

软件架构被高估,清晰简单的设计被低估

软件架构最佳实践、企业架构模式以及系统描述的正式方法都是非常重要且实用的工具,总会有合适的场景让它们发挥作用。但在设计系统时,请从简单始、以简单终,尽可能避免一切会无谓提高复杂度的架构与正式工具。

微服务架构之「 服务注册 」

微服务架构是一个庞大复杂的工程,为什么说它庞大复杂呢?因为想要做好微服务,就必须先要建设好微服务所需的一系列基础设施和组件。我在前面的文章《架构设计之「微服务入门」》中已经初步介绍过了这些组件,包括:

微服务架构中如何构建一个数据报告服务?

场景描述在微服务架构中,每个微服务负责自己的数据库,微服务A是不允许直接连接微服务B的数据库进行操作的。现在有2个微服务,一个是订单服务,一个是用户服务。有一个数据报告的需求:生成一份包含用户信息的订

Pandas数据分析——超好用的Groupby详解

微信公众号:「Python读财」如有问题或建议,请公众号留言在日常的数据分析中,经常需要将数据根据某个(多个)字段划分为不同的群体(group)进行分析,如电商领域将全国的总销售额根据省份进行划分,分

PHP 代码简洁之道 ( PHP Clean Code)

介绍 RobertC.Martin's的软件工程师准则CleanCode同样适用于PHP。它并不是一个编码风格指南,它指导我们用PHP写出具有可读性,可复用性且可分解的代码。 并非所有的准则都必须严格

万字详解Oracle架构、原理、进程,学会世间再无复杂架构

学习是一个循序渐进的过程,从面到点、从宏观到微观,逐步渗透,各个击破,对于Oracle, 怎么样从宏观上来理解呢?先来看一个图,这个图取自于教材,这个图对于从整体上理解ORACLE 的体系结构组件,非

微服务架构:拆分单体应用的难点

拆分单体应用为服务的难点从表面上看,通过定义与业务能力或子域相对应的服务来创建微服务架构的策略看起来很简单。但是,你可能会遇到几个障碍:网络延迟。同步进程间通信导致可用性降低。 在服务之间维持数据一致

微服务架构的四大金刚利器

Photo@ChristopherCampbell 文 | 孔凡勇概述互联网应用发展到今天,从单体应用架构到SOA以及今天的微服务,随着微服务化的不断升级进化,服务和服务之间的稳定性变得越来越重要,分

大神讲解微服务治理的技术演进和架构实践

摘要:随着业务的发展,规模扩大,服务越来越多,需要协调线上运行的各个服务,保障服务的SLA;基于服务调用的性能KPI数据进行容量管理,合理分配各服务的资源占用;对故障业务做服务降级、流量控制、流量迁移

一个知名网站的微服务架构最佳实现

译者:蓝梦,十余年研发经验,现就职于某上市互联网公司。作者:小马,Medium 首席架构师。译者有话说,如果你的项目正在从单体升级为微服务而忧心;或者你在实践微服务过程中手忙脚乱,本文都是你不容错过的

金融行业微服务架构解析

转载本文需注明出处:微信公众号EAWorld,违者必究。引言:对于微服务,每个人都有自己的理解,与互联网企业的大量落地相比,微服务在传统金融行业还没有普及,这首先是传统金融行业线上系统需求更新和版本迭

微服务架构的四大杀手锏

Photo@ChristopherCampbell 文 | 孔凡勇概述互联网应用发展到今天,从单体应用架构到SOA以及今天的微服务,随着微服务化的不断升级进化,服务和服务之间的稳定性变得越来越重要,分

UfqiNews有福新闻的第N+1批次更新:清晰,流畅,省电

UfqiNews有福新闻自推出以来,一直持续更新、优化升级,作为一款带来全新新闻阅读体验的资讯应用,已经日益成熟,浏览量也日渐攀升。近日针对UfqiNews有福新闻的升级改进包括如下方面。1.UI调整

4.图片加载从模糊到清晰

很常见的一个效果虽然前端无法获知加载进度,但是可以通过效果来模拟通过先拉大预览的小图将图片模糊,然后再加载大图模拟效果效果预览:https://codepen.io/andy-js/pen/dyPZv

讲一讲Java的字符串常量池,看完你的思路就清晰了

课程推荐:Java开发工程师--学习猿地精品课程 前言很多朋友Java的字符串常量池的概念困扰了很长一段时间,最近研究了一下jvm指令码,终于对它有了大概的了解。在展示案例前,我们需要先搞清楚一个概念

一站式入口服务|爱奇艺微服务平台 API 网关实战

写在前面在互联网业务微服务化改造过程中,按照以往的服务治理体系,各服务需要单独实现限流、鉴权、监控、日志等通用功能,构建入口时资源申请、工单批复、多系统配置等一系列流程对精力消耗极大,学习成本较高

美团大规模微服务通信框架及治理体系OCTO核心组件开源

微服务通信框架及治理平台OCTO作为美团基础架构设施的重要组成部分,目前已广泛应用于公司技术线,稳定承载上万应用、日均支撑千亿级的调用。业务基于OCTO提供的标准化技术方案,能够轻松实现服务注册/发现

Lumen日志接入 Elasticsearch

日志分析系统的安装请看部署章节,安装elasticsearch组件composerrequireelasticsearch/elasticsearch修改config/logging.php添加一个e

一文读懂云原生 go-zero 微服务框架

课程推荐:GO开发工程师--学习猿地精品课程 go-zero介绍从今年8月7日github开源以来,已经获得了2700+star的go-zero是一个集成了各种工程实践的web和rpc框架。通过弹性设

PB级数据实时查询,滴滴Elasticsearch多集群架构实践

Elasticsearch是基于Lucene实现的分布式搜索引擎,提供了海量数据实时检索和分析能力。Elastic 公司开源的一系列产品组成的ElasticStack,可以为日志服务、搜索引擎、系统监

日志监控实践 – 监控Agent集成Lua引擎实现多维度日志采集

作者简介:董涵   百度资深研发工程师负责百度智能运维(Noah)服务管理和分布式监控架构研发工作,在分布式系统和大规模数据处理、可用性工程方向有广泛的实践经验。干货概览对于互联网行业来说,最有价值的

IBM Spectrum Protect 8.1.7在AIX7.1上的安装和配置

                                                本文作者: 谷铁柏摘要:    本文章主要讲述IBMSpectrumProtect8.1.7版本在AIX

巨杉TechDay回顾 | 与携程、巨杉、知乎大牛一起探寻DT时代数据库架构之道

数据,已成众多企业的核心资产。如今企业越来越懂得数据的重要性,也愈发清楚数据将为公司带来的巨大价值。在物联网、AI等技术的普及下,数据井喷仍在持续进行,如何更好地管理和使用这些“无穷无尽”的数据,则成