
在开发,测试和支持阶段,关于服务工作的问题很多,而且乍一看,它们都不像:
“发生了什么事?” ,
“有要求吗?” ,
“日期格式是什么?” ,
“为什么服务没有响应?” 等
正确编译的日志将能够在没有开发人员参与的情况下,绝对自主地详细回答这些问题以及许多其他问题。 为了实现这一诱人的目标,Eclair日志库诞生了,旨在与过程中的所有参与者进行对话,而不会花费太多的毯子。
关于毯子和解决方案的功能-如下。
日志记录有什么问题
如果您对了解前提不很感兴趣,可以立即继续对我们的解决方案进行说明。
- 应用程序日志就是它的借口。
通常,只有他才能证明应用程序的成功。 微服务中没有任何状态;相邻的系统是移动的且有缺陷。 “重复”,“重新创建”,“仔细检查”-所有这些都是困难和/或不可能的。 该日志应足够有信息以回答以下问题: “发生了什么事?”。 。 日志应该对所有人都清晰:开发人员,测试人员,有时是分析师,有时是管理员,有时是第一线支持-一切都会发生。 - 微服务与多线程有关。
到达服务的请求(或服务请求的数据)通常由多个线程处理。 所有线程的日志通常混合在一起。 您要区分并行线程还是区分“顺序”线程? 相同的流可重复用于请求的顺序处理,一遍又一遍地为不同的数据集执行其自己的逻辑。 这些“顺序”从另一个平面流出,但读者应清楚其边界。 - 日志应保存原始数据格式。
如果实际上服务是通过XML交换的,则相应的日志应存储XML。 它并不总是紧凑且并不总是美观(但很方便)。 更容易看到成功,更容易分析失败。 在某些情况下,日志可用于手动播放或重新处理请求。 - 日志中的部分数据需要特殊的关系。
通常需要分别存储传入数据(请求),传出数据(答案),对第三方系统的请求以及它们的响应。 它们有特殊要求:保质期或可靠性。 此外,与典型的日志行相比,该数据可以具有令人印象深刻的数量。 - 部分数据不用于日志。
通常应从常规日志中排除以下内容:二进制数据(字节数组,base64等),客户/合作伙伴/其他个人和法人的个人数据。 它总是一个单独的故事,但是系统性的,不适合手动控制。
为什么不动手
以
org.slf4j.Logger
(使用任何
org.slf4j.Logger
Appenders
org.slf4j.Logger
回它)并将所需的所有内容写入日志。 主要方法的入口(如有必要,出口)反映捕获的错误和一些数据。 这有必要吗? 是的,但是:
- 代码量在不合理地增长(通常)。 首先,如果只记录最基本的内容(顺便说一下,使用这种方法获得了成功的支持),这并不是很惊人。
- 用手快速调用记录器变得很懒惰。 用记录器声明一个
static
字段太懒了(嗯,龙目岛可以为我们做到这一点)。 我们开发人员很懒。 我们聆听我们的懒惰,这是崇高的懒惰:它正在不断改变世界,使世界变得更好。 - 微服务在所有方面都不好。 是的,它们虽小又漂亮,但有一个缺点:有很多! 从头到尾一个应用程序通常是由一个开发人员编写的。 遗产不会隐约出现在他的眼前。 高兴的是,开发人员认为自己有义务发明自己的日志格式,其原理和自己的规则,而不必为所施加的规则感到负担。 然后,出色地实现了本发明。 每个类都是不同的。 这有问题吗? 巨大的。
- 重构将破坏您的日志。 即使是万能的主意也无法挽救他。 更新日志与更新Javadoc一样不可能。 同时,至少Javadoc只能由开发人员读取(不,没有人可以读取),但是日志的读者范围更广,并且开发团队不受限制。
- MDC(映射诊断上下文)是多线程应用程序的组成部分。 手动填充MDC要求在工作结束时及时清洁。 否则,您将冒着将一个
ThreadLocal
与不相关数据绑定的风险。 我敢说,用手和眼睛来控制它是不可能的。
这就是我们解决应用程序中这些问题的方式。
什么是埃克莱尔,它能做什么
Eclair是一种简化记录代码编写的工具。 它有助于收集有关源代码的必要元信息,将其与运行时在应用程序中飞行的数据相关联,并将其发送到通常的日志存储库,同时生成最少的代码。
主要目标是使日志对开发过程中的所有参与者均易于理解。 因此,编写代码的便利性,Eclair的好处不会结束,而只是开始。
Eclair记录带注释的方法和参数:
- 记录方法的进入/退出方法/异常/参数/方法返回的值
- 过滤异常以将其专门记录到类型: 仅在必要时
- 根据当前位置的应用程序设置来更改日志的“详细信息”: 例如,在最详细的情况下,它以最短的版本打印参数(全部或部分)的值-仅输入方法的事实
- 以JSON / XML /其他任何格式打印数据(可以与Jackson,JAXB配合使用): 了解哪种格式最适合特定参数
- 了解SpEL(Spring表达式语言)进行声明性安装和MDC自动清理
- 写给N个记录器,理解Eclair的“记录器”是实现
EclairLogger
接口的上下文中的bean:您可以指定应按名称,别名或默认方式处理注释的记录器 - 告诉程序员使用批注的一些错误: 例如,Eclair知道它可以处理动态代理(具有所有附带的功能),因此它可以告诉您
private
方法上的批注将永远无法工作 - 接受元注释(如Spring所说): 您可以使用一些基本注释来定义用于记录的注释-减少代码
- 能够在打印时屏蔽“敏感”数据: 开箱即用的XPath屏蔽XML
- 以“手动”模式编写日志,定义调用者并“扩展”实现
Supplier
的参数: 有机会“懒惰地”初始化参数
如何连接埃克莱尔
源代码在Apache 2.0许可下
在GitHub上发布。
要进行连接,您需要Java 8,Maven和Spring Boot 1.5+。 Maven中央存储库托管的工件:
<dependency> <groupId>ru.tinkoff</groupId> <artifactId>eclair-spring-boot-starter</artifactId> <version>0.8.3</version> </dependency>
入门程序包含
EclairLogger
的标准实现,该实现使用由Spring Boot初始化并带有一些经过验证的设置的日志记录系统。
例子
以下是一些典型的库用法示例。 首先,给出代码片段,然后给出相应的日志,具体取决于特定级别的日志记录的可用性。 可以在项目Wiki的“
示例”部分中找到更完整的示例集。
最简单的例子
默认级别为DEBUG。
@Log void simple() { }
如果级别可用 | ...那么日志将像这样 |
---|
TRACE DEBUG | DEBUG [] rteeExample.simple > DEBUG [] rteeExample.simple < |
INFO WARN ERROR | - |
日志详细信息取决于可用的日志记录级别。
当前位置中可用的日志记录级别会影响日志详细信息。 可用级别越低(即越接近TRACE),则日志越详细。
@Log(INFO) boolean verbose(String s, Integer i, Double d) { return false; }
等级 | 记录 |
---|
TRACE DEBUG | INFO [] rteeExample.verbose > s="s", i=4, d=5.6 INFO [] rteeExample.verbose < false |
INFO | INFO [] rteeExample.verbose > INFO [] rteeExample.verbose < |
WARN ERROR | - |
微调异常记录
可以过滤记录的异常类型。 选定的例外及其后代将被抵押。 在此示例中,将在WARN级别记录
NullPointerException
,在ERROR级别记录
Exception
(默认情况下),并且根本不会记录
Error
(因为
Error
并未包含在第一个注释
@Log.error
的过滤器中,并且明确地排除在了第二个注释的过滤器中)。
@Log.error(level = WARN, ofType = {NullPointerException.class, IndexOutOfBoundsException.class}) @Log.error(exclude = Error.class) void filterErrors(Throwable throwable) throws Throwable { throw throwable; }
等级 | 记录 |
---|
TRACE DEBUG INFO WARN | WARN [] rteeExample.filterErrors ! java.lang.NullPointerException java.lang.NullPointerException: null at rteeExampleTest.filterErrors(ExampleTest.java:0) .. ERROR [] rteeExample.filterErrors ! java.lang.Exception java.lang.Exception: null at rteeExampleTest.filterErrors(ExampleTest.java:0) ..
|
ERROR | ERROR [] rteeExample.filterErrors ! java.lang.Exception java.lang.Exception: null at rteeExampleTest.filterErrors(ExampleTest.java:0) .. |
分别设置每个参数
@Log.in(INFO) void parameterLevels(@Log(INFO) Double d, @Log(DEBUG) String s, @Log(TRACE) Integer i) { }
等级 | 记录 |
---|
TRACE | INFO [] rteeExample.parameterLevels > d=9.4, s="v", i=7 |
DEBUG | INFO [] rteeExample.parameterLevels > d=9.4, s="v" |
INFO | INFO [] rteeExample.parameterLevels > 9.4 |
WARN ERROR | - |
选择和自定义打印输出格式
负责打印格式的“打印机”可以由预处理器和后处理器配置。 在上面的示例中,
maskJaxb2Printer
配置为使得匹配XPath表达式
"//s"
元素使用
"********"
进行掩码。 同时,
jacksonPrinter
打印
jacksonPrinter
。
@Log.out(printer = "maskJaxb2Printer") Dto printers(@Log(printer = "maskJaxb2Printer") Dto xml, @Log(printer = "jacksonPrinter") Dto json, Integer i) { return xml; }
等级 | 记录 |
---|
TRACE DEBUG | DEBUG [] rteeExample.printers > xml=<dto><i>5</i><s>********</s></dto>, json={"i":5,"s":"password"} DEBUG [] rteeExample.printers < <dto><i>5</i><s>********</s></dto> |
INFO WARN ERROR | - |
上下文中的多个记录器
同时使用多个记录器记录该方法:默认情况下,记录器(使用
@Primary
注释)和auditLogger
auditLogger
。 如果要不仅按级别(TRACE-ERROR)分开记录的事件,还可以将它们发送到不同的存储,则可以定义多个记录器。 例如,主记录器可以使用slf4j将日志写入磁盘上的文件,
auditLogger
可以将其特殊格式的特殊数据片写入出色的存储(例如,在Kafka中)。
@Log @Log(logger = "auditLogger") void twoLoggers() { }
MDC管理
退出带注释的方法后,使用注释设置的MDC会自动删除。 可以使用SpEL动态计算MDC记录值。 下面是示例:一个常量感知的静态字符串,计算表达式
1 + 1
,调用
jacksonPrinter
,调用
static
方法
randomUUID
。
退出方法后,不会删除具有
global = true
属性的MDC:如您所见,直到日志末尾的MDC中唯一保留的记录是
sum
。
@Log void outer() { self.mdc(); } @Mdc(key = "static", value = "string") @Mdc(key = "sum", value = "1 + 1", global = true) @Mdc(key = "beanReference", value = "@jacksonPrinter.print(new ru.tinkoff.eclair.example.Dto())") @Mdc(key = "staticMethod", value = "T(java.util.UUID).randomUUID()") @Log void mdc() { self.inner(); } @Log.in void inner() { }
执行以上代码时记录:
DEBUG [] rteeExample.outer >
DEBUG [beanReference={"i":0,"s":null}, sum=2, static=string, staticMethod=01234567-89ab-cdef-ghij-klmnopqrstuv] rteeExample.mdc >
DEBUG [beanReference={"i":0,"s":null}, sum=2, static=string, staticMethod=01234567-89ab-cdef-ghij-klmnopqrstuv] rteeExample.inner >
DEBUG [beanReference={"i":0,"s":null}, sum=2, static=string, staticMethod=01234567-89ab-cdef-ghij-klmnopqrstuv] rteeExample.mdc <
DEBUG [sum=2] rteeExample.outer <
基于参数的MDC安装
如果使用参数上的注释指定MDC,则带注释的参数可用作评估上下文的根对象。 这里的
"s"
是类型为
String
Dto
类的字段。
@Log.in void mdcByArgument(@Mdc(key = "dto", value = "#this") @Mdc(key = "length", value = "s.length()") Dto dto) { }
执行以上代码时记录:
DEBUG [length=8, dto=Dto{i=12, s='password'}] rteeExample.mdcByArgument > dto=Dto{i=12, s='password'}
手动记录
对于“手动”日志记录,足以实现
ManualLogger
的实现。 实现接口提供者的传递参数仅在必要时才“扩展”。
@Autowired private ManualLogger logger; @Log void manual() { logger.info("Eager logging: {}", Math.PI); logger.debug("Lazy logging: {}", (Supplier) () -> Math.PI); }
等级 | 记录 |
---|
TRACE DEBUG | DEBUG [] rteeExample.manual > INFO [] rteeExample.manual - Eager logging: 3.141592653589793 DEBUG [] rteeExample.manual - Lazy logging: 3.141592653589793 DEBUG [] rteeExample.manual < |
INFO | INFO [] rteeExample.manual - Eager logging: 3.141592653589793 |
WARN ERROR | - |
埃克莱尔不做什么
Eclair不知道您将日志存储在何处以及持续多长时间和详细信息。 Eclair不知道您打算如何使用日志。 Eclair会从您的应用程序中仔细提取所需的所有信息,并将其重定向到您配置的存储中。
EclairLogger
的示例配置,该日志将日志定向到具有特定Appender的Logback记录器:
@Bean public EclairLogger eclairLogger() { LoggerFacadeFactory factory = loggerName -> { ch.qos.logback.classic.LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger logger = context.getLogger(loggerName);
此解决方案并不适合所有人。
在开始使用Eclair作为日志记录的主要工具之前,您应该熟悉此解决方案的许多功能。 这些“功能”归因于Eclair基于Spring的标准代理机制。
-下一个代理中包装的代码的执行速度微不足道,但会下降。 对于我们来说,这些损失很少。 如果出现减少交货时间的问题,则有许多有效的优化措施。 拒绝提供方便的信息日志可以被视为一种措施,但并非一开始就是如此。
-StackTrace“膨胀”了一点。 如果您不习惯使用Spring代理的冗长的stackTrace,这可能对您造成麻烦。 由于同样明显的原因,调试代理类也很困难。
-
并非每个类和每个方法都可以被代理 :
private
方法不能被代理,您将需要self来将方法链记录在一个bean中,您不能代理非bean的任何东西,等等。
最后
显然,该工具必须与其他工具一样能够使用,以便从中受益。 而且这种材料只是表面地照亮了我们为寻求完美解决方案而决定移动的一面。
批评,想法,提示,链接-我热烈欢迎您参与该项目的整个过程! 如果您发现Eclair对您的项目有用,我会很高兴。