清晰架构(Clean Architecture)的Go微服务: 日志管理
良好的日志记录可以提供丰富的日志数据,便于在调试时发现问题,从而大大提高编码效率。 记录器提供的自动化信息越多越好,日志信息也需要以简洁的方式呈现,便于找到重要的数据。
日志需求:
无需修改业务代码即可切换到其他日志库
不需直接依赖任何日志库
整个应用程序只有一个日志库的全局实例,因此你可以在一个位置更改日志配置并将其应用于整个程序。
可以在不修改代码的情况下轻松更改日志记录选项,例如,日志级别
能够在程序运行时动态更改日志级别
资源句柄:为什么日志记录与数据库不同
当应用程序需要处理外部资源时,例如数据库,文件系统,网络连接, 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接口设计的优点在于,你可以先创建具体实现,然后再创建接口,如果函数签名相互匹配,则自动实现接口。这有点作弊,但非常有效。如果要用的记录器不支持公共的接口,则还是要对它进行封装, 这样就只能暂时先牺牲调用者功能或修改源代码。
日志库比较:
不同的日志库提供不同的功能,其中一些功能对于调试很重要。
需要记录的重要信息(需要以下数据):
文件名和行号
方法名称和调用文件名
消息记录级别
时间戳
错误堆栈跟踪
自动记录每个函数调用包括参数和结果
我希望日志库自动提供这些数据,例如调用方法名称,而不编写显式代码来实现。对于上述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)”语句包装该错误,以获取堆栈跟踪信息。由于我们无法控制第三方库,因此最好的解决方案是在我们的程序中对所有错误进行包装。详情请见这里³。
2.使用堆栈跟踪打印错误
你需要使用“logger.Log.Errorf(”%+v\n“,err)”或“fmt.Printf(”%+v\n“,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/servicetmpl
索引:
[1] zap
[2] Logrus
[3]Stack traces and the errors package
[5]database/sql Tx — detecting Commit or Rollback
[6]On the uses and misuses of panics in Go
清晰架构(Clean Architecture)的Go微服务: 日志管理的更多相关文章
- (转)微服务架构 互联网保险O2O平台微服务架构设计
http://www.cnblogs.com/Leo_wl/p/5049722.html 微服务架构 互联网保险O2O平台微服务架构设计 关于架构,笔者认为并不是越复杂越好,而是相反,简单就是硬道理也 ...
- Java 18套JAVA企业级大型项目实战分布式架构高并发高可用微服务电商项目实战架构
Java 开发环境:idea https://www.jianshu.com/p/7a824fea1ce7 从无到有构建大型电商微服务架构三个阶段SpringBoot+SpringCloud+Solr ...
- ELK-6.5.3学习笔记–使用filebeat管理微服务日志
微服务日志打印. 转载于http://www.eryajf.net/2369.html 上边是输出了nginx日志,从而进行展示,以及各种绘图分析,而现在的需求是,要将微服务当中的日志汇总到elk当中 ...
- 微服务日志监控与查询logstash + kafka + elasticsearch
使用 logstash + kafka + elasticsearch 实现日志监控 https://blog.csdn.net/github_39939645/article/details/788 ...
- SpringCloud微服务实战——搭建企业级开发框架(三十七):微服务日志系统设计与实现
针对业务开发人员通常面对的业务需求,我们将日志分为操作(请求)日志和系统运行日志,操作(请求)日志可以让管理员或者运营人员方便简单的在系统界面中查询追踪用户具体做了哪些操作,便于分析统计用户行为: ...
- 微服务日志之Spring Boot Kafka实现日志收集
前言 承接上文( 微服务日志之.NET Core使用NLog通过Kafka实现日志收集 https://www.cnblogs.com/maxzhang1985/p/9522017.html ).NE ...
- 利用 istio 来对运行在 Kubernetes 上的微服务进行管理
尝试在一个准生产环境下,利用 istio 来对运行在 Kubernetes 上的微服务进行管理. 这一篇是第一篇,将一些主要的坑和环境准备工作. 内容较多,因此无法写成手把手教程,希望读者有一定 Ku ...
- SpringBoot之微服务日志链路追踪
SpringBoot之微服务日志链路追踪 简介 在微服务里,业务出现问题或者程序出的任何问题,都少不了查看日志,一般我们使用 ELK 相关的日志收集工具,服务多的情况下,业务问题也是有些难以排查,只能 ...
- 清晰架构(Clean Architecture)的Go微服务: 事物管理
为了支持业务层中的事务,我试图在Go中查找类似Spring的声明式事务管理,但是没找到,所以我决定自己写一个. 事务很容易在Go中实现,但很难做到正确地实现. 需求: 将业务逻辑与事务代码分开. 在编 ...
随机推荐
- css属性overflow:hidden在IE6、7下失效解决方案
当父元素的直接子元素或者下级子元素的样式拥有position:relative属性时,父元素的overflow:hidden属性就会失效.我们在IE 6 7内发现子元素会超出父元素设定的高度,即使父元 ...
- Python里的迭代器
迭代器(iterator)协议 · 在Python中,支持迭代器协议就是实现对象的__iter__()和__next__()方法. 1.__iter__()方法:返回迭代器对象本身: 2.__next ...
- 云原生生态周报 Vol. 5 | etcd性能知多少
业界要闻 1 Azure Red Hat OpenShift已经GA.在刚刚结束的Red Hat Summit 2019上,Azure Red Hat OpenShift正式宣布GA,这是一个微软和红 ...
- @codechef - MXMN@ Maximum and Minimum
目录 @description@ @solution@ @accepted code@ @details@ @description@ 定义函数 f(G, x, y) 为 G 中点 x 和点 y 之间 ...
- PHP实现图片的等比缩放和Logo水印功能示例
文章来自于:脚本之家 文章链接:https://www.jb51.net/article/112909.htm 这篇文章主要介绍了PHP实现图片的等比缩放和Logo水印功能,结合实例形式分析了php图 ...
- Websocket 群聊功能
websocket 群聊 前提关闭防火墙 写入代码 from flask import Flask,request,render_template from geventwebsocket.handl ...
- 解决TortoiseSVN中out of date问题的一个方法
http://blog.csdn.net/freefalcon/article/details/645058 从去年开始,公司的代码管理从CVS转向了subvsersion,后者确实是前者的一个飞跃, ...
- 全面理解Python中的类型提示(Type Hints)
众所周知,Python 是动态类型语言,运行时不需要指定变量类型.这一点是不会改变的,但是2015年9月创始人 Guido van Rossum 在 Python 3.5 引入了一个类型系统,允许开发 ...
- TabHost选项卡的实现(二):使用Fragment实现
在上一篇博客<TabHost选项卡的实现(一):使用TabActivity实现>中,讲解了如何使用TabActivity创建管理选项卡,但是,通过TabActivity创建选项卡的方式已经 ...
- clone() 方法
<html> <head> <script type="text/javascript" src="/jquery/jquery.js&qu ...