在生产环境中,要求对日志进行分类切割及ERR异常类能及时预警,便于及时发现线上问题。

一、技术要求:

(1).日志按照以天为单位存储,超过一定大小后要另起文件,便于查阅,日志可设置过期时间,过期后系统可自动删除,避免海量存储空间

(2) 出现线上ERROR级别异常,需要通过钉钉或者邮件及时预警

(3)把ERROR级别异常信息存储到数据库,方便线上查询

(4)要能方便区分除生产环境及开发环境,开发环境不需要邮件及钉钉预警

二、技术解决思路

对应生产环境异常错误预警,大概有两种解决方

(1)采用全局拦截器,拦截所有异常,在拦截器里实现存储、推送等操作,这个需要考虑并发

(2)采用日志系统自带的相关功能及扩展

本论文介绍通过日志扩展来解决问题

Spring Boot 2.*默认采用了slf4j+logback的形式 ,slf4j是个通用的日志门面,logback就是个具体的日志框架了,我们记录日志的时候采用slf4j的方法去记录日志,底层的实现就是根据引用的不同日志jar去判定了。所以Spring Boot也能自动适配JCL、JUL、Log4J等日志框架,它的内部逻辑就是通过特定的JAR包去适配各个不同的日志框架。

logback日志集成了邮件发送、数据库存储、日志文件分类存储等功能,钉钉推送预警没有集成,需要去扩展

分环境编写配置文件,springboot已有解决方案,通过application.yml里面配置不同的对应文件,logback可读取当前的环境参数:

springboot 集成docker compose springboot 集成钉钉_spring

解决步骤

1.在resources文件夹力创建logback-spring.xml文件,并在yml文件里声明

:注意默认文件名是logback-spring.xml,可省略

yml文件里声明

#日志信息
logging:
  config: classpath:logback-spring.xml #如果不配置config,默认查找logback-spring.xml
  path: D:/log

2.在logback.xml里配置控制台日志和文件输出

<!-- 控制面板输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
    </encoder>
</appender>

<!-- 按照每天生成日志文档 -->
<appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!--日志文档输出的文档名-->
        <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/MIXPAY_%d{yyyy-MM-s}.log</FileNamePattern>
        <!--日志文档保留天数-->
        <MaxHistory>50</MaxHistory>
    </rollingPolicy>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
    </encoder>
    <!--日志文档最大的大小-->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
        <MaxFileSize>50MB</MaxFileSize>
    </triggeringPolicy>
</appender>
<!--输出配置 -->
<root level="INFO">
    <!-- 控制面板输出 -->
    <appender-ref ref="STDOUT"/>
    <!-- 按照每天生成日志文档 -->
    <appender-ref ref="FILE"/>
 

</root>

3.配置插入数据库

pom.xml文件引入数据库相关驱动

<!--spring-jdbc驱动 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
</dependency>


<!--druid 数据库连接池 -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.1.10</version>
</dependency>

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.48</version>
</dependency>

yml里配置相关链接,这里日志数据库虽然用不到,但不配置要报启动错误

spring:
  profiles:
    active: prod
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.gjt.mm.mysql.Driver
    platform: mysql
    url: jdbc:mysql://127.0.0.1:3306/pmlog?useUnicode=true&characterEncoding=UTF-8&useSSL=true
    username: root
    password: 111111
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT1FROMDUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    filters: stat,wall,log4j
    logSlowSql: truelog

在logback.xml文件里配置

<!--连接数据库配置-->
<appender name="db_classic_mysql_pool" class="ch.qos.logback.classic.db.DBAppender">
    <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
        <dataSource class="com.alibaba.druid.pool.DruidDataSource">
            <driverClassName>org.gjt.mm.mysql.Driver</driverClassName>
            <url>jdbc:mysql://127.0.0.1:3306/pmlog?useUnicode=true&characterEncoding=UTF-8&useSSL=true</url>
            <username>root</username>
            <password>111111</password>
        </dataSource>
    </connectionSource>

    <!--这里设置日志级别为error-->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>error</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>

</appender><root level="INFO">
  
    <!-- 数据库输出 -->
    <appender-ref ref="db_classic_mysql_pool"/>
  

</root>

初始化数据库表

 

BEGIN;
DROP TABLE IF EXISTS logging_event_property;
DROP TABLE IF EXISTS logging_event_exception;
DROP TABLE IF EXISTS logging_event;
COMMIT;
BEGIN;
CREATE TABLE logging_event
  (
    timestmp         BIGINT NOT NULL,
    formatted_message  TEXT NOT NULL,
    logger_name       VARCHAR(254) NOT NULL,
    level_string      VARCHAR(254) NOT NULL,
    thread_name       VARCHAR(254),
    reference_flag    SMALLINT,
    arg0              VARCHAR(254),
    arg1              VARCHAR(254),
    arg2              VARCHAR(254),
    arg3              VARCHAR(254),
    caller_filename   VARCHAR(254) NOT NULL,
    caller_class      VARCHAR(254) NOT NULL,
    caller_method     VARCHAR(254) NOT NULL,
    caller_line       CHAR(4) NOT NULL,
    event_id          BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
  );
COMMIT;
BEGIN;
CREATE TABLE logging_event_property
  (
    event_id       BIGINT NOT NULL,
    mapped_key        VARCHAR(254) NOT NULL,
    mapped_value      TEXT,
    PRIMARY KEY(event_id, mapped_key),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
  );
COMMIT;
BEGIN;
CREATE TABLE logging_event_exception
  (
    event_id         BIGINT NOT NULL,
    i                SMALLINT NOT NULL,
    trace_line       VARCHAR(254) NOT NULL,
    PRIMARY KEY(event_id, i),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
  );
COMMIT;

4.配置发送到邮件

pom.xml引入mail包

 

<dependency>
  <groupId>org.codehaus.janino</groupId>
  <artifactId>janino</artifactId>
  <version>3.1.2</version>
</dependency>
<!-- email -->
<dependency>
  <groupId>javax.mail</groupId>
  <artifactId>mail</artifactId>
  <version>1.4.5</version>
</dependency>
<!-- email END-->

logback.xml配置邮件信息

<!--邮件发送-->
<appender name="email" class="ch.qos.logback.classic.net.SMTPAppender">
    <!--smtp 服务器-->
    <smtpHost>smtp.qiye.aliyun.com</smtpHost>
    <!--port-->
    <smtpPort>25</smtpPort>
    <!-- 发给谁的邮件列表,多个人用逗号分隔 -->
    <to>jiangzengkui@lpcollege.com</to>
    <!--发件人,添加邮箱和上面的username保持一致-->
    <from>jyj_stuff@lpcollege.com</from>
    <subject>${ACTIVE_PROFILE_NAME}: %logger - %msg</subject>
    <!--发件人的邮箱-->
    <username>jyj_stuff@lpcollege.com</username>
    <!--发件人的邮箱密码-->
    <password>xxxxxxx</password>
    <SSL>false</SSL>
    <!--是否异步-->
    <asynchronousSending>true</asynchronousSending>
    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
    </layout>
    <cyclicBufferTracker class = "ch.qos.logback.core.spi.CyclicBufferTracker" >
        <bufferSize> 1 </bufferSize>
    </cyclicBufferTracker>
    <!--过滤器-->
    <!-- 这里采用等级过滤器 指定等级相符才发送 -->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
</appender><root level="INFO">
    <appender-ref ref="email"/>

</root>

5.钉钉推送等其他操作

 logback没有像邮件,数据库一样集成,只有继承UnsynchronizedAppenderBase去扩展

注:关于钉钉如何发预警消息,请参考钉钉和springboot的集成

 

(1)扩展类:

package com.jyj.soft.comm;



import ch.qos.logback.classic.spi.ILoggingEvent;

import ch.qos.logback.core.UnsynchronizedAppenderBase;



import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;



/**

* @class: com.jyj.soft.comm.CustomizeApperder

* @description:

* logback的节点扩展类,获取输出,并进行异步处理

* @author: jiangzengkui

* @company: 教育家

* @create: 2020-12-05 09:13

*/

public class CustomizeApperder extends UnsynchronizedAppenderBase<ILoggingEvent> {

@Override



public void append(ILoggingEvent eventObject) {



try {

 //节点输出内容
            String content = eventObject.getMessage();
            //异常的IP
            String  ip= InetAddress.getLocalHost().getHostAddress();
            String run_machine=SpringContextUtil.getActiveProfile();//运行服务器类型如在yml配置的生存、开发、测试等环境
            //System.out.println("当前运行环境: " + run_machine);
           // System.out.println("content内容是: " + content);

            System.out.println("服务器IP:"+ip);
            if("prod".equals(run_machine)){//如果是生产环境
                //1.可发邮件

                //2.可钉钉推送

                String title=">生产环境发生异常";
                 String markDown=">**服务器IP:**"+ip+"\n\n";
                  markDown+=">**异常原因:**"+content;
                  RobotUtil.sendMarkdownMsg(RobotUtil.robot_name_test,null,title,markDown);

                //3.可插入数据库
            }


/** Map<String, String> map = new HashMap<String, String>();



map.put("LOG_LEVEL", eventObject.getLevel().levelStr);



map.put("CONTENT", content.replace("'", "''"));



SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");



map.put("CREATE_DATE", sdf.format(new Date()));

**/



// 拼接SQL语句,然后执行



// … …



} catch (Throwable e) {



String errorMsg = e.getLocalizedMessage();



System.out.println(errorMsg);



}



}

}

这里用到一个帮助类SpringContextUtil,通过非注解的方式或者bean实例和配置属性

package com.jyj.soft.comm;

/**
 * @class: com.jyj.soft.comm.SpringContextUtil
 * @description:
 * 作用:
 *  (1)不通过@Autowired注解来获得对象实例
 *  (2)直接读取propertie,yml文件里的配置值
 * @author: jiangzengkui
 * @company: 教育家
 * @create: 2020-12-05 11:05
 */
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.Map;


/**
 * 获取Spring的ApplicationContext对象工具,可以用静态方法的方式获取spring容器中的bean
 * @author 
 * @date 2019/6/26 16:20
 */
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    /**
     * 系统启动如tomcat时会执行这个方法
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * 获取applicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 通过name获取 Bean.
     */
    public static Object getBean(String name) {
        Object o = null;
        try {
            o = getApplicationContext().getBean(name);
        } catch (NoSuchBeanDefinitionException e) {
            // e.printStackTrace();
        }
        return o;
    }

    /**
     * 通过class获取Bean.
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过name,以及Clazz返回指定的Bean
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

    /**
     * 通过name获取 Bean.
     */
    public static <T> Map<String, T> getBeansOfType(Class<T> clazz) {
        return getApplicationContext().getBeansOfType(clazz);
    }

    /**
     * 获取配置文件配置项的值
     *
     * @param key 配置项key,注意这个key支撑连写
     * persoon:
     *   name: jzk
     * 则key是persoon.name,不是name
     */
    public static String getEnvironmentProperty(String key) {
        return getApplicationContext().getEnvironment().getProperty(key);
    }

    /**
     * 获取spring.profiles.active
     */
    public static String getActiveProfile() {
        return getApplicationContext().getEnvironment().getActiveProfiles()[0];
    }
}

(2)配置logback.xml

<!--自定义节点 -->
<appender name="CustomLog" class="com.jyj.soft.comm.CustomizeApperder">
<!--过滤类 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
 
<level>error</level>
 
</filter>
 
</appender>
 
<root level="INFO">
  
    <!--自定义节点 -->
    <appender-ref ref="CustomLog"/>
   
</root>

5.日志在不同的环境运用

比如只有在生产环境推送钉钉预警和邮件等,开发和测试环境不需要

要用到logback.xml里<springProfile name="xx">这个这个标签,意思是对yml配置文件里的spring.profiles.active的数据

spring: profiles: active: prod

logback根据不同的值,调用不同的appender,如

<!-- 日志输出级别 ,就是说在整个项目中,日志级别在info一上的日志都打印。 root是根日志打印器,只有一个,负责整个系统的日志输出  -->
<!--
springProfile name="prod" 对应着yml配置文件spring.profiles.active: dev
-->
<springProfile name="prod"><!--生产环境 -->
<root level="INFO">
    <!-- 控制面板输出 -->
    <appender-ref ref="STDOUT"/>
    <!-- 按照每天生成日志文档 -->
    <appender-ref ref="FILE"/>
    <!-- 数据库输出 -->
    <appender-ref ref="db_classic_mysql_pool"/>
    <!--自定义节点 -->
    <appender-ref ref="CustomLog"/>
    <!--邮件输出-->
    <appender-ref ref="email"/>

</root>
</springProfile>

<springProfile name="dev"><!--开发环境 -->
    <root level="debug">
        <!-- 控制面板输出 -->
        <appender-ref ref="STDOUT"/>

    </root>
</springProfile>

7.其他知识

 

(1)logback读取yml配置文件的数据

先声明,在用{}引用

yml:
persoon:
    name: jzk
在logback 里读取
定义
<springProperty scope="context" name="pro_name" source="persoon.name"/>
引用
<subject>${pro_name}: %logger - %msg</subject>

(3)日志简写

每个类都要写

LoggerFactory.getLogger(SbDemoApplicationTests.class);很麻烦,可以省掉

//标签
@Slf4j
@RestController
public class HelloCtrol {

    @Autowired
    private Persoon persoon;
    @Autowired
    private Dage dage;

    //访问路径及方法
    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public String hello(){
        dage.h();
//直接用log
        log.error("error================");
        log.warn("warn==============");
        log.info("info==============");
        log.debug("debug===================");
        return "hello, "+persoon.getName()+",address:"+persoon.getAddress();
    }

实现方式

    1.使用idea首先需要安装Lombok插件;

springboot 集成docker compose springboot 集成钉钉_mysql_02

                                                                                                                  

2..在pom文件加入lombok的依赖

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>

 

3.钉钉消息推送