Spring MVC結合日誌框架給一次請求日誌列印出唯一標示
https://blog.csdn.net/helloworldwt/article/details/51818309
1.簡介
在tomcat web專案中, 一次使用者請求在同步情況下都是交給同一個執行緒去處理,如果能知道這個執行緒的處理路徑,可以方便線上問題的排查、程式碼的除錯。因此可以讓同一次請求的執行緒日誌帶上同一個唯一的token,在查詢日誌時,grep token可以把相關的日誌都查詢出來。這時,如果已經在程式碼邏輯關鍵部分已經列印了日誌,那麼可以準確知道這次請求執行了哪些程式碼。
2.日誌框架原理簡析
日誌框架通常提供了一定的靈活性,以log4j和log4j2為例。
3.實現
3.1 對日誌框架進行擴充
繼承log4j的PatternParser類,並重寫finalizeConverter方法,實現對自定義字元的解析,並ThreadTokenHelper.getThreadToken()返回自定義的token資訊。
public class Log4jExPatternParser extends PatternParser {
public Log4jExPatternParser(String pattern) {
super(pattern);
}
@Override
protected void finalizeConverter(char c) {
if (c == 'T') {
this.addConverter(new ExPatternConverter(this.formattingInfo));
} else {
super.finalizeConverter(c);
}
}
private static class ExPatternConverter extends PatternConverter {
public ExPatternConverter(FormattingInfo fi) {
super(fi);
}
@Override
protected String convert(LoggingEvent event) {
return String.valueOf(ThreadTokenHelper.getThreadToken());
}
}
}
繼承log4j的PatternLayout類,並重寫createPatternParser方法。
public class Log4jExPatternLayout extends PatternLayout {
public Log4jExPatternLayout(String pattern){
super(pattern);
}
public Log4jExPatternLayout(){
super();
}
@Override
protected PatternParser createPatternParser(String pattern) {
return new Log4jExPatternParser(pattern);
}
}
log4j配置檔案的配置,PatternLayout使用自己定義的Log4jExPatternLayout。[%T]就是我們token資訊的位置,通過上面的重寫log解析,可以解析'T'這個字元。
<!-- console log-->
<appender name="stdout" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out" />
<layout class="com.my.extend.log.Log4jExPatternLayout">
<param name="ConversionPattern" value="%d %p [%c] [%T] - (%m)%n" />
</layout>
</appender>
<!-- root logger -->
<root>
<priority value="info" />
<appender-ref ref="stdout" />
</root>
3.2 生成唯一token
生成token演算法,head與時間相關,精確到毫秒,body是執行緒號,foot是隨機數。head模一天的毫秒數(86400000),這樣生成的token在某一天內,可以保證唯一性。
private static String genThreadToken(){
long head = System.currentTimeMillis()%86400000;
long body = Thread.currentThread().getId();
int foot = ThreadLocalRandom.current().nextInt(1000);
String token = head + "_" + body + "_" + foot;
ThreadContext.getContext().put("token", token);
return token;
}
token的儲存與清除,token儲存線上程的threadlocal中,每一次請求結束清除執行緒當前的token值,下一次請求就會重新獲取最新的token。如果請求發生異常,也需要清除token。
public class ThreadContext {
private static ThreadContext CONTEXT = new ThreadContext();
private static ThreadLocal<Map<String, Object>> CONTEXT_LOCAL = new ThreadLocal<Map<String, Object>>() {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<String, Object>();
}
};
private ThreadContext() {
}
public static ThreadContext getContext() {
return CONTEXT;
}
public boolean put(String key, Object value) {
CONTEXT_LOCAL.get().put(key, value);
return true;
}
public Object get(String key) {
return CONTEXT_LOCAL.get().get(key);
}
public Object remove(String key) {
return CONTEXT_LOCAL.get().remove(key);
}
public void clear() {
CONTEXT_LOCAL.get().clear();
}
}
public static String getThreadToken(){
String token = (String) ThreadContext.getContext().get("token");
if(token == null){
return genThreadToken();
}
return token;
}
public static void clear(){
ThreadContext.getContext().remove("token");
}
3.3 請求的處理
通過Spring MVC攔截器intercepter,可以在一個請求前後做一些處理,通過把唯一token儲存線上程中的ThreadLocal中,token能在同一個執行緒中進行傳遞。在這裡主要需要,當一個請求結束後,把儲存在當前ThreadLocal中的token清除,那麼當下一個請求處理時,會重新獲取新的token儲存在ThreadLocal中。遇到異常丟擲時,程式碼不會執行到這裡,這時需要在異常處理的地方對執行緒ThreadLocal中的token進行清除。
intercepter清除token,同時可以列印出請求訪問的相關資訊,訪問使用者, 引數,執行時間
public class RequestLoggerHandler extends HandlerInterceptorAdapter {
private static Logger LOGGER = LogManager.getLogger(RequestLoggerHandler.class);
private ThreadLocal<Long> startTime = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI();
if (DefaultConstant.DEFAULT_WELCOME_URL.equals(url)){
return true;
}
startTime.set(System.currentTimeMillis());
OpWorker user = new OpWorker(); //TODO
AjaxRequest ajaxRequest = new AjaxRequest();
LOGGER.info("[http]preHandle url: {"+request.getRequestURL()+"}, user:{"+JSON.toJSONString(user)+"}, httpBodyParams: {"+toJSONString(ajaxRequest.getParams())+"}, urlParams: {"+toJSONString(request.getQueryString())+"}" );
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
String url = request.getRequestURI();
if (DefaultConstant.DEFAULT_WELCOME_URL.equals(url)){
return;
}
Long causeTime = System.currentTimeMillis() - startTime.get();
LOGGER.info("[http]postHandle url: {"+request.getRequestURL()+"}, cause\t"+causeTime+"\tms");
ThreadTokenHelper.clear();
}
private String toJSONString(Object object){
try {
if (object == null){
return "";
}
return JSON.toJSONString(object);
}catch (Exception e){
LOGGER.warn("request請求引數序列化{}異常......");
e.printStackTrace();
}
return null;
}
}
在Spring配置檔案中的配置
<mvc:interceptor>
<mvc:mapping path="/**/*" />
<bean class="com.ttyc.ebike.op.backend.interceptor.RequestLoggerHandler" />
</mvc:interceptor>
3.4 日誌示例
下圖紅框內,就是一次請求唯一token資訊,可以看到,兩個token是相同的。通過grep 'token' log就可以把這次請求的所有日誌都查詢出來,可以追蹤程式的執行,方便排查問題。
4.注意
因為每一個請求都是繫結一個執行緒,並生成唯一token與此執行緒繫結,通過spring inteceptor在請求後清除此請求的token資訊。如果發生異常時,會導致inteceptor不能在請求後清除token資訊,會導致請求的token資訊重複。這個時候需要在異常攔截的地方,手動清除下儲存線上程token資訊。
4.1 異常情況
因為每一個請求都是繫結一個執行緒,並生成唯一token與此執行緒繫結,通過spring inteceptor在請求後清除此請求的token資訊。如果發生異常時,會導致inteceptor不能在請求後清除token資訊,會導致請求的token資訊重複。這個時候需要在異常攔截的地方,手動清除下儲存線上程token資訊。
4.2 多執行緒處理
有這樣的場景,一個API在處理過程中,需要呼叫非同步方法傳送訊息,在呼叫非同步方法時就會有第二條執行緒來處理,這個時候,一個請求就會有兩個token資訊。通過查詢一個token,只能得到部分日誌。
4.3 單執行緒處理多工
在使用訊息佇列的業務裡,消費者可用使用單執行緒來處理多條訊息,每一條訊息是一個任務,應該在每一條訊息處理後,手動清除下執行緒中的token資訊,這樣在處理下一條訊息時,日誌資訊中會是新的token,從而不會與上一條訊息的處理日誌產生混淆。
相關文章
- Spring Cloud Gateway 之 請求應答日誌列印SpringCloudGateway
- 為 fastapi 新增全域性唯一請求id,用於日誌跟蹤ASTAPI
- 如何快速過濾出一次請求的所有日誌?
- 給日誌打標籤,讓你的日誌定位再無困難
- Spring Boot日誌框架實踐Spring Boot框架
- flask 框架日誌Flask框架
- iris 系列文章 封裝 請求日誌封裝
- SpringBoot記錄HTTP請求日誌Spring BootHTTP
- 【Spring】日誌列印sql,日誌配置列印sqlSpringSQL
- Go Web輕量級框架Gin學習系列:HTTP請求日誌GoWeb框架HTTP
- Spring Boot 2.x 教程系列 | AOP 切面統一列印請求日誌Spring Boot
- [Java/日誌] 日誌框架列印應用程式日誌程式碼的執行情況Java框架
- 日誌框架學習框架
- Apache 記錄請求響應時間日誌Apache
- Scrapy的日誌等級和請求傳參
- 你還用ES存請求日誌?ClickHouse+Vector打造最強Grafana日誌分析看板Grafana
- Spring Boot--日誌框架的學習Spring Boot框架
- 萬字長文帶你瞭解Java日誌框架使用Java日誌框架Java框架
- mysql之 日誌體系(錯誤日誌、查詢日誌、二進位制日誌、事務日誌、中繼日誌)MySql中繼
- mongodb profiling慢請求監控日誌MongoDB
- Spring Boot 揭祕與實戰(三) 日誌框架篇 – 如何快速整合日誌系統Spring Boot框架
- Spring Boot 揭祕與實戰(三) 日誌框架篇 - 如何快速整合日誌系統Spring Boot框架
- SpringBoot 日誌框架Spring Boot框架
- 日誌採集框架Flume框架
- Spring Boot日誌使用Spring Boot
- Spring Boot日誌配置Spring Boot
- Spring Boot與日誌Spring Boot
- Java的標準日誌Java
- php日誌,記錄日誌PHP
- 日誌分析-apache日誌分析Apache
- java 日誌脫敏框架 sensitive,優雅的列印脫敏日誌Java框架
- 自定義Egg.js的請求級別日誌JS
- 聯機重做日誌、歸檔日誌、備用重做日誌
- mysql 日誌之錯誤日誌MySql
- mysql 日誌之慢查詢日誌MySql
- 【Oracle日誌】- 日誌檔案重建Oracle
- 解讀ASP.NET 5 & MVC6系列(9):日誌框架ASP.NETMVC框架
- 日誌框架SLF4J框架