我们在日常开发中,经常会使用logback打印日志,经常会在日志中打印比如手机号、卡号、邮箱等敏感信息,对数据安全而言是有风险的;但是如果业务程序如果处理这些问题,则需要在每个打印日志的地方都需要进行重复的脱敏操作,繁琐而且影响代码风格,还会有遗漏情况;此时我们可能需要考虑一个相对统一的解决方案,那就是增强logback底层的特性,在日志message落盘之前统一进行检测、脱敏。
我们通常的日志处理面临的通用诉求:
1)超长日志message截取:程序打印的日志message可能非常大,比如超过1M,这种message极大的影响系统的性能,而且通常数据价值比较低。我们应该对这种message进行截取或者直接抛弃。
2)日志格式:通常情况下,我们的production环境的业务日志通过会按需采集、分析、存储,那么日志格式的统一对下游数据处理是非常必要的;为了避免各种原因错误配置了日志格式,我们应该将日志格式规范进行默认集成且限制修改。(我司目前支持两种格式:普通业务日志,通用数据集(监控指标)类日志)
日志格式中,通常包含一些用于数据分拣的系统信息(项目名、部署集群名、IP、云平台、rack等),也包含一些运行时的MDC动态参数值,最终格式是一致的。
3)脱敏:日志中存在特定规则的字符串时,比如手机号,需要对其进行脱敏处理。
设计核心思想:
1)基于PatternLayoutEncoder来实现日志格式的限定,不再使用默认的pattern参数指定格式,而是固定字段格式 + 自定义字段,最终拼接成格式规范。
其中局部可控字段,可以是系统变量、也可以MDC字段列表;固定格式部分,通常是message的头部,统一包含时间、IP、项目名等等。
2)基于logback提供的MessageConverter特性,在message打印之前允许对“参数格式化之后的message”(formattedMessage)进行转换,最终logger打印的实际内容是converter返回的整形后的结果。
那么,我们就可以基于此特性,在convert方法中执行“超长message截取”、“内容脱敏”两个主要操作。
主要类列表(新增类):
1)CommonPatternLayoutEncoder:父类为PatternLayoutEncoder,用于定义日志格式,包括固定字段部分、自定义字段部分,将系统属性、MDC属性等,进行拼接,同时基于logback的option特性,将动态参数传递给MessageConverter;最终拼接成一个字符串,作为pattern属性。同时converter所需要的配置参数,比如消息最大长度、正则表达式、替换策略,都需要通过Encoder声明。
2)ComplexMessageConverter:message转换,只会操作logger.info(String message,Throwable ex)传递的message部分,其中throwable栈信息不会被操作(其实也无法修改)。
Converter可以获取Encoder传递的option参数列表,并初始化相关的处理类;内部实现基于正则表达式来匹配敏感信息。
3)DataSetPatternLayoutEncoder(可选):主要用于限定数据集类的日志格式,它本身不能对敏感信息进行过滤;数据格式主要为了便于数据分析。
1、CommonPatternLayoutEncoder.java
package ch.qos.logback.classic.encoder; import ch.qos.logback.classic.PolicyEnum; import ch.qos.logback.classic.Utils; import java.text.MessageFormat; import static ch.qos.logback.classic.Utils.DOMAIN_DELIMITER; import static ch.qos.logback.classic.Utils.FIELD_DELIMITER; /** * @author liuguanqing * created 2018/6/22 下午8:01 * 适用于基于File的Appender * <p> * 限定我司日志规范,增加有关敏感信息的过滤。 * 可以通过regex指定需要匹配和过滤的表达式,对于符合表达式的字符串,则采用policy进行处理。 * 1)replace:替换,将字符串替换为facade,比如:18611001100 > 186****1100 * 2) drop:抛弃整条日志 * 3)erase:擦除字符串,全部替换成等长度的"****",18611001100 > *********** * <p> * depth:正则匹配深度,默认为12,即匹配成功次数达到此值以后终止匹配,主要考虑是性能。如果一个超长的日志,我们不应该全部替换,否则可能引入性能问题。 * maxLength:单条message的最大长度(不计算throwable),超长则截取,并在message尾部追加终止符。 * <p> * 考虑到扩展性,用户仍然可以直接配置pattern,此时regex、policy、depth等option则不生效。但是maxLength会一致生效。 * 格式样例: * %d{yyyy-MM-dd/HH:mm:ss.SSS}|IP_OR_HOSTNAME|REQUEST_ID|REQUEST_SEQ|^_^| * SYS_K1:%property{SYS_K1}|SYS_K2:%property{SYS_K2}|MDC_K1:%X{MDC_K1:--}|MDC_K2:%X{MDC_K2:--}|^_^| * [%t] %-5level %logger{50} %line - %m{o1,o2,o3,o4}%n * 格式中domain1是必选,而且限定无法扩展 * domain2根据配置文件指定的system properties和mdcKeys动态拼接,K-V结构,便于解析;可以为空。 * domain3是常规message部分,其中%m携带options,此后Converter可以获取这些参数。 **/ public class CommonPatternLayoutEncoder extends PatternLayoutEncoder { protected static final String PATTERN_D1 = "%d'{'yyyy-MM-dd/HH:mm:ss.SSS'}'|{0}|%X'{'requestId:--'}'|%X'{'requestSeq:--'}'"; protected static final String PATTERN_D2_S1 = "{0}:%property'{'{1}'}'"; protected static final String PATTERN_D2_S2 = "{0}:%X'{'{1}:--'}'"; protected static final String PATTERN_D3_S1 = "[%t] %-5level %logger{50} %line - "; //0:message最大长度(超出则截取),1:正则表达式,2:policy,3:查找深度(超过深度后停止正则匹配) protected static final String PATTERN_D3_S2 = "%m'{'{0},{1},{2},{3}'}'%n"; protected String mdcKeys;//来自MDC的key,多个key用逗号分隔。 protected String regex = "-";//匹配的正则表达式,如果此值为null或者"-",那么policy、deep参数都将无效 protected int maxLength = 2048;//单条消息的最大长度,主要是message protected String policy = "replace";//如果匹配成功,字符串的策略。 protected int depth = 128; protected boolean useDefaultRegex = true; protected static final String DEFAULT_REGEX = "'((?<\\d)1[3-9]\\d{9}(?!\\d))'";//手机号,11位数字,并且前后位不再是数字。 //系统参数,如果未指定,则使用default; protected String systemProperties; protected static final String DEFAULT_SYSTEM_PROPERTIES = "project,profiles,cloudPlatform,clusterName"; @Override public void start() { if (getPattern() == null) { StringBuilder sb = new StringBuilder(); String d1 = MessageFormat.format(PATTERN_D1, Utils.getHostName()); sb.append(d1); sb.append(FIELD_DELIMITER) .append(DOMAIN_DELIMITER) .append(FIELD_DELIMITER); //拼装系统参数,如果当前数据视图不存在,则先set一个默认值 if (systemProperties == null || systemProperties.isEmpty()) { systemProperties = DEFAULT_SYSTEM_PROPERTIES; } //系统参数 String[] properties = systemProperties.split(","); for (String property : properties) { String value = Utils.getSystemProperty(property); if (value == null) { System.setProperty(property, "-");//初始化 } sb.append(MessageFormat.format(PATTERN_D2_S1, property, property)) .append(FIELD_DELIMITER); } //拼接MDC参数 if (mdcKeys != null) { String[] keys = mdcKeys.split(","); for (String key : keys) { sb.append(MessageFormat.format(PATTERN_D2_S2, key, key)); sb.append(FIELD_DELIMITER); } sb.append(DOMAIN_DELIMITER) .append(FIELD_DELIMITER); } sb.append(PATTERN_D3_S1); if (PolicyEnum.codeOf(policy) == null) { policy = "-"; } if (maxLength < 0 || maxLength > 10240) { maxLength = 2048; } //如果设定了自定义regex,则优先生效;否则使用默认 if (!regex.equalsIgnoreCase("-")) { useDefaultRegex = false; } if (useDefaultRegex) { regex = DEFAULT_REGEX; } sb.append(MessageFormat.format(PATTERN_D3_S2, String.valueOf(maxLength), regex, policy, String.valueOf(depth))); setPattern(sb.toString()); } super.start(); } public String getMdcKeys() { return mdcKeys; } public void setMdcKeys(String mdcKeys) { this.mdcKeys = mdcKeys; } public String getRegex() { return regex; } public void setRegex(String regex) { this.regex = regex; } public int getMaxLength() { return maxLength; } public void setMaxLength(int maxLength) { this.maxLength = maxLength; } public String getPolicy() { return policy; } public void setPolicy(String policy) { this.policy = policy; } public int getDepth() { return depth; } public void setDepth(int depth) { this.depth = depth; } public Boolean getUseDefaultRegex() { return useDefaultRegex; } public boolean isUseDefaultRegex() { return useDefaultRegex; } public void setUseDefaultRegex(boolean useDefaultRegex) { this.useDefaultRegex = useDefaultRegex; } @Override public String getPattern() { return super.getPattern(); } @Override public void setPattern(String pattern) { super.setPattern(pattern); } public String getSystemProperties() { return systemProperties; } public void setSystemProperties(String systemProperties) { this.systemProperties = systemProperties; } }
1)日志格式部分,仅供参考。
2)MDC参数声明格式为:%X{key},如果上下文中key不存在,则打印"";我们通过使用“:-”来声明其默认值,比如%X{key:--}表示如果key不存在则将打印“-”。
3)根据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}”来声明,有个问题,就是如果这些系统参数不是通过“java -jar -Dkey=value”设置的,而是在运行时通过System.setProperty(key,value)设置的,这些系统参数在logback初始化时是无法获得的,因为logback初始化结束后才会执行application程序;你可以在Encoder的start方法中先设定为这些系统参数设定一个默认值,以免日志打印是出现大量null。
4)MessageFormat格式化字符串时,字符串中如果包含“{”、“}”特殊字符,也需要将这两个字符使用''包含,比如:
MessageFormat.format("展示一下'{'{0}'}'格式化的效果。","hello")
输出>>
"展示一下{hello}格式化效果。"
5)useDefaultRegex:是否使用默认表达式,即手机号数字(连续11位数字,且后位不再跟进数字)。
6)regex:我们也允许用户自定义表达式。此时需要将useDefaultRegex设定为false才能生效。
7)maxLength:默认值为2048,即message的最大长度超过此值后将会被截取,可配置。
8)policy:对于regex匹配成功的字符串,如何处理。(处理规则,参见下文ComplexMessageConverter)
A)drop:直接抛弃,将message重置为一个“终止符号”。比如:
“我的手机号为18611001100”
将会被整形为:
“><”。
B)replace:替换,将敏感信息除去前三、后四位字符之外的其他字符用“*”替换,也是默认策略。比如:
“我的手机号为18611001100”
将会被整形为:
“我的手机号为186****1100”。
C)erase:参数,将匹配成功的字符串,全部替换为等长度的“*”,比如:
“我的手机号为18611001100”
将会被整形为:
“我的手机号为***********”。
9)depth:匹配深度,即message中,最多匹配成功的次数,超过之后将会终止匹配,主要考虑性能,默认值为128。假如message中有200个手机号,那么匹配和替换到128个之后,将会终止操作,剩余的手机号将不会再替换。
10)mdcKeys:指定pattern拼接时,需要植入的mdc参数列表,比如mdcKeys="name,address",那么在pattern中将会包含:
“name:%X{name:--}|address:%X{address:--}”
其实大家主要关注的是option部分,Encoder的主要作用就是拼接一个pattern大概样例:
格式样例: %d{yyyy-MM-dd/HH:mm:ss.SSS}|IP_OR_HOSTNAME|REQUEST_ID|REQUEST_SEQ|^_^| SYS_K1:%property{SYS_K1}|SYS_K2:%property{SYS_K2}|MDC_K1:%X{MDC_K1:--}|MDC_K2:%X{MDC_K2:--}|^_^| [%t] %-5level %logger{50} %line - %m{2048,'(\\d{11})','replace',128} 格式中domain1是必选,而且限定无法扩展 domain2根据配置文件指定的system properties和mdcKeys动态拼接,K-V结构,便于解析;可以为空。 domain3是常规message部分,其中%m携带options,此后Converter可以获取这些参数。
2、ComplexMessageConverter.java
package ch.qos.logback.classic.pattern; import ch.qos.logback.classic.PolicyEnum; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author liuguanqing * created 2018/6/22 下午8:01 * <p> * 日志格式转换器,会为每个appender创建一个实例,所以在配置层面需要考虑兼容。 * 主要目的是,根据配置的regex来匹配message,对于匹配成功的字符串进行替换操作,并返回修正后的message。 **/ public class ComplexMessageConverter extends MessageConverter { protected String regex = "-"; protected int depth = 0; protected String policy = "-"; protected int maxLength = 2048; private ReplaceMatcher replaceMatcher = null; @Override public void start() { List<String> options = getOptionList(); //如果存在参数选项,则提取 if (options != null && options.size() == 4) { maxLength = Integer.valueOf(options.get(0)); regex = options.get(1); policy = options.get(2); depth = Integer.valueOf(options.get(3)); if ((regex != null && !regex.equals("-")) && (PolicyEnum.codeOf(policy) != null) && depth > 0) { replaceMatcher = new ReplaceMatcher(); } } super.start(); } @Override public String convert(ILoggingEvent event) { String source = event.getFormattedMessage(); if (source == null || source.isEmpty()) { return source; } //复杂处理的原因:尽量少的字符串转换、空间重建、字符移动。共享一个builder if (source.length() > maxLength || replaceMatcher != null) { StringBuilder sb = null; //如果超长截取 if (source.length() > maxLength) { sb = new StringBuilder(maxLength + 6); sb.append(source.substring(0, maxLength)) .append("❮❮❮");//后面增加三个终止符 } //如果启动了matcher if (replaceMatcher != null) { //如果没有超过maxLength if (sb == null) { sb = new StringBuilder(source); } return replaceMatcher.execute(sb, policy); } return sb.toString(); } return source; } class ReplaceMatcher { Pattern pattern; ReplaceMatcher() { pattern = Pattern.compile(regex); } String execute(StringBuilder source, String policy) { Matcher matcher = pattern.matcher(source); int i = 0; while (matcher.find() && (i < depth)) { i++; int start = matcher.start(); int end = matcher.end(); if (start < 0 || end < 0) { break; } String group = matcher.group(); switch (policy) { case "drop": return "❯❮";//只要匹配,立即返回 case "replace": source.replace(start, end, facade(group, true)); break; case "erase": default: source.replace(start, end, facade(group, false)); break; } } return source.toString(); } } /** * 混淆,但是不能改变字符串的长度 * * @param source * @param included * @return */ public static String facade(String source, boolean included) { int length = source.length(); StringBuilder sb = new StringBuilder(); //长度超过11的,保留前三、后四,中间全部*替换 //低于11位或者included=false,全部*替换 if (length >= 11) { if (included) { sb.append(source.substring(0, 3)); } else { sb.append("***"); } sb.append(repeat('*', length - 7)); if (included) { sb.append(source.substring(length - 4)); } else { sb.append(repeat('*', 4)); } } else { sb.append(repeat('*', length)); } return sb.toString(); } private static String repeat(char t, int times) { char[] r = new char[times]; for (int i = 0; i < times; i++) { r[i] = t; } return new String(r); } }
此类主要是从CommonPatternLayoutEncoder声明的options(即regix、maxLength、policy、depth)并初始化一个Matcher,针对message进行匹配和替换。正则比较消耗CPU,此外还要认真设计,避免在message处理过程中,新建太多的字符串,否则会大量消耗内存;我们在处理时,尽可能确保主message只有一个,replace时不改变message的长度,可以避免因为重建String导致一些空间浪费。
自所以Converter能够发挥作用,离不开<conversionRule>,参看下文的配置样例。不过还需要注意,每个Appender都会根据<conversionRule>创建一个Converter实例,所以Converter设计时注意代码兼容。
3、logback.xml配置样例
<?xml version="1.0" encoding="UTF-8"?> <configuration> ... <conversionRule conversionWord="m" converterClass="ch.qos.logback.classic.pattern.ComplexMessageConverter"/> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>INFO</level> </filter> <file>你的日志文件名</file> <Append>true</Append> <prudent>false</prudent> <encoder class="ch.qos.logback.classic.encoder.CommonPatternLayoutEncoder"> <useDefaultRegex>true</useDefaultRegex> <policy>replace</policy> <maxLength>2048</maxLength> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <FileNamePattern>你的日志名.%d{yyyy-MM-dd}.%i</FileNamePattern> <maxFileSize>64MB</maxFileSize> <maxHistory>7</maxHistory> <totalSizeCap>6GB</totalSizeCap> </rollingPolicy> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.ConsolePatternLayoutEncoder"/> </appender> ... </configuration>
<conversionRule>节点中的“conversionWord='m'”,其中m就是对应pattern中的“%m”,可以从“%m”获取options列表。
因为CommonPatternLayoutEncoder中已经限定了pattern的格式,所以我们再logback.xml中也不需要再显示的声明pattern参数,基于此可以限定业务日志的格式保持统一。当然如果有特殊情况需要自定义,仍然可以使用<pattern>来声明以覆盖默认格式。
相关推荐
`Logback`和`Log4j`都是广泛使用的日志框架,它们允许自定义日志格式和处理策略。 2. **SpringMVC返回报文脱敏**:`SpringMVC`是Spring框架的一个模块,主要用于构建Web应用。在响应报文时,如果包含了敏感信息(如...
本文将深入探讨如何利用Logback和Slf4j在SpringBoot项目中实现日志的敏感信息脱敏,以保护用户隐私,满足合规性需求。 首先,我们需要理解什么是日志脱敏。日志脱敏是指在记录日志时,对敏感信息如身份证号、手机号...
总结来说,通过结合Spring Boot、Logback和MDC,我们可以轻松地在日志中添加自定义信息,实现链路追踪功能。这不仅有助于在生产环境中快速定位问题,还可以为性能优化和用户体验改进提供有价值的数据。不过,要注意...
通过将日志输出到Kafka,可以方便地将这些日志与其他系统集成,如ELK(Elasticsearch、Logstash、Kibana)堆栈,实现日志的集中收集、分析和检索。 总之,扩展Logback将日志输出到Kafka是一种常见的日志管理实践,...
总的来说,利用Logback和WebSocket实现日志实时传输,可以极大地提升日志管理和监控的效率,同时保持代码的简洁性。对于开发者而言,这是一项非常实用的技术,特别是在需要实时查看和分析日志的项目中。
日志格式 我们可以通过配置 logging.pattern.console 属性或 logging.pattern.file 属性来控制日志的格式。 例如,我们可以将控制台日志的格式设置为: logging.pattern.console= %d{yyyy-MM-dd HH:mm:ss} [%...
Java日志数据脱敏是为了确保在记录日志时,敏感信息不会被泄露,从而保护用户隐私和企业数据安全。在本文中,我们将探讨如何在Java应用程序中实现这一目标。 首先,理解数据脱敏的重要性至关重要。在处理包含敏感...
Shiro支持基于角色的权限控制和基于资源的权限控制。通过注解或拦截器,我们可以限制只有具有特定角色或权限的用户才能访问某些资源。例如,使用`@RequiresRoles`和`@RequiresPermissions`注解。 接下来,我们关注...
它提供了高效、灵活的日志记录机制,允许开发者根据需要调整日志级别、格式和输出目的地。在某些情况下,尤其是处理异常时,Logback可能会生成大量的日志输出,这可能会导致性能问题,甚至淹没真正重要的信息。因此...
在日常工程开发中,日志是非常重要的一部分,通过日志可以迅速定位线上问题,日志框架也有很多选择,日志框架Logback和Log4j是同一个作者,Logback相比于Log4j,性能提高了10倍以上的性能,占用的内存也变小了,并且...
Logback 是一个在Java开发中广泛使用的日志框架,它由Ceki Gülcü创造,作为log4j的后续项目,旨在提供更高的性能、...通过合理的配置和代码实践,可以充分利用Logback实现日志记录的最佳实践,提高问题排查的效率。
它提供事件处理、日志级别的管理、Appender(日志输出目的地)和 Layout(日志格式化)的抽象类和接口。例如,`ch.qos.logback.core.CoreConstants` 包含了常量,`ch.qos.logback.core.AppenderBase` 是所有 ...
这个配置将日志输出到控制台,日志格式包括时间戳、线程名、日志级别、类名和日志消息。 5. **使用SLF4J进行日志记录** 在Java代码中,可以通过SLF4J接口进行日志记录,例如: ```java import org.slf4j.Logger...
对于日志格式化,LogBack提供了`PatternLayout`,允许使用特定的模式字符串来控制日志输出的格式。例如,`%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n`会打印出时间戳、线程名、日志级别、 logger ...
Logback是由log4j创始人设计的另一个开源日志组件,基于slf4j的日志规范实现的框架,性能比log4j要好。 Logback主要分为三个技术模块: logback-core:该模块为其他两个模块奠定了基础。 logback-classic:是log4j...
该项目是一款基于Logback的LoggingAppender日志收集插件,源码文件共计27个,其中包含21个Java源文件、3个XML配置文件、1个Git忽略文件、1个LICENSE文件和1个Markdown文件。此插件旨在将Java日志收集至Redis或Kafka...
Logback是SLF4J(Simple Logging Facade for Java)的一个实现,它提供了灵活的日志记录解决方案。Kafka则是一个高吞吐量的分布式消息系统,常用于实时数据流处理和大数据分析。 这个过程涉及的主要知识点包括: 1...
本文主要介绍如何在Grails3项目中配置logback,实现日志的详细配置,包括按天生成独立的日志文件、日志格式设置以及对日志文件大小的控制。 首先,logback的配置文件是logback.groovy,需要放在项目的grails-app/...
logback日志写logstash配置appender参考