分佈式架構之 TLog
一、TLog 是什麼?
TLog 是一個輕量級的分佈式日誌標記追蹤神器。
TLog 官方網站:
https://tlog.yomahub.com/
TLog Github 源代碼:
https://github.com/dromara/TLog
TLog Gitee 源代碼:
https://gitee.com/dromara/TLog
二、TLog 的架構圖是怎樣的?
三、TLog 能解決什麼痛點?
隨着微服務盛行,很多公司都把系統按照業務邊界拆成了很多微服務,在排錯查日誌的時候。因爲業務鏈路貫穿着很多微服務節點,導致定位某個請求的日誌以及上下游業務的日誌會變得有些困難。
這時候很多童鞋會開始考慮上 SkyWalking,Pinpoint 等分佈式追蹤系統來解決,基於 OpenTracing 規範,而且通常都是無侵入性的,並且有相對友好的管理界面來進行鏈路 Span 的查詢。
但是搭建分佈式追蹤系統,熟悉以及推廣到全公司的系統需要一定的時間週期,而且當中涉及到鏈路 span 節點的存儲成本問題,全量採集還是部分採集?如果全量採集,就以 SkyWalking 的存儲來舉例,ES 集羣搭建至少需要 5 個節點。這就需要增加服務器成本。況且如果微服務節點多的話,一天下來產生幾十 G 上百 G 的數據其實非常正常。如果想保存時間長點的話,也需要增加服務器磁盤的成本。
當然分佈式追蹤系統是一個最終的解決方案,如果您的公司已經上了分佈式追蹤系統,那 TLog 並不適用。
注意:
TLog 提供了一種最簡單的方式來解決日誌追蹤問題,它不收集日誌,也不需要另外的存儲空間,它只是自動的對你的日誌進行打標籤,自動生成 TraceId 貫穿你微服務的一整條鏈路。並且提供上下游節點信息。適合中小型企業以及想快速解決日誌追蹤問題的公司項目使用。
四、TLog 目前支持哪些特性?
-
- 通過對日誌打標籤完成輕量級微服務日誌追蹤。
-
- 提供三種接入方式:javaagent 完全無侵入接入,字節碼一行代碼接入,基於配置文件的接入。
-
- 對業務代碼無侵入式設計,使用簡單,10 分鐘即可接入。
-
- 支持常見的 log4j,log4j2,logback 三大日誌框架,並提供自動檢測,完成適配。
-
- 支持 dubbo,dubbox,springcloud 三大 RPC 框架。
-
- 支持 Spring Cloud Gateway 和 Soul 網關。
-
- 適配 HttpClient 和 Okhttp 的 http 調用標籤傳遞。
-
- 支持三種任務框架,JDK 的 TimerTask,Quartz,XXL-JOB。
-
- 支持日誌標籤的自定義模板的配置,提供多個系統級埋點標籤的選擇。
-
- 支持異步線程的追蹤,包括線程池,多級異步線程等場景。
-
- 幾乎無性能損耗,快速穩定,經過壓測,損耗在 0.01%。
五、如何選擇你的接入方式?
六、YC-Framework 是如何支持 TLog 的?
YC-Framework 中的 yc-common-log 模塊用到 TLog,主要用於分佈式微服務場景下的接口日誌存儲(最終存儲在 MongoDB 裏)。
主要核心代碼如下:
@Aspect
@Component
@Slf4j
public class LogAspect {
@Autowired
private OperateLogApi operateLogApi;
// 配置織入點
@Pointcut("@annotation(com.yc.common.log.annotation.Log)")
public void logPointCut() {
log.info("織入");
}
/**
* 處理完請求後執行
*
* @param joinPoint 切點
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleLog(joinPoint, null, jsonResult);
}
/**
* 攔截異常操作
*
* @param joinPoint 切點
* @param e 異常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e, null);
}
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
try {
// 獲得註解
Log controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null) {
return;
}
// *========日誌=========*//
log.info("currIp:" + TLogContext.getCurrIp());
log.info("preIp:" + TLogContext.getPreIp());
log.info("traceId:" + TLogContext.getTraceId());
log.info("spanId:" + TLogContext.getSpanId());
OperateLog operLog = new OperateLog();
operLog.setOperId(IdUtil.simpleUUID());
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 請求的地址
String ip = IpUtil.getIpAddr(ServletUtil.getRequest());
operLog.setOperIp(ip);
// 返回參數
operLog.setJsonResult(JSON.toJSONString(jsonResult));
//請求接口URL
operLog.setOperUrl(ServletUtil.getRequest().getRequestURI());
//當前用戶ID
operLog.setOperName(String.valueOf(StpUtil.getLoginId()));
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtil.substring(e.getMessage(), 0, 2000));
}
// 設置方法名稱
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 設置請求方式
operLog.setRequestMethod(ServletUtil.getRequest().getMethod());
// 處理設置註解上的參數
getControllerMethodDescription(joinPoint, controllerLog, operLog);
// 保存數據庫或MongoDB
log.info("operLog:" + operLog);
ThreadUtil.execAsync(() -> {
//調用日誌存儲API
operateLogApi.add(operLog);
});
} catch (Exception exp) {
// 記錄本地異常日誌
log.error("==前置通知異常==");
log.error("異常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 獲取註解中對方法的描述信息 用於Controller層註解
*
* @param log 日誌
* @param operLog 操作日誌
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperateLog operLog) throws Exception {
// 是否需要保存request,參數和值
if (log.isSaveReqData()) {
operLog.setFunctionName(log.value());
// 獲取參數的信息
setRequestValue(joinPoint, operLog);
}
}
/**
* 獲取請求的參數,放到log中
*
* @param operLog 操作日誌
* @throws Exception 異常
*/
private void setRequestValue(JoinPoint joinPoint, OperateLog operLog) throws Exception {
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperParam(StringUtil.substring(params, 0, 2000));
} else {
Map<?, ?> paramsMap = (Map<?, ?>) ServletUtil.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
operLog.setOperParam(StringUtil.substring(paramsMap.toString(), 0, 2000));
}
}
/**
* 是否存在註解,如果存在就獲取
*/
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(Log.class);
}
return null;
}
/**
* 參數拼裝
*/
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (int i = 0; i < paramsArray.length; i++) {
if (!isFilterObject(paramsArray[i])) {
try {
Object jsonObj = JSON.toJSON(paramsArray[i]);
params += jsonObj.toString() + " ";
} catch (Exception e) {
}
}
}
}
return params.trim();
}
/**
* 判斷是否需要過濾的對象。
*
* @param o 對象信息。
* @return 如果是需要過濾的對象,則返回true;否則返回false。
*/
public boolean isFilterObject(final Object o) {
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;
}
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
該模塊完整代碼可參考:
https://github.com/developers-youcong/yc-framework/tree/main/yc-common/yc-common-log
以上源代碼均已開源,開源不易,如果對你有幫助,不妨給個 star!!!
YC-Framework 官網:
https://framework.youcongtech.com/
YC-Framework Github 源代碼:
https://github.com/developers-youcong/yc-framework
YC-Framework Gitee 源代碼:
https://gitee.com/developers-youcong/yc-framework
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/fwXQYWCy-wpDa9zgvysupQ