java异常日志,java 日志记录
java基础教程栏目介绍如何解决Java日志级别等问题
如何解决写爬虫IP受阻的问题?立即使用。
1 日志常见错因
1.1 日志框架繁多
不同的类库可能使用不同的日志框架,兼容性是个难题。
1.2 配置复杂且容易出错
日志配置文件通常很复杂。很多同学习惯了直接从其他项目或者网志上复制一个配置文件,却没有仔细研究如何修改。常见错误发生在重复日志记录、同步日志记录的性能和异步日志记录的错误配置中。
1.3 日志记录本身就有些误区
比如获取日志内容的成本,日志级别的乱用等等。不被考虑。
2 SLF4J
Logback、Log4j、Log4j2、commons-logging、JDK自带的java.util.logging等。都是Java系统的日志框架,确实很多。不同的类库可能会选择使用不同的日志框架。因此,日志的统一管理变得非常困难。
SLF4J(Java的简单日志门面)就是为了解决这个问题。
提供了统一的日志facade API,即图中紫色部分实现了中性日志API桥接功能,蓝色部分将各种日志框架API(绿色部分)桥接到SLF4J API。这样,即使你的程序使用各种日志API来记录日志,最终也会桥接到SLF4J facade API。适配功能,红色部分,可以实现SLF4J API和实际日志框架的绑定(灰色部分)。
SLF4J只是一个日志标准,还是它需要一个实际的日志框架?日志框架本身没有实现SLF4J API,所以需要进行预转换。Logback是按照SLF4J API标准实现的,所以不需要绑定模块进行转换。虽然Log4j-over-SLF4J可以用来桥接Log4j到SLF4J,SLF4J-log4j12也可以用来适配slf4j到Log4j,而且它们是画成一排的,但是它不能同时使用它们,否则会出现死循环。Jcl和jul是一样的。
虽然图中有四个灰色的日志实现框架,但是Logback和Log4j是日常业务中最常用的,都是同一个人开发的。Logback可以算是Log4j的改进版,比较推荐,基本上是主流。
Spring Boot的日志框架也是回溯的。那为什么不用手动引入Logback包就可以直接使用Logback呢?
Spring-boot-starter模块依托spring-boot-starter-logging模块,spring-boot-starter-logging模块自动引入logback-classic(包括SLF4J和Logback log框架)以及SLF4J的部分适配器。log4j-to-SLF4J用于将Log4j2 API连接到SLF4J,而jul-to-SLF4J用于将java.util.logging API连接到slf4j。
3 日志重复记录
重复日志不仅给日志查看和统计带来不必要的麻烦,还会增加磁盘和日志收集系统的负担。
logger配置继承关系导致日志重复记录
定义记录四种日志的方法:调试、信息、警告和错误。
回溯配置
配置没什么问题。执行该方法后,存在重复的日志记录。
分析
这个控制台附加器同时安装到两个记录器上,即定义的记录器和根记录器。由于定义的日志程序是从root继承的,相同的日志会被日志程序记录并发送给root,所以应用程序包下的日志中存在重复记录。
这种配置的初衷是什么?
心里想实现自定义日志器配置,让应用中的日志可以临时打开调试级日志。实际上,不需要重复安装附加器,只需拆除安装在记录器下方的附加器即可:
logger name= org . javaedge . time . common errors . logging level= debug /如果自定义记录器需要将日志输出到不同的Appender:
例如
应用程序日志输出到文件app.log其他框架日志输出到控制台可以将logger的additivity属性设置为false,不会继承root的Appender。
错误配置LevelFilter造成日志重复
将日志记录到控制台时,根据不同的级别将日志记录到两个文件中。
执行结果
INFO.log文件包含与预期不同的信息、警告和错误日志。
ERROR.log包含两个级别的日志,WARN和ERROR,导致日志的重复收集。
事故责任
有些公司使用自动化ELK方案收集日志,日志会同时输出到控制台和文件。开发人员在本地测试时并不关心文件中记录的日志。但是,在测试和生产环境中,由于开发人员没有访问服务器的权限,因此很难在原始日志文件中找到重复的问题。
为什么日志会重复?
ThresholdFilter源码解析
当日志级别配置级别返回NEUTRAL时,继续调用过滤器链中的下一个过滤器;否则返回DENY,直接拒绝登录。在这种情况下,我们将ThresholdFilter设置为WARN,这样我们就可以记录WARN和ERROR级别的日志。
LevelFilter
用于比较日志级别,然后进行相应的处理。
如果有匹配,调用onMatch中定义的处理方法:默认情况下会交给下一个过滤器(AbstractMatcherFilter基类中定义的默认值);否则调用onMismatch中定义的处理方法:默认情况下也会交给下一个过滤器。
不像ThresholdFilter,LevelFilter只靠配置等级真的不行。
由于未配置onMatch和onMismatch属性,过滤器失败,导致记录高于INFO级别的日志。
修正
配置LevelFilter的onMatch属性为ACCEPT,表示接收INFO级别的日志;将onMismatch属性配置为DENY,这意味着除了INFO级别之外,不记录任何记录:
这样,_INFO.log文件将只有INFO级别的日志,不会有日志重复。
4 异步日志提高性能?
在确切地了解了如何正确地将日志输出到文件之后,是时候考虑如何避免日志成为系统性能的瓶颈了。这样可以解决磁盘(如机械磁盘)IO性能差,日志量大时如何记录日志的问题。
使用两个Appender定义以下日志配置:
FILE是一个FileAppender,用来记录所有日志;
CONSOLE是一个ConsoleAppender,用来记录标有时间的日志。
如果将大量日志输出到一个文件中,日志文件将会非常大。如果混合了性能测试结果,将很难找到该日志。所以这里使用EvaluatorFilter根据标记过滤日志,过滤后的日志单独输出到控制台。在这种情况下,输出测试结果的日志标有时间。
测试代码:记录指定数量的大型日志,每个日志包含1MB的模拟数据,最后记录一个用时间标记的方法执行的耗时日志。执行程序后,可以看到记录1000个日常日志需要5.1秒,调用10000个日志需要39秒。
对于只记录文件日志的代码,这需要太长时间。
源码解析
FileAppender从OutputStreamAppender继承。
追加日志时,日志直接写入OutputStream,属于同步日志记录。
这就是为什么要花很长时间写很多日志的原因。如何在不影响业务逻辑执行时间和吞吐量的情况下,写大量日志?
AsyncAppender
AsyncAppender使用回退
可以实现异步日志记录。AsyncAppender类似于decorator模式,在类中添加新的函数,而不改变其原有的基本函数。这可以将AsyncAppender附加到其他Appender,并使其异步。
定义一个异步Appender ASYNCFILE,将之前同步文件日志记录的FileAppender包装起来,这样就可以异步的将日志记录到文件中。
记录1000条日志和10000条日志需要时间,分别是537毫秒和1019毫秒。异步日志真的那么高性能吗?没有,因为它没有记录所有的日志。
AsyncAppender异步日志坑
记录异步日志突发内存记录异步日志丢失记录异步日志阻塞。
案例
模拟慢速记录场景:
首先用一个从ConsoleAppender继承的自定义MySlowAppender作为登录控制台的输出设备,写入日志时休眠1秒。
在配置文件中使用AsyncAppender,并将MySlowAppender包装为异步日志记录。
试验码
耗时短,但日志丢失:记录1000条日志,控制台只能搜索215条日志,日志的行号变成问号。
原因AsyncAppender提供了一些配置参数,但是目前没有用。
源码解析
includeCallerData
默认情况下为False:对于方法行号和方法名等信息,不显示queueSize。
控制阻塞队列的大小。使用的阻塞队列是ArrayBlockingQueue。默认容量为256:内存中最多存储256个日志,丢弃阈值。
丢弃日志的阈值,以防止队列满时阻塞。如果队列的默认剩余容量小于队列长度的20%,跟踪、调试和信息级别的日志neverBlock将被丢弃。
控制队列满时,加入的数据是否直接丢弃,不会阻塞等待,默认是错误的队列满时:报价不阻塞,而放会阻塞永不封锁为真实的时,使用公开招股类AsyncAppender扩展async appender baseiloggingevent {
//是否收集调用方数据
boolean includeCallerData=false;
受保护的布尔值isDiscardable(ILoggingEvent事件){
级别级别=事件。getlevel();
//丢弃信息级日志
返回level.toInt()=Level .INFO _ INT
}
受保护的空的预处理(ILoggingEvent事件对象){
事件对象。preparefordeferredprocessing();
if (includeCallerData)
事件对象。getcallerdata();
} }公共类AsyncAppenderBaseE扩展UnsynchronizedAppenderBaseE实现AppenderAttachableE {
//阻塞队列:实现异步日志的核心
阻塞队列
//默认队列大小
public static final int DEFAULT _ QUEUE _ SIZE=256;
int QUEUE SIZE=DEFAULT _ QUEUE _ SIZE;
static final int UNDEFINED=-1;
int discardingThreshold=UNDEFINED;
//当队列满时:加入数据时是否直接丢弃,不会阻塞等待
布尔型neverBlock=false
@覆盖
public void start() {
.
blocking queue=new arrayblockingqueue(queueSize);
if(discardingThreshold==UNDEFINED)
//默认丢弃阈值是队列剩余量低于队列长度的20%,参见isQueueBelowDiscardingThreshold方法
discardingThreshold=队列大小/5;
.
}
@覆盖
受保护的void append(E eventObject) {
if(isQueueBelowDiscardingThreshold()可丢弃(事件对象)){//判断是否可以丢数据
返回;
}
预处理(事件对象);
put(事件对象);
}
private boolean isQueueBelowDiscardingThreshold(){
返回(阻塞队列。remainingcapacity()discardingThreshold);
}
私有void put(E eventObject) {
if (neverBlock) { //根据永不封锁决定使用不阻塞的提供还是阻塞的放方法
阻塞队列。offer(事件对象);
}否则{
不间断地放入(事件对象);
}
}
//以阻塞方式添加数据到队列
私有空的不间断地放入(E事件对象){
布尔中断=假;
尝试{
while (true) {
尝试{
阻塞队列。put(事件对象);
打破;
} catch (InterruptedException e) {
中断=真;
}
}
}最后{
如果(中断){
Thread.currentThread().中断();
}
}
}}默认队列大小256,达到80%后开始丢弃=信息级日志后,即可理解日志中为什么只有两百多条信息日志了。
queueSize 过大
可能导致OOM
queueSize 较小
默认值256就已经算很小了,且discardingThreshold设置为大于0(或为默认值),队列剩余容量少于discardingThreshold的配置就会丢弃=信息日志。这里的坑点有两个:
因为discardingThreshold,所以设置queueSize时容易踩坑。
比如本案例最大日志并发1000,即便置queueSize为1000,同样会导致日志丢失discardingThreshold参数容易有歧义,它不是百分比,而是日志条数。对于总容量10000队列,若希望队列剩余容量少于1000时丢弃,需配置为1000
neverBlock 默认false
意味总可能会出现阻塞。
若discardingThreshold = 0,那么队列满时再有日志写入就会阻塞若discardingThreshold != 0,也只丢弃信息级日志,出现大量错误日志时,还是会阻塞队列大小、丢弃阈值和永不封锁三参密不可分,务必按业务需求设置:
如果绝对性能优先,neverBlock=true,永不阻塞;如果数据优先,永不丢失,设置discardingThreshold=0,即使INFO级别日志也不会丢失。但是最好把queueSize设置的大一点。毕竟默认的queueSize显然太小,太容易阻塞。如果两者兼顾,可以舍弃不重要的日志,将queueSize设置成大一点的,然后设置一个大于discardingThreshold的合理日志配置。两个最常见的误解是
再看日志本身的误区。
使用日志占位符就无需判断日志级别?
验证码:返回结果需要1秒。如果记录了调试日志,并且只设置了=INFO log,程序会不会也需要1秒?
三种测试方法:
通过拼接字符串记录slowString,通过占位符记录slowString。首先,判断在日志级别是否启用了DEBUG。
前两个方法调用slowString,所以需要1s。第二种方法是使用占位符来记录slowString。这样虽然允许传递Object而不显式拼接字符串,只是延迟了(如果不记录日志,会保存)拼接日志参数对象.toString()和个字符串的耗时。
除非事先判断好日志级别,否则这种情况下会调用slowString。
因此,使用{}占位符并不能通过延迟参数值获取来解决日志数据获取的性能问题。
除了提前判断日志级别,还可以通过lambda表达式延迟参数内容获取。但是SLF4J的API还不支持lambda,所以你需要使用Log4j2 log API,用**@Log4j2**注释替换Lombok的@Slf4j注解,提供lambda表达式参数的方法:
这样,当你调用debug并签署Supplier?时,参数会延迟到你真正需要日志的时候:
因此,debug4不调用slowString方法。
改成Log4j2 API就行了,真正的日志还是Logback,这就是SLF4J适配的优势。
总结
SLF4J统一了Java日志框架。当使用SLF4J时,你应该弄清楚它的桥接API和绑定。如果程序启动时出现SLF4J错误提示,可能是配置问题。可以使用Maven的dependency:tree命令来整理依赖关系。异步日志通过用空间换取时间来解决性能问题。但毕竟空间有限。当空间已满时,考虑阻塞并等待或丢弃日志。如果您不希望丢弃重要的日志,请选择阻塞和等待;如果您希望程序不被日志阻止,您需要丢弃日志。日志框架提供的参数化日志记录方式并不能完全代替日志级别判断。如果你的日志量很大,而且获取日志参数花费很大,那么就要判断日志的级别,避免花时间获取日志参数而不进行日志记录。以上是了解Java日志级别,重复记录,丢失日志的详细情况。请多关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。