houyi技术框架设计:日志设计
2019-12-24 本文已影响0人
do_young
背景
在Jdk中所自带的日志功能java.util.logging,往往不能满足系统对日志输出的要求,需要自定义开发或引入第三方的日志组件来增强日志输出的功能。
开源的成熟日志组件有commons-logging-impl,log4j,log4j2,logback等。这里就不比较组件的差异了,主流搜索引擎搜到很多详细的比较文章。
由于不同的日志组件使用到的API不一样,如果在一个框架中更换了日志组件选型,就会导致所有调用日志API的源码文件都需要修改对日志API的引用,比如:
- log4j的Logger为org.apache.log4j.Logger
- logback的Logger为org.slf4j.Logger
为了解决这个问题,实现日志输出调用的统一接口,又有slf4j,common-logging-api等技术组件,这统一接口的组件需要提供配套的配置器组件,如下图所示:
image.png
那么问题来了,如果统一接口的组件也变了呢?所以一些公司会根据自己的业务场景定义日志接口,再在实现类中使用统一日志接口的第三方组件或直接引用第三方日志组件。
虽然通过这种方式输出的日志方式,会减弱第三方日志组件的功能。但也可以做了一些业务封装,降低基于框架开发业务的门槛。
方案
通过以上分析,我采用的技术方案为:
- 自定义日志输出工具类+slf4j+logback
由于现在的分布式系统及微服务架构,应用日志都分布在不同的日志文件中或不同的资源结点中。要在所有的应用结点所输出的日志文件中找到关键信息,对运维来说是一件痛苦的事情。
所以需要将技术方案进行细化,我的细化方案如下: - 1、使用专用日志工具类(LogUtil),记录常规日志。这些日志主要用于业务追踪,一般不会关注这些日志信息,只有遇到不可预期的问题时,但需要借助于这些日志进行分析。如果系统或平台到一定规模的情况下,可以基于ELK等方案,对日志进行归集,实现日志统一查询。
-
2、使用业务埋点工具类(PointUitl),记录业务信息。这些信息主要用于后续业务的统计分析功能。为了方便查询,我选择使用MySql来存储埋点的业务数据。当然你也可以选择logback支持的其它数据库,如下图所示:
image.png -
3、在所有异常处理中,使用日志工具类(LogUtil),记录ERROR日志信息。并使用Sentry归集所有日志信息,对异常信息进行预警及跟踪处理。
整体方案所下图所示:
image.png
实现
LogUtil实现
-
引入slf4j及logback包
image.png -
实现LogUtil类
package com.xxx.log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogUtil {
private static Logger logger = LoggerFactory.getLogger(LogUtil.class);
public static void debug(String message){
logger.debug(message);
}
public static void info(String message){
logger.info(message);
}
public static void warn(String message){
logger.warn(message);
}
public static void warn(String message, Throwable t){
logger.warn(message, t);
}
public static void warn(Throwable t){
logger.warn(t.getMessage(), t);
}
public static void error(String message){
logger.error(message);
}
public static void error(String message, Throwable t){
logger.error(message, t);
}
public static void error(Throwable t){
logger.error(t.getMessage(), t);
}
}
- 在工程工程资源路径下的logback.xml配置文件中,配置LogUtil日志输出。
<!-- 按照每天生成常规日志文件 -->
<appender name="FileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}/${APP_NAME}_${HOST_NAME}.log</file>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 基于时间的分包策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}/${APP_NAME}_${HOST_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!--保留时间,单位:天-->
<maxHistory>${maxHistory}</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${maxFileSize}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="log_async" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FileAppender"/>
</appender>
<logger name="com.xxx.log" level="info" addtivity="false">
<appender-ref ref="log_async" />
</logger>
PointUtil实现
- 实现PointUtil类
package com.xxx.monitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PointUtil {
private static Logger logger = LoggerFactory.getLogger(PointUtil.class);
public static final String SPLIT = "|";
/**
* 格式为:{时间}|{来源}|{对象id}|{类型}|{对象属性(以&分割)}
* 例子1:2016-07-27 23:37:23|business-center|user-login|1|ip=xxx.xxx.xx&userName=张三&userType=后台管理员
* 例子2:2016-07-27 23:37:23|file-center|file-upload|c0a895e114526786450161001d1ed9|fileName=xxx&filePath=xxx
*
* @param id 对象id
* @param type 类型
* @param message 对象属性
*/
public static void info(String type, String id, String message) {
logger.info(type + SPLIT + id + SPLIT + message);
}
public static void debug(String type, String id, String message) {
logger.debug(type + SPLIT + id + SPLIT + message);
}
public static void warn(String type, String id, String message) {
logger.warn(type + SPLIT + id + SPLIT + message);
}
public static void trace(String type, String id, String message) {
logger.trace(type + SPLIT + id + SPLIT + message);
}
public static void error(String type, String id, String message) {
logger.error(type + SPLIT + id + SPLIT + message);
}
}
- 在工程资源路径下的logback.xml配置文件中,配置PointUtil日志输出。
<appender name="point_db_log" class="ch.qos.logback.classic.db.DBAppender">
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> -->
<!-- <level>INFO</level> 日志过滤级别 -->
<!-- </filter> -->
<connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
<dataSource class="org.apache.commons.dbcp.BasicDataSource">
<driverClassName>com.mysql.jdbc.Driver</driverClassName>
<url>jdbc:mysql://10.10.108.114:3306/system-log?characterEncoding=UTF-8</url>
<username>root</username>
<password>password</password>
</dataSource>
</connectionSource>
</appender>
<appender name="point_log_async" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="point_db_log"/>
</appender>
<logger name="com.xxx.monitor" level="info" addtivity="false">
<appender-ref ref="point_log_async" />
</logger>
Exception实现
-
引入sentry包
image.png -
添加spring配置
<bean class="io.sentry.spring.SentryExceptionResolver"/>
- 在工程资源路径下添加sentry.properties配置
dsn=http://e4339498afe44e99aea9065948ad93d2@IP:PROD/2
- 在工程资源路径下的logback.xml配置文件中,配置Exception日志输出。
<appender name="Sentry" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<!-- Optionally add an encoder -->
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="StdoutAppender"/>
<appender-ref ref="Sentry"/>
</root>