实战分析:Spring boot logback 异常日志发送http请求,记录到数据库

先放个官方API文档吧 https://logback.qos.ch/manual/appenders.html

言归正传,项目中有个功能:

线上项目一旦报错,将报错信息通过http请求发送到自己部署开发环境服务器项目中

将处理一下http请求信息存起来,就是一个小型的异常数据库,

当然也实现了开发环境查库然后界面化,这个就是一个后台系统展现就不说了

主要想学习一下,如何将错误日志收集并发送,并加上自己的感悟和注释记录一下

ps:为了一些敏感的信息吧 我就统一将所有敏感的替换的改为test  或者****号了  

在application.properties中

配置logging.level.*来具体输出哪些包的日志级别

logging.level.root=info
logging.level.com.****.web=debug
#当然可以设置日志文件输出路径以及文件
logging.file=logs/web.log

2、Logback默认配置的步骤

     (1). 尝试在 classpath 下查找文件 logback-test.xml;

     (2). 如果文件不存在,则查找文件 logback.xml;

     (3). 如果两个文件都不存在,logback 用 Bas icConfigurator 自动对自己进行配置,这会导致记录输出到控制台。

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
 <configuration debug="false">
    <!--读取application.properties中的属性  一会代码用的到
        例如 test.logUrl=http://localhost:8098/-->
    <springProperty scope="context" name="logUrl" source="test.logUrl"/>
        
    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />

    <!-- 日志输出级别 -->
    <root level="INFO">
        <!--这里就会依次根据以下三个,分别处理这个info日志-->
        <appender-ref ref="STDOUT" /><!--给控制台输出-->
        <appender-ref ref="FILEOUT" /><!--给logs产生文件输出-->
        <appender-ref ref="ASYNCOUT" /><!--给专门的类  一会写到数据库-->
    </root>

    <!-- Console 输出设置 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <!--logs文件-->
	<appender name="FILEOUT" class="ch.qos.logback.core.rolling.RollingFileAppender">   
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">   
	      	<fileNamePattern>logs/web.%d{yyyy-MM-dd}.log</fileNamePattern>   
	      	<maxHistory>30</maxHistory>    
	    </rollingPolicy>
	    <encoder>
	       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
	    </encoder>
    </appender>    
   

    <!-- 将日志写入数据库 -->
    <appender name="ASYNCOUT" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>8196</queueSize>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="DBOUT" />
    </appender>
    <appender name="DBOUT" class="com.*****.web.DbLoggerAppender">
        <!--这里设置日志级别为error-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
</configuration>

控制台输出就是照旧输出,会有特别整齐的格式和彩色配置,这个格式网上很多,就略过了

文件logs就根据配置文件生成了在项目中,文件夹,文件名,日期一样不落

还有剩下半句配置,我一贴日志里面的文件内容,日期/等级啥的一一对应,本来{}里面的翻译也挺准确的

<encoder>
	 <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>


接下来写数据库部分了看到

<appender name="ASYNCOUT" class="ch.qos.logback.classic.AsyncAppender">

AsyncAppender这个类, logback高级特性使用(三)  里面说的很详细,文章说因此AsynAppender仅仅充当事件转发器,必须引用另一个appender来做事。就是" <appender-ref ref="DBOUT" />"这句话,再接着DBOUT往里面看看配置

对照看spring-boot-started-logging logback常用配置之<filter>标签详解,再详细标注一下,这么一看就明白了:

原来所有的error日志都让filter过滤了给了appender指定的class类了(代码中的DBloggerAppender)

 <appender name="DBOUT" class="com.*****.web.DbLoggerAppender">
        <!--这里设置日志级别为error-->
        <!--将过滤器的日志级别配置为INFO,所有INFO级别的日志交给appender中的class处理,非INFO级别的日志,被过滤掉。-->
        <!--LevelFilter: 级别过滤器,根据日志级别进行过滤。
        如果日志级别等于配置级别,过滤器会根据onMath 和 onMismatch接收或拒绝日志。有以下子节点:-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level><!--<level>:设置过滤级别-->
            <onMatch>ACCEPT</onMatch><!--<onMatch>:用于配置符合过滤条件的操作-->
            <onMismatch>DENY</onMismatch><!--<onMismatch>:用于配置不符合过滤条件的操作-->
        </filter>
</appender>

指定的这个自己写的DBloggerAppender类,就是自定义Appender,

那这个Appender有什么讲究吗,去翻翻官方文档(文章最上面那个链接)

A UML diagram showing OutputStreamAppender and sub-classes

仔细一看类名ConsoleAppender和FileAppender,就知道这两个类是干嘛的了,

在往上一级OutputStreamAppender,这个明显就是IO流才用到的,pass

那我的自定义Appender咋办,再往上啊 UnsynchronizedAppenderBase

这个在官方API文档也有说明,但是英语渣渣的我,还是看看其他资料吧

读logback源码系列文章(五)——Appender 中看到了不少东西,摘抄一下

实现Appender接口有2个base类,一个是AppenderBase,另一个是UnsynchronizedAppenderBase,这2个类非常接近,80%以上的代码都是相同的。

如果我们自己要自定义Appender的话,只要写一个类继承自这2个base类就好 

下面看一个简单的Appender,就是我自己写的MyAppender 

  1. public class MyAppender extends AppenderBase<LoggingEvent> {  
  2.   
  3.     @Override  
  4.     protected void append(LoggingEvent eventObject) {  
  5.         System.out.println(eventObject.getMessage());  
  6.     }  
  7.   
  8. }  

看完以后,哦,原来要重写一下Append方法,再回过头看项目DbLoggerAppender代码,果不其然

public class DbLoggerAppender extends UnsynchronizedAppenderBase<LoggingEvent> {
    @Override
	protected void append(LoggingEvent le) {
           //业务处理
    }
}

到了这一步,基本上就差不多明了日志如何实现自定义的,接下来业务方法具体实现,大部分都贴注释吧.

看官到这里就可以撤了,下面是具体的业务

我自己make学习一下,剖析一下代码是怎么写的,

package com.****.web;

import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.UnsynchronizedAppenderBase;

public class DbLoggerAppender extends UnsynchronizedAppenderBase<LoggingEvent> {

	@Override
	protected void append(LoggingEvent le) {
		
		try {
			/*
			这个和配置是一一对应的,贴上logback.xml的代码

			<!--读取application.properties中的属性  一会代码用的到
             test.logUrl=http://localhost:8098/-->
    		<springProperty scope="context" name="logUrl" source="test.logUrl"/>*/
			String logUrl = context.getProperty("logUrl") + "saveprogramlog";

            String content = le.getFormattedMessage();
			//异常内容,就是log.error("出错", e);的内容
			//如果没有就是以下内容了,例如(贴多点,方便大家理解)
			//{dataSource-11} init error
			//uriSessionMapFullCount is full
			//session ip change too many
			//Application run failed
			/*
			***************************
			APPLICATION FAILED TO START
			***************************

			Description:

			Web server failed to start. Port 8088 was already in use.

			Action:

			Identify and stop the process that's listening on port 8088 or configure this application to listen on another port.
			*/
			
			Map<String, Object> log = new HashMap<String, Object>();
			log.put("timestamp", le.getTimeStamp() );
			log.put("level", le.getLevel().levelStr);
			log.put("content", content);
			log.put("serviceName", "WEB");//给这个服务起个别名 Spring boot可是多服务的...
			log.put("serviceIP", InetAddress.getLocalHost().getHostAddress());//主机IP

			StringBuilder builder = new StringBuilder();
			//这个builder 得到的内容就是密密麻麻的报错信息  有缩进和换行的那种
			//具体实现我也不懂..希望有大拿评论贴链接或者剖析..
			IThrowableProxy thProxy = le.getThrowableProxy();
			while (thProxy != null) {
				builder.append(thProxy.getClassName() + ": " + thProxy.getMessage() );
				builder.append(CoreConstants.LINE_SEPARATOR);
				
		        for (StackTraceElementProxy step : le.getThrowableProxy().getStackTraceElementProxyArray()) {
		            String string = step.toString();
		            builder.append(CoreConstants.TAB).append(string);
		            ThrowableProxyUtil.subjoinPackagingData(builder, step);
		            builder.append(CoreConstants.LINE_SEPARATOR);
		        }
		        
		        thProxy = thProxy.getCause();
			}

	                log.put("cause", builder.toString());
	                //json序列化
			ObjectMapper om = new ObjectMapper();
			String logJson = om.writeValueAsString(log);
			
			HttpHeaders headers = new HttpHeaders();
			// 提交方式,大部分的情况下,提交方式都是表单提交
			headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

			MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
			map.add("log", logJson);

			HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);

			RestTemplate restTemplate = new RestTemplate();
			restTemplate.postForEntity( logUrl, request , String.class );
			
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}


}

猜你喜欢

转载自blog.csdn.net/qq_16513911/article/details/82054190
今日推荐