原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。
在日常开发中,我们经常会使用logback
打印日志,还会包含一些敏感内容。比如手机号
、卡号
、邮箱等,这对数据安全而言是有风险的。
但是如果让业务去处理这些问题,则需要在每个打印日志的地方,进行重复的脱敏操作,不仅繁琐影响代码风格,还会有遗漏情况。
这个时候,我们就需要考虑一个相对统一的解决方案,通过增强logback
,在日志message
落盘之前,统一进行检测、脱敏。
一、需求来源
我们通常的日志处理,面临的通用诉求:
1)超长日志message截取: 程序打印的日志message
可能非常大,比如超过1M
,这种message极大的影响系统的性能,而且通常数据价值比较低。我们应该对这种message进行截取或者直接抛弃。
2)日志格式统一: 通常情况下,生产环境的业务日志通过会按需采集、分析、存储,那么日志格式的统一对下游数据处理是非常必要的。
为了避免错误配置了日志格式,我们应该将日志格式规范,默认进行集成且限制修改。
日志格式中,通常包含一些用于数据分拣的系统信息(例如,项目名、部署集群名、IP、云平台、rack等),也包含一些运行时的MDC
动态参数值,最终格式要求是一致的。
3)脱敏: 日志中存在特定规则的字符串时,比如手机号,需要对其进行脱敏处理。
二、设计核心思想
我们可以基于PatternLayoutEncoder
来实现日志格式的限定,不再使用默认的pattern参数指定格式,而是固定字段格式 + 自定义字段,最终拼接成格式规范。
其中,局部可控字段,可以是系统变量、也可以MDC字段列表;固定格式部分,通常是message的头部,包含时间、IP、项目名等等。
基于logback
提供的MessageConverter
特性,在message打印之前,允许对“参数格式化之后的message”(formattedMessage)进行转换,最终logger打印的实际内容是converter返回的整形后的结果。
那么,我们就可以基于此特性,在convert方法中执行“超长message截取”、“内容脱敏”两个主要操作。
三、设计编码
设计理念
CommonPatternLayoutEncoder:
父类为PatternLayoutEncoder
,用于定义日志格式,包括固定字段部分
、自定义字段部分
,将系统属性、MDC属性等,进行拼接。
同时,基于logback
的option
特性,将动态参数传递给MessageConverter
,最终拼接成一个字符串,作为pattern属性。同时converter所需要的配置参数,比如消息最大长度、正则表达式、替换策略,都需要通过Encoder声明。
ComplexMessageConverter:
message转换,只会操作logger.info(String message,Throwable ex)
传递的message部分。其中,throwable栈信息不会被操作(其实也无法修改)。
Converter可以获取Encoder传递的option参数列表,并初始化相关的处理类;内部实现基于正则表达式来匹配敏感信息。
DataSetPatternLayoutEncoder(可选):
主要用于限定数据集类的日志格式,它本身不能对敏感信息进行过滤;数据格式主要为了便于数据分析。
主要代码
下面是CommonPatternLayoutEncoder.java
的主要代码,详细参见注释。
1 | 复制代码package ch.qos.logback.classic.encoder; |
代码介绍
下面简单介绍一下上面的代码。
MDC参数声明格式为:%X{key}
,如果上下文中key不存在,则打印””;我们通过使用:-
来声明其默认值。比如,%X{key:--}
表示,如果key不存在则将打印“-”。
根据logback
的规定,option参数列表需要声明在某个字段中,并配合<conversionRule>
才能生效,以本文为例,我们主要对message进行整形。所以option参数声明在%m
上,其格式为:%m{o1,o2...}
,多个option之间以,
分割。o1,o2的字面值,可以在Converter中获取。简单来说,你需要将参数传递给Converter时,这些参数必须以option方式声明在某个字段上,否则没法做。
特别注意,如果option参数中包含{
、}
时,必须将option参数使用''
包括。比如%m{2048,'\\d{11}','replace','128'}
,为了便于理解,建议所有的option参数都使用''
逐个包含。
此外,如果你对日志格式中,还需要使用系统参数(System Property),可以使用%property{key}
来声明。比如,
1 | 复制代码MessageFormat.format("展示一下'{'{0}'}'格式化的效果。","hello") |
输出>>
1 | 复制代码展示一下{hello}格式化效果。 |
还有一些比较重要的参数。
useDefaultRegex
是否使用默认表达式,即手机号数字(连续11位数字,且后位不再跟进数字)。
regex
我们也允许用户自定义表达式。此时需要将useDefaultRegex设定为false才能生效。
maxLength
默认值为2048,即message的最大长度超过此值后将会被截取,可配置。
policy
对于regex匹配成功的字符串,如何处理。(处理规则,参见下文ComplexMessageConverter)
A)drop 直接抛弃,将message重置为一个“终止符号”。比如:
1 | 复制代码我的手机号为18611001100 |
将会被整形为:
1 | 复制代码>< |
B)replace 替换,将敏感信息除去前三、后四位字符之外的其他字符用“*”替换,也是默认策略。比如:
1 | 复制代码我的手机号为18611001100 |
将会被整形为
1 | 复制代码我的手机号为186****1100 |
C)erase:参数,将匹配成功的字符串,全部替换为等长度的“*”,比如:
1 | 复制代码我的手机号为18611001100 |
将会被整形为:
1 | 复制代码我的手机号为*********** |
depth
匹配深度,即message中,最多匹配成功的次数,超过之后将会终止匹配,主要考虑性能,默认值为128。假如message中有200个手机号,那么匹配和替换到128个之后,将会终止操作,剩余的手机号将不会再替换。
mdcKeys
指定pattern拼接时,需要植入的mdc参数列表,比如mdcKeys=”name,address”,那么在pattern中将会包含:
1 | 复制代码name:%X{name:--}|address:%X{address:--} |
其实大家主要关注的是option部分,Encoder的主要作用就是拼接一个pattern大概样例:
1 | 复制代码%d{yyyy-MM-dd/HH:mm:ss.SSS}|IP_OR_HOSTNAME|REQUEST_ID|REQUEST_SEQ|^_^| |
1 | 复制代码%X{MDC_K2:--}|^_^| |
格式中,domain1是必选,而且限定无法扩展 。
domain2根据配置文件指定的system properties和mdcKeys动态拼接,K-V结构,便于解析;可以为空。
domain3是常规message部分,其中%m携带options,此后Converter可以获取这些参数。
日志格式转换器
1 | 复制代码package ch.qos.logback.classic.pattern; |
这个类,主要是从CommonPatternLayoutEncoder
声明的options(即regix、maxLength、policy、depth)初始化一个Matcher,针对message进行匹配和替换。正则比较消耗CPU。我门还要避免在message处理过程中,新建太多的字符串,否则会大量消耗内存;在处理时,尽可能确保主message只有一个,replace时不改变message的长度,可以避免因为重建String导致一些空间浪费。
之所以Converter能够发挥作用,离不开<conversionRule>
,参看下文的配置样例。不过还需要注意,每个Appender都会根据<conversionRule>
创建一个Converter实例,所以Converter设计时注意代码兼容。
1 | 复制代码<?xml version="1.0" encoding="UTF-8"?> |
注意<conversionRule>
节点中的conversionWord='m'
,其中m
就是对应pattern中的%m
,可以从%m
获取options列表。
因为CommonPatternLayoutEncoder中已经限定了pattern的格式,所以我们在logback.xml
中也不需要再显示的声明pattern参数。基于此,可以限定业务日志的格式保持统一。当然,如果有特殊情况需要自定义,仍然可以使用<pattern>
来声明以覆盖默认格式。
作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。
本文转载自: 掘金