在上一篇文章——《Java 日志系列一:详解主流日志框架Log4j、Log4j 2、JUL、Commons Logging和Slf4j&Logback》中,笔者介绍了常用的日志框架,本文作为日志话题的延续,将结合具体案例介绍日志的使用。
在使用日志框架的时候,可以根据应用的诉求在日志配置文件中去自定义日志打印格式和日志级别等信息。如下所示,为 logback.xml 配置样例,其中对配置文件中的关键信息做了解释,其它日志框架的使用示例将在下一部分介绍。
<configuration debug="false">
<property name="LOG_HOME" value="/home" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
encoder>
appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.logFileNamePattern>
<MaxHistory>30MaxHistory>
rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
encoder>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MBMaxFileSize>
triggeringPolicy>
appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
root>
configuration>
在公开的阿里巴巴 java 开发手册中对日志规范也有相关介绍,在此,笔者介绍其中两条规范,即日志命名方式和日志保存期限。
appName_logType_logName.log。
其中 appName 表示应用名;logType 表示日志类型,推荐分类有 stats,monitor,visit 等;logName 为日志描述,这种命名好处是可以快速清晰地了解日志文件类型和目的,便于归类查找。
如何确定日志保持期限是个比较棘手的问题,如果日志存放时间过长,会消耗大量存储资源,甚至会导致磁盘压力过大影响系统稳定性;如果日志存放时间过短,可能导致日志数据“丢失”,出现问题时无法追溯。阿里 java 开发手册中对日志文件保存期限的建议是至少保存 15 天。在实际应用中,可以根据日志文件的重要程度、文件大小以及磁盘空间自行调整,此外,还可以对日志进行监控,定期转储。
通常,日志记录的优先级分为 OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL 或自定义的级别。Log4j 建议只使用四个级别,优先级从高到低分别是 ERROR、WARN、INFO、DEBUG。通过在日志框架的配置文件中设置日志级别,可以控制应用程序中相应级别的日志信息开关。比如配置为 INFO 级别,那么只有等于及高于这个级别的日志才进行处理,应用程序中所有 DEBUG 级别的日志信息不会被打印出来。需要说明的是,日志等级不仅关乎“详细程度”,还关系到适用场景、服务对象等。常见日志等级的说明如下:
通过配置日志输出格式可以控制输出日志信息的内容和样式。以上面日志配置文件 logback.xml 为例,其中 pattern 标签定义了日志的输出格式,默认参数如下表格所示。
参数 | 含义 |
---|---|
%d | 输出日志时间点的日期或时间,如:%d{yyyy-MM-dd HH:mm:ss.SSS} |
%thread | 输出产生该日志事件的线程名 |
%-5level | 日志级别,从左显示 5 个字符宽度 |
%msg | 输出日志消息,即输出代码中指定的消息 |
%n | 输出一个回车换行符,Windows 平台为“\r\n”,Unix 平台为“\n” |
除了上述默认的日志格式参数,Logback 还支持日志格式自定义配置,比如,希望每条日志能打印 ip 地址,实现方式如下:
public class IPLogConfig extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return null;
}
}
<conversionRule conversionWord="ip" converterClass="com.test.conf.IPLogConfig" />
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<layout>
<pattern>[ip=%ip] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
layout>
appender>
与 Logback 的日志输出格式配置相比,log4j 的输出格式配置有所不同,主要参数配置如下所示:
参数 | 含义 |
---|---|
%m | 输出代码中指定的消息 |
%p | 输出优先级,即 DEBUG,INFO,WARN,ERROR,FATAL |
%r | 输出自应用启动到输出该日志信息耗费的毫秒数 |
%c | 输出所属的类目,通常就是所在类的全名 |
%t | 输出产生该日志事件的线程名 |
%n | 输出一个回车换行符,Windows 平台为“\r\n”,Unix 平台为“\n” |
%d | 输出日志时间点的日期或时间,默认格式为 ISO8601,也可以在其后指定格式,比如:%d{yy MMMM dd HH:mm:ss,SSS} |
%l | 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数 |
%F | 输出日志消息产生时所在的文件名称 |
本节将结合例子介绍日志应用中需要注意的基础事项,如异常记录、对象实例记录、日志监控、日志分类等。
记录异常时一定要输出异常堆栈信息。如果没有完整的堆栈信息,那么一旦应用程序出现异常,维护人员将很难定位问题。示例:某应用对处理链路进行功能点的拆分,每个功能点的失败都会抛异常。在服务入口处对异常日志进行分类记录。
try {
this.startOrderProcess(request, result, processName);
} catch (ProductBizException e) {
if (CollectionUtils.isNotEmpty(e.getErrorMessages())) {
e.getErrorMessages().forEach(errorMessage -> {
log.error("biz process error" + e.getMessage(), e);
});
}
} catch (ProductSystemException ex) {
log.error("system error" + ex.getMessage(), ex);
} catch (TMFRuntimeException e) {
ErrorMessage errorMessage = e.getErrorMessage();
if (null != errorMessage) {
log.error("tmf runtime error" + e.getMessage(), e);
}
}
日志中如果输出对象实例,要确保实例类重写了 toString 方法,否则只会输出对象的 hashCode 值,毫无意义。此外,也可以通过 java 反射来获取对应的属性。主要好处是当增加或修改属性的时候,不需要修改 toString 方法,通常采用 fastjson 将对象转换成 json 对象。
示例:在某项目中,有一处 debug 级别的日志信息需要记录服务调用方的请求参数,因此,对 ProductQueryRequest 对象实例重写了 toString 方法,以获取完整的对象信息。
// 采用 slf4j 中占位符输出形式记录 debug 信息
logger.debug("query request: {}", productQueryRequest);
// 其中 ProductQueryRequest 对象重写了 toString 方法
@Override
public String toString() {
return "ProductQueryRequest{" +
"queryOption=" + queryOption +
", productIds=" + productIds +
", MemberId=" + MemberId +
'}';
}
日志分类建议从功能上分为监控日志、统计日志、访问日志。
在实践中,常用的日志级别有 debug、info、warn、error 四个级别,那么应如何具体判断日志的归属级别呢?
通过对日志中的关键字进行监控,可以及时发现系统故障并报警,对系统运维至关重要。服务的监控和报警是一个很大的话题,本节只介绍日志监控报警需要注意的一些问题:
监控配置规范
项目 | 规范 |
---|---|
覆盖率 | 重大问题故障类、财务资损类、用户投诉类,监控通常须 100% 覆盖 |
分层监控 | 监控内容要涵盖系统监控、JVM 监控、关键中间件监控、集群链路监控、依赖上下游业务监控、自身业务监控 |
多维度分析 | 监控形式包括:线下和预发环境可以自动化跑全量业务监控、线上重要功能短周期定时单机监控、线上全量功能周期自动化执行监控、线上流量错误率等大盘分析指标实时监控、离线分析业务指标大盘监控 |
误报率 | 监控配置后,要跟进数据结果合理设置预警,预警配置后要持续优化直到不再出现误报 |
日志文件不宜过大,过大的日志文件会降低日志监控、问题定位的效率。因此需要进行日志文件的切分,具体而言,按天来分割还是按照小时分割,可以根据日志量来决定,原则就是方便开发或运维人员能快速查找日志。如下配置所示,日志文件大小定义为 20M,按天来做文件分割,并保留最近 15 天的数据。
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.logfileNamePattern>
<maxHistory>15maxHistory>
<maxFileSize>20MBmaxFileSize>
<totalSizeCap>20GBtotalSizeCap>
rollingPolicy>
为了防止日志文件将整个磁盘空间占满,需要定期对日志文件进行删除。例如,在收到磁盘报警时,可以将一周以前的日志文件删除或者转储。在实践中,日志转储/删除应实现自动化,当系统监控发现磁盘空间使用率超过设定的阈值时,便根据日志文件名标注的日期进行转储/删除。