JAVA && Spring && SpringBoot2.x — 學習目錄
如何根據(jù)日志文件快速定位到應用的行為。
1. layout
Layout組件:
([累奧特] 布局),將日志事件進行格式化,返回一個字符串。
1.1 常用的轉(zhuǎn)換詞
| 轉(zhuǎn)換詞 | 備注 |
|---|---|
| C{length},class{length} | 輸出發(fā)出日志請求調(diào)用者的完整類名。注:類名最右邊部分不會省略,即使長度超過了設定了length長度。 |
| contextName | 日志記錄器上下文的名字 |
| d{pattern},date{pattern},d{pattern,timezone},date{pattern,timezone} | 日志事件的日期,日期轉(zhuǎn)換詞需要一個格式化字符串作為參數(shù),格式化字符串語義與java.text.SimpleDateFormat相同。 |
| F/file | 日志請求的java源文件名,一般生成文件信息比較慢,因此一般不使用這個轉(zhuǎn)換詞 |
| caller{depth} | 輸出日志調(diào)用者位置信息 |
| L/line | 日志請求的行號,由于生成行號信息比較慢,一般不使用這個轉(zhuǎn)換詞 |
| m/msg/message | 日志事件相關(guān)的應用提供信息 |
| M/method | 輸出與日志事件相關(guān)的應用提供的信息 |
| n | 輸出與平臺獨立的行分割符,等價于"\n"或者"\r\n" |
| p/le/level | 輸出日志級別 |
| r/relative(相對的) | 輸出日志事件的相對時間 |
t/thread[斯亂德]
|
輸出產(chǎn)生日志事件的線程名 |
| X{key:-defaultVal} | 輸出與產(chǎn)生日志事件線程相關(guān)的MDC(mapped diagnosis context)。如果mdc轉(zhuǎn)換詞后面花括號中有key,那么value就會輸出,如果值為空,那么輸出默認值;若沒有默認值,則輸出空字符串。 |
ex{depth}[帶婆斯]
|
若發(fā)生異常時,指定輸出異常的行數(shù)。 |
| replace(p){r,t} | 用't'替換p中正則表達式r的內(nèi)容,例如:%replace('%m'){'\s',''}會去替換事件消息中包含的所有空格 |
詳細請見:https://blog.csdn.net/lingbomanbu_lyl/article/details/89852037
1.2 自定義layout
- 繼承自LayoutBase接口即可:
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.LayoutBase;
/**
* @ClassName MySampleLayout
* @Description 日志布局器
* @Date 2019/7/1
* @Version 1.0
**/
public class MySampleLayout extends LayoutBase<ILoggingEvent> {
@Override
public String doLayout(ILoggingEvent event) {
StringBuffer sb=new StringBuffer();
sb.append(event.getTimeStamp()-event.getLoggerContextVO().getBirthTime());
sb.append(" [").append(event.getThreadName()).append("] ");
sb.append(event.getLoggerName()).append("-");
sb.append(event.getFormattedMessage()).append(CoreConstants.LINE_SEPARATOR);
return sb.toString();
}
}
- 配置文件
<configuration>
<springProperty scope="context" name="logPath" source="log.out.path" defalutValue="/app/test.log"/>
<!-- %m輸出的信息,%p日志級別,%t線程名,%d日期,%c類的全名,%i索引【從數(shù)字0開始遞增】,,, -->
<!-- appender是configuration的子節(jié)點,是負責寫日志的組件。 -->
<!-- ConsoleAppender:把日志輸出到控制臺 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="com.tellme.layout.MySampleLayout"/>
<!--<pattern>%d %p [%r] [%t] [%X{traceRootId}] (%file:%line\): %m%n</pattern>-->
<!--<!– 控制臺也要使用UTF-8,不要使用GBK,否則會中文亂碼 –>-->
<!--<charset>UTF-8</charset>-->
</encoder>
</appender>
</configuration>
- 輸出格式:
1230 [restartedMain] com.MmWebApplication-The following profiles are active: test
若包含自定義參數(shù)的layout組件
- java實現(xiàn)類
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.LayoutBase;
import org.apache.commons.lang3.StringUtils;
/**
* @ClassName CustomLayout
* @Description 自定義Layout
* @Date 2019/7/1
* @Version 1.0
**/
public class CustomLayout extends LayoutBase<ILoggingEvent> {
private String prefix = null;
boolean printThreadName = true;
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public void setPrintThreadName(boolean printThreadName) {
this.printThreadName = printThreadName;
}
@Override
public String doLayout(ILoggingEvent event) {
StringBuffer sbuf = new StringBuffer(128);
if (StringUtils.isNotBlank(prefix)) {
sbuf.append(prefix).append(" ");
}
sbuf.append(event.getTimeStamp() - event.getLoggerContextVO().getBirthTime());
sbuf.append(" ");
sbuf.append(event.getLevel());
if (printThreadName) {
sbuf.append(" [");
sbuf.append(event.getThreadName());
sbuf.append("] ");
} else {
sbuf.append(" ");
}
sbuf.append(event.getLoggerName());
sbuf.append(" - ");
sbuf.append(event.getFormattedMessage());
sbuf.append(CoreConstants.LINE_SEPARATOR);
return sbuf.toString();
}
}
- 配置文件
<configuration>
<springProperty scope="context" name="logPath" source="log.out.path" defalutValue="/app/test.log"/>
<!-- %m輸出的信息,%p日志級別,%t線程名,%d日期,%c類的全名,%i索引【從數(shù)字0開始遞增】,,, -->
<!-- appender是configuration的子節(jié)點,是負責寫日志的組件。 -->
<!-- ConsoleAppender:把日志輸出到控制臺 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="com.tellme.layout.CustomLayout">
<prefix>MyPrefix</prefix>
<printThreadName>false</printThreadName>
</layout>
<!--<pattern>%d %p [%r] [%t] [%X{traceRootId}] (%file:%line\): %m%n</pattern>-->
<!--<!– 控制臺也要使用UTF-8,不要使用GBK,否則會中文亂碼 –>-->
<!--<charset>UTF-8</charset>-->
</encoder>
</appender>
</configuration>
- 輸出格式
MyPrefix 1405 INFO com.MmWebApplication - The following profiles are active: test
2. MDC
logback內(nèi)置的日志字段還是比較少的,如果我們需要打印有關(guān)業(yè)務的更多內(nèi)容,包括自定義的一些數(shù)據(jù),需要借助logback的MDC機制,(映射診斷上下文),即將一些運行時的上下文數(shù)據(jù)通過logback打印出來;此時我們需要借助org.sl4j.MDC類。
MDC類的原理:其內(nèi)部持有一個InheritableThreadLocal [因海瑞特包 遺傳]實例,用于保存context數(shù)據(jù),MDC提供了put/get/clear等幾個接口,用來操作ThreadLocal中的數(shù)據(jù);ThreadLocal中的K-V,可以在logback.xml中聲明,最終會打印在日志中。
MDC.put("userId",1000);
那么在logback.xml中,即可在layout中通過"%X{userId}"來打印此信息。
注意:因為使用的是ThreadLocal,在需要線程退出之前,需要清除(clear)MDC里面的數(shù)據(jù);在線程池中使用MDC時,在子線程退出之前調(diào)用MDC.clear()方法。
2.1 MDC實踐
public class LogMDCUtil {
//接口名
private final static String SVR_ID = "SVR_ID";
private final static Logger logger = LoggerFactory.getLogger(LogMDCUtil.class);
/**
* 為日志設置服務名
*
* @param serviceId
*/
public static void setServiceNameToLog(String serviceId) {
try {
MDC.put(SVR_ID, serviceId);
} catch (Exception e) {
logger.error("【Logback設置MDC】-設置服務名失敗", e);
}
}
/**
* 刪除ThreadLocal中關(guān)于ServiceName的數(shù)據(jù),防止內(nèi)存溢出
*/
public static void removeServiceNameToLog() {
//線程執(zhí)行完畢,需要刪除ServiceName
try {
MDC.remove(SVR_ID);
} catch (Exception e) {
logger.error("【Logback設置MDC】-刪除服務名失敗", e);
}
}
}
配置文件:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %p [%t %X{SVR_ID}] (%C{0}:%line\): %m%n</pattern>
<!-- 控制臺也要使用UTF-8,不要使用GBK,否則會中文亂碼 -->
<charset>UTF-8</charset>
</encoder>
</appender>
2.2 MDC實戰(zhàn)—為請求生成唯一編號
如何為每一請求生成唯一編號,并在日志中打印出來呢?
- 借助Redis自增命令,獲取唯一編號
@Service
public class GainIdService {
private static Logger logger = LoggerFactory.getLogger(GainIdService.class);
@Resource
private StringRedisTemplate template;
//前綴 不同系統(tǒng)的前綴可以不相同
public String gainId(String prefix) {
Long seq = template.opsForValue().increment("system:idseq:" + pre.getPrefix());
DecimalFormat df = new DecimalFormat("0000");
String str = df.format(seq);
//substring(int beginIndex),[beginIndex,endIndex]的子串,即獲取后四位編號
str = str.substring(str.length() - 4);
SimpleDateFormat sf=new SimpleDateFormat("yyyyMMddHHmmss");
String timeStr = sf.format(new Date());
return pre.getPrefix() + timeStr + str;
}
}
- 在SpringBoot攔截器中放入MDC中
@Component
public class LogInterceptor implements HandlerInterceptor {
private final static String REQ_ID = "REQ_ID";
@Autowired
GainIdService gainIdService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
String traceId = gainIdService.gainId(IdPrefixEnum.REQ_ID);
MDC.put(REQ_ID, traceId);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
MDC.remove(REQ_ID);
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
- 將攔截器配置到系統(tǒng)中
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
@Autowired
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
}