日志使用项目实战
一、准备工作
本文计划以log4j以及logback配置及使用为例,以真实的线上项目中的配置展示具体应该如何进行日志相关的配置以及如何使用。
由于计划在日志中打印分布式追踪ID信息traceId,因此需要编写编写的工具类:
package com.netease.cloud.scaffold.util;
import com.netease.cloud.scaffold.common.Constant;
import com.netease.cloud.scaffold.common.http.ThreadContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.MDC;
import java.util.UUID;
public class TraceUtil {
/**
* 启动跟踪
*/
public static void traceStart(String traceId) {
ThreadContext.init();
if (StringUtils.isBlank(traceId)) {
traceId = UUID.randomUUID().toString();
}
MDC.put(Constant.TRACE_ID_KEY, traceId);
ThreadContext.putContext(Constant.TRACE_ID_KEY, traceId);
}
/**
* 结束跟踪
*/
public static void traceEnd() {
MDC.clear();
ThreadContext.clean();
}
/**
* 生成跟踪ID
*
* @return
*/
private static String generateTraceId() {
return UUID.randomUUID().toString();
}
}
编写测试日志打印用到的测试Controller
package com.netease.cloud.scaffold.controller;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/log")
@Slf4j
public class LoggerController {
@RequestMapping(value = "", method = RequestMethod.GET)
@ResponseBody
public String basicLogTest() {
log.debug("debug [{}]", 1);
log.info("info [{}]", 2);
log.warn("warn [{}]", 3);
log.error("error [{}]", 4, new RuntimeException());
return "hello";
}
}
二、实战
2.1 log4j2日志配置示例
2.1.1 引入依赖
由于slf4j只提供一个核心slf4j api(就是slf4j-api.jar包),这个包只有日志的接口,并没有实现,所以如果要使用就得再给它提供一个实现了些接口的日志包,比 如:log4j,common logging,jdk log日志实现包等,但是这些日志实现又不能通过接口直接调用,实现上他们根本就和slf4j-api不一致,因此slf4j又增加了一层来转换各日志实 现包的使用,比如slf4j-log4j12等。
<properties>
<slf4j-api.version>1.7.13</slf4j-api.version>
<slf4j-log4j12.version>1.7.25</slf4j-log4j12.version>
<log4j.version>1.2.15</log4j.version>
<lombok.version>1.18.6</lombok.version>
</properties>
<dependencies>
<!--简化日志声明-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>
<!--桥接-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12.version}</version>
</dependency>
<!--实现类-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
2.1.2 配置文件
配置文件log4j.xml
的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//log4j/log4j Configuration//EN" "log4j.dtd">
<log4j:configuration>
<!-- 日志输出到控制台 -->
<appender name="ConsoleAppender" class="org.apache.log4j.ConsoleAppender">
<!-- 日志输出格式 -->
<layout class="org.apache.log4j.PatternLayout">
<!--日期、级别、traceId、线程、详细类名及行号、日志、换行-->
<param name="ConversionPattern" value="[%d{yyyy-MM-dd HH:mm:ss SSS}] [%-5p] [%X{traceId}] [%t] [%l] %m%n" />
</layout>
<!--过滤器设置输出的级别-->
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<!-- 设置日志输出的最小级别 -->
<param name="levelMin" value="DEBUG" />
<!-- 设置日志输出的最大级别 -->
<param name="levelMax" value="ERROR" />
<!-- 设置日志输出的xxx,默认是false -->
<param name="AcceptOnMatch" value="true" />
</filter>
</appender>
<!-- 日志输出到文件,可以配置多久产生一个新的日志信息文件 -->
<appender name="DailyRollingFileAppender" class="com.netease.cloud.scaffold.common.log.CustomLogAppender">
<param name="File" value="${user.home}/scaffold/logs/scaffold.log" /><!-- 设置最新的日志输出的文件全路径名 -->
<param name="Append" value="true" /><!--是否追加-->
<param name="encoding" value="UTF-8" /><!--设置文件内容的字符编码格式-->
<param name="threshold" value="INFO" />
<!--保留最近30天的日志文件-->
<param name="maxBackupIndex" value="30" />
<!-- 设置日志输出的样式 -->
<layout class="org.apache.log4j.PatternLayout">
<!--日期、级别、traceId、线程、简短类名及方法名、日志、换行-->
<param name="ConversionPattern" value="[%d{HH:mm:ss.SSS}] [%-5p] [%X{traceId}] [%F %M] %m%n" />
</layout>
</appender>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<logger name="org.springframework">
<level value="WARN"/>
</logger>
<logger name="org.mybatis">
<level value="WARN"/>
</logger>
<logger name="org.apache.kafka">
<level value="WARN"/>
</logger>
<logger name="org.redisson">
<level value="WARN"/>
</logger>
<!-- 根logger的设置-->
<root>
<level value="DEBUG"/>
<appender-ref ref="DailyRollingFileAppender"/>
<appender-ref ref="ConsoleAppender"/>
</root>
</log4j:configuration>
2.1.3 自定义翻滚类
扩展的一个按天滚动的appender类, 暂时不支持datePattern设置, 但是可以配置maxBackupIndex。
package com.netease.cloud.scaffold.common.log;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
* 扩展的一个按天滚动的appender类, 暂时不支持datePattern设置, 但是可以配置maxBackupIndex
*
* @author Dan Shan
*/
public class CustomLogAppender extends FileAppender {
/**
* 不允许改写的datepattern
*/
private static final String datePattern = "'.'yyyy-MM-dd";
/**
* 最多文件增长个数
*/
private int maxBackupIndex = 2;
/**
* 文件名+上次最后更新时间
*/
private String scheduledFilename;
/**
* The next time we estimate a rollover should occur.
*/
private long nextCheck = System.currentTimeMillis() - 1;
Date now = new Date();
SimpleDateFormat sdf;
/**
* The default constructor does nothing.
*/
public CustomLogAppender() {
}
/**
* 改造过的构造器
*/
public CustomLogAppender(Layout layout, String filename, int maxBackupIndex) throws IOException {
super(layout, filename, true);
this.maxBackupIndex = maxBackupIndex;
activateOptions();
}
/**
* 初始化本Appender对象的时候调用一次
*/
@Override
public void activateOptions() {
super.activateOptions();
if (fileName != null) {
// perf.log now.setTime(System.currentTimeMillis());
sdf = new SimpleDateFormat(datePattern);
File file = new File(fileName);
// 获取最后更新时间拼成的文件名
scheduledFilename = fileName + sdf.format(new Date(file.lastModified()));
} else {
LogLog.error("File is not set for appender [" + name + "].");
}
if (maxBackupIndex <= 0) {
LogLog.error("maxBackupIndex reset to default value[2],orignal value is:" + maxBackupIndex);
maxBackupIndex = 2;
}
}
/**
* 滚动文件的函数:<br>
* 1. 对文件名带的时间戳进行比较, 确定是否更新<br>
* 2. if需要更新, 当前文件rename到文件名+日期, 重新开始写文件<br>
* 3. 针对配置的maxBackupIndex,删除过期的文件
*/
void rollOver() throws IOException {
String datedFilename = fileName + sdf.format(now);
// 如果上次写的日期跟当前日期相同,不需要换文件
if (scheduledFilename.equals(datedFilename)) {
return;
}
// close current file, and rename it to datedFilename
this.closeFile();
File target = new File(scheduledFilename);
if (target.exists()) {
try {
target.delete();
} catch (SecurityException e) {
e.printStackTrace();
}
}
File file = new File(fileName);
boolean result = file.renameTo(target);
if (result) {
LogLog.debug(fileName + " -> " + scheduledFilename);
} else {
LogLog.error("Failed to rename [" + fileName + "] to [" + scheduledFilename + "].");
}
// 删除过期文件
if (maxBackupIndex > 0) {
File folder = new File(file.getParent());
List<String> maxBackupIndexDates = getMaxBackupIndexDates();
for (File ff : folder.listFiles()) {
// 遍历目录,将日期不在备份范围内的日志删掉
if (ff.getName().startsWith(file.getName()) && !ff.getName().equals(file.getName())) {
// 获取文件名带的日期时间戳
String markedDate = ff.getName().substring(file.getName().length());
if (!maxBackupIndexDates.contains(markedDate)) {
result = ff.delete();
}
if (result) {
LogLog.debug(ff.getName() + " -> deleted ");
} else {
LogLog.error("Failed to deleted old DayRollingFileAppender file :" + ff.getName());
}
}
}
}
try {
// This will also close the file. This is OK since multiple
// close operations are safe.
this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
} catch (IOException e) {
errorHandler.error("setFile(" + fileName + ", false) call failed.");
}
scheduledFilename = datedFilename;
// 更新最后更新日期戳
}
/**
* Actual writing occurs here.
* 这个方法是写操作真正的执行过程.
*/
@Override
protected void subAppend(LoggingEvent event) {
long n = System.currentTimeMillis();
if (n >= nextCheck) {
// 在每次写操作前判断一下是否需要滚动文件
now.setTime(n);
nextCheck = getNextDayCheckPoint(now);
try {
rollOver();
} catch (IOException ioe) {
LogLog.error("rollOver() failed.", ioe);
}
}
super.subAppend(event);
}
/**
* 获取下一天的时间变更点
*
* @param now
* @return
*/
long getNextDayCheckPoint(Date now) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);// 注意MILLISECOND,毫秒也要置0.。。否则错了也找不出来的 calendar.add(Calendar.DATE, 1);
return calendar.getTimeInMillis();
}
/**
* 根据maxBackupIndex配置的备份文件个数,获取要保留log文件的日期范围集合
*
* @return list<' fileName + yyyy-MM-dd '>
*/
List<String> getMaxBackupIndexDates() {
List<String> result = new ArrayList<String>();
if (maxBackupIndex > 0) {
for (int i = 1; i <= maxBackupIndex; i++) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// 注意MILLISECOND,毫秒也要置0...否则错了也找不出来的
calendar.add(Calendar.DATE, -i);
result.add(sdf.format(calendar.getTime()));
}
}
return result;
}
public int getMaxBackupIndex() {
return maxBackupIndex;
}
public void setMaxBackupIndex(int maxBackupIndex) {
this.maxBackupIndex = maxBackupIndex;
}
public String getDatePattern() {
return datePattern;
}
}
2.1.4 测试
访问地址:localhost:8080/log
,
控制台输出显示如下:
日志文件输出显示如下:
日志文件输出不难发现,日志文件中的日志内容只有info级别及其以上的日志内容,并且在打印类信息的时候只有简短的类名和方法名信息。
2.2 lockback日志配置示例
2.2.1 引入依赖
<properties>
<slf4j-api.version>1.7.13</slf4j-api.version>
<logback-classic.version>1.2.3</logback-classic.version>
<lombok.version>1.18.6</lombok.version>
</properties>
<dependencies>
<!--简化日志声明-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--门面-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>
<!--实现类-->
<!--logback-classic依赖logback-core,会自动级联引入-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
</dependency>
</dependencies>
2.2.2 配置文件
配置文件logback.xml
的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<!--定义参数,后面可以通过${app.name}使用-->
<property name="app.name" value="scaffold"/>
<!--ConsoleAppender 用于在屏幕上输出日志-->
<appender name="ConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<!--定义了一个过滤器,在LEVEL之下的日志输出不会被打印出来-->
<!--这里定义了DEBUG,也就是控制台不会输出比DEBUG级别小的日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<!-- encoder 默认配置为PatternLayoutEncoder -->
<!--定义控制台输出格式-->
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss SSS}] [%-5p] [%X{traceId}] [%t] [%l] %m%n</pattern>
</encoder>
</appender>
<appender name="DailyRollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--定义日志输出的路径-->
<!--这里的scheduler.manager.server.home 没有在上面的配置中设定,所以会使用java启动时配置的值-->
<!--比如通过 java -Dscheduler.manager.server.home=/path/to XXXX 配置该属性-->
<file>${scheduler.manager.server.home}/logs/${app.name}.log</file>
<!--这里定义了INFO,也就是控制台不会输出比INFO级别小的日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<!--定义日志滚动的策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--定义文件滚动时的文件名的格式-->
<fileNamePattern>${scheduler.manager.server.home}/logs/${app.name}.%d{yyyy-MM-dd.HH}.log.gz
</fileNamePattern>
<!--30天的时间周期,日志量最大20GB-->
<maxHistory>30</maxHistory>
<!-- 该属性在 1.1.6版本后 才开始支持-->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<!--每个日志文件最大500MB-->
<maxFileSize>500MB</maxFileSize>
</triggeringPolicy>
<!--定义输出格式-->
<encoder>
<pattern>[%d{HH:mm:ss.SSS}] [%-5p] [%X{traceId}] [%F %M] %m%n</pattern>
</encoder>
</appender>
<!--对于类路径以 com.example.logback 开头的Logger,输出级别设置为warn,并且只输出到控制台-->
<!--这个logger没有指定appender,它会继承root节点中定义的那些appender-->
<logger name="org.springframework" level="WARN"/>
<logger name="org.mybatis" level="WARN"/>
<logger name="org.apache.kafka" level="WARN"/>
<logger name="org.redisson" level="WARN"/>
<!--root是默认的logger 这里设定输出级别是debug-->
<root level="DEBUG">
<!--定义了两个appender,日志会通过往这两个appender里面写-->
<appender-ref ref="ConsoleAppender"/>
<appender-ref ref="DailyRollingFileAppender"/>
</root>
<!--通过 LoggerFactory.getLogger("mytest") 可以获取到这个logger-->
<!--由于这个logger自动继承了root的appender,root中已经有stdout的appender了,自己这边又引入了stdout的appender-->
<!--如果没有设置 additivity="false" ,就会导致一条日志在控制台输出两次的情况-->
<!--additivity表示要不要使用rootLogger配置的appender进行输出-->
<!-- <logger name="mytest" level="info" additivity="false">-->
<!-- <appender-ref ref="stdout"/>-->
<!-- </logger>-->
<!--由于设置了 additivity="false" ,所以输出时不会使用rootLogger的appender-->
<!--但是这个logger本身又没有配置appender,所以使用这个logger输出日志的话就不会输出到任何地方-->
<!-- <logger name="mytest2" level="info" additivity="false"/>-->
</configuration>
JVM启动参数配置:
-Dscheduler.manager.server.home=C:\Users\louxiujun\scaffold
JVM启动参数
2.2.3 测试
再次利用准备工作中的Controller进行测试,观察到控制台和日志文件的输出与上面配置log4j时的输出完全一致,这里就不再贴图展示了。
三、其他说明
如果使用了Springboot搭建的项目,如果计划使用logback作为日志框架,那么恭喜你,上述的依赖都不需要引入的,各种stater都会帮忙引入logback的依赖包,因为logback已经成为SpringBoot的默认日志依赖,你需要做的就是配置一下日志配置文件而已。但是如果需要在SpringBoot项目中使用log4j,那么你需要将各个starter中的logback给排除掉,下面举一个具体的例子:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>