在生产环境中,要求对日志进行分类切割及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可读取当前的环境参数:
解决步骤
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插件; |
2..在pom文件加入lombok的依赖
|
3.钉钉消息推送