分佈式架構之 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 目前支持哪些特性?

五、如何選擇你的接入方式?

六、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