完善系統(tǒng)的最后一公里,增加系統(tǒng)日志功能

當(dāng)我們在開發(fā)一個系統(tǒng)的時候,隨著規(guī)劃的功能越來越多浅悉,按照復(fù)雜度和穩(wěn)定性相反的原則趟据,為了保證系統(tǒng)能夠按照我們設(shè)想的目標運行,我們需要對系統(tǒng)的運行狀況進行監(jiān)控术健。

那么什么時候介入監(jiān)控比較好汹碱?在系統(tǒng)功能開發(fā)的前期(還沒有任何實質(zhì)性的功能),似乎不太合適荞估,那么在系統(tǒng)一切功能開發(fā)接近尾聲的時候好像也不太合適咳促,最好在這中間,選擇一個迭代不是很緊急的階段勘伺,系統(tǒng)已經(jīng)有那么一個成熟的功能在用的時候跪腹,并且隨著用戶量的不斷增大,我們需要對系統(tǒng)的運營情況進行一些了解的時候飞醉。

前期我們對系統(tǒng)日志進行設(shè)計的時候尺迂,可以不必考慮的那么周全,就一些必要的信息進行收集冒掌。日志大概分為兩種:1. 操作日志噪裕;2. 異常日志

操作日志用來監(jiān)控用戶在使用系統(tǒng)時候的一些行為,比如請求了什么接口可以推斷出他在系統(tǒng)前臺進行了什么操作股毫;異常日志則是用戶在請求某個接口的時候膳音,接口內(nèi)部出現(xiàn)了程序上的錯誤,這個日志主要提供給程序員進行問題追蹤的铃诬。隨后我們還可以從這些日志中分析出很多有需要的數(shù)據(jù)祭陷,包括系統(tǒng)的健康度,功能的使用頻率和頻次等趣席。

1. 日志表設(shè)計

梳理出一些必要的字段后兵志,我們可以設(shè)計出如下的兩張不同功能的日志表,它們有些字段是相同的宣肚,都需要記錄誰在請求某個接口想罕、請求的Uri、請求人的訪問IP等信息霉涨。

CREATE TABLE `sys_log_operation`  (
 `id` INT NOT NULL AUTO_INCREMENT COMMENT '自動編號',
 `opera_module` VARCHAR(64) DEFAULT NULL COMMENT '功能模塊',
 `opera_type` VARCHAR(64) DEFAULT NULL COMMENT '操作類型',
 `opera_desc` VARCHAR(500) DEFAULT NULL COMMENT '操作描述',
 `opera_req_param` TEXT DEFAULT NULL COMMENT '請求參數(shù)',
 `opera_resp_param` TEXT DEFAULT NULL COMMENT '返回參數(shù)',
 `opera_employee_account` VARCHAR(11) DEFAULT NULL COMMENT '操作人賬號',
 `opera_method` VARCHAR(255) DEFAULT NULL COMMENT '操作方法',
 `opera_uri` VARCHAR(255) DEFAULT NULL COMMENT '請求URI',
 `opera_ip` VARCHAR(64) DEFAULT NULL COMMENT '請求IP',
 `created_time` DATETIME DEFAULT NULL COMMENT '創(chuàng)建時間',
 `modified_time` DATETIME DEFAULT NULL COMMENT '修改時間',
 PRIMARY KEY (`id`) USING BTREE
) COMMENT = '操作日志表' ROW_FORMAT = COMPACT;

CREATE TABLE `sys_log_exception`  (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT '自動編號',
  `exc_req_param` TEXT DEFAULT NULL COMMENT '請求參數(shù)',
  `exc_name` VARCHAR(255) DEFAULT NULL COMMENT '異常名稱',
  `exc_message` TEXT DEFAULT NULL COMMENT '異常信息',
  `opera_employee_account` VARCHAR(11) DEFAULT NULL COMMENT '操作人賬號',
  `opera_method` VARCHAR(255) DEFAULT NULL COMMENT '操作方法',
  `opera_uri` VARCHAR(255) DEFAULT NULL COMMENT '請求URI',
  `opera_ip` VARCHAR(64) DEFAULT NULL COMMENT '請求IP',
  `created_time` DATETIME DEFAULT NULL COMMENT '創(chuàng)建時間',
  `modified_time` DATETIME DEFAULT NULL COMMENT '修改時間',
  PRIMARY KEY (`id`) USING BTREE
) COMMENT = '異常日志表' ROW_FORMAT = COMPACT;

2. 后臺系統(tǒng)日志收集

到了后端編碼的階段按价,我們來構(gòu)思下這個代碼架構(gòu)如何去實現(xiàn)?因為日志代碼是需要穿插在業(yè)務(wù)代碼中的笙瑟,這樣必然帶來一個問題楼镐,導(dǎo)致代碼過于混亂的問題。有沒有一種途徑往枷,通過 Spring 里注解的方式框产,只需要在接口的入口增加攜帶參數(shù)的注解凄杯,然后在注解的代碼里就實現(xiàn)具體的日志收集功能。

Spring 里的特性 AOP(面向切面)就很適合用來實現(xiàn)日志記錄秉宿,性能統(tǒng)計戒突,安全控制,事務(wù)處理蘸鲸,異常處理等功能妖谴,將代碼從業(yè)務(wù)邏輯代碼中劃分出來。

我們在項目代碼的 Utils 目錄創(chuàng)建注解接口 OperLog

package com.lead.utils;

import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Documented;
import java.lang.annotation.Target;

/**
 * 自定義操作日志注解
 * @author Fan
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog {
    String operModule() default ""; // 操作模塊
    String operType() default ""; // 操作類型
    String operDesc() default ""; // 操作說明
}

具體的功能酌摇,我們創(chuàng)建一個 OperLogAspect 切面處理類來實現(xiàn)膝舅。這里面通過 @Pointcut 注解定義了日志的切入點和執(zhí)行范圍

package com.lead.utils;

import com.alibaba.fastjson.JSON;
import com.lead.entity.System.ExceptionLog;
import com.lead.entity.System.OperationLog;
import com.lead.service.System.IExceptionLogService;
import com.lead.service.System.IOperationLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 切面處理類,操作日志異常日志記錄處理
 * @author Fan
 */

@Aspect
@Component
public class OperLogAspect {

    private final IOperationLogService operationLogService;

    private final IExceptionLogService exceptionLogService;

    public OperLogAspect(
            IOperationLogService operationLogService,
            IExceptionLogService exceptionLogService) {
        Assert.notNull(operationLogService, "operationLogService must not be null!");
        Assert.notNull(exceptionLogService, "exceptionLogService must not be null!");
        this.operationLogService = operationLogService;
        this.exceptionLogService = exceptionLogService;
    }

    /**
     * 設(shè)置操作日志切入點 記錄操作日志 在注解的位置切入代碼
     */
    @Pointcut("@annotation(com.lead.utils.OperLog)")
    public void operLogPoinCut() {

    }

    /**
     * 設(shè)置操作異常切入點記錄異常日志 掃描所有controller包下操作
     */
    @Pointcut("execution(* com.lead.controller..*.*(..))")
    public void exceptionLogPoinCut() {

    }

    /**
     * 正常返回通知窑多,攔截用戶操作日志仍稀,連接點正常執(zhí)行完成后執(zhí)行, 如果連接點拋出異常埂息,則不會執(zhí)行
     * @param joinPoint 切入點
     * @param keys 返回結(jié)果
     */
    @AfterReturning(value = "operLogPoinCut()", returning = "keys")
    public void saveOperLog(JoinPoint joinPoint, Object keys) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        OperationLog operlog = new OperationLog();
        String token = request.getHeader("accessToken");
        String employeeAccount = "";

        Object[] args = joinPoint.getArgs();
        Object params = args[0]; // 請求參數(shù)對象
        if (token != null && !token.equals("")) {
            employeeAccount = JwtUtil.getUserId(token);
        } else {
            Map argMap = (Map) params;
            String resp = JSON.toJSONString(keys);
            if (argMap.get("account") != null && argMap.get("passWord") != null && resp.indexOf("用戶登錄成功") > -1) {
                employeeAccount = argMap.get("account").toString();
            }
        }

        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();

            OperLog opLog = method.getAnnotation(OperLog.class);
            if (opLog != null) {
                String operaModule = opLog.operModule();
                String operaType = opLog.operType();
                String operaDesc = opLog.operDesc();
                operlog.setOperaModule(operaModule);
                operlog.setOperaType(operaType);
                operlog.setOperaDesc(operaDesc);
            }
            // 獲取請求的類名
            String className = joinPoint.getTarget().getClass().getName();
            // 獲取請求的方法名
            String methodName = method.getName();
            methodName = className + '.' + methodName;
            operlog.setOperaMethod(methodName);

            // Map<String, String> rtnMap = converMap(request.getParameterMap());
            // 將參數(shù)所在的數(shù)組轉(zhuǎn)換成json
            operlog.setOperaReqParam(JSON.toJSONString(params));
            operlog.setOperaRespParam(JSON.toJSONString(keys));
            operlog.setOperaEmployeeAccount(employeeAccount);
            operlog.setOperaUri(request.getRequestURI());
            operlog.setOperaIp(IPUtil.getIpAddress(request));

            operationLogService.addOperationLog(operlog);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 異常返回通知技潘,用于攔截異常日志信息 連接點拋出異常后執(zhí)行
     */
    @AfterThrowing(pointcut = "exceptionLogPoinCut()", throwing = "e")
    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        ExceptionLog exceptLog = new ExceptionLog();
        String token = request.getHeader("accessToken");
        String employeeAccount = "";

        Object[] args = joinPoint.getArgs();
        Object params = args[0]; // 請求參數(shù)對象
        if (token != null && !token.equals("")) {
            employeeAccount = JwtUtil.getUserId(token);
        } else {
            employeeAccount = request.getParameter("account");
        }

        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();

            // 獲取請求的類名
            String className = joinPoint.getTarget().getClass().getName();
            // 獲取請求的方法名
            String methodName = method.getName();
            methodName = className + '.' + methodName;

            exceptLog.setExcReqParam(JSON.toJSONString(params)); // 請求參數(shù)
            exceptLog.setOperaMethod(methodName); // 請求方法名
            exceptLog.setExcName(e.getClass().getName()); // 異常名稱
            exceptLog.setExcMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())); // 異常信息
            exceptLog.setOperaEmployeeAccount(employeeAccount);
            exceptLog.setOperaUri(request.getRequestURI());
            exceptLog.setOperaIp(IPUtil.getIpAddress(request));

            exceptionLogService.addExceptionLog(exceptLog);

        } catch (Exception e2) {
            e2.printStackTrace();
        }

    }

    /**
     * 轉(zhuǎn)換request 請求參數(shù)
     * @param paramMap request獲取的參數(shù)數(shù)組
     */
    public Map<String, String> converMap(Map<String, String[]> paramMap) {
        Map<String, String> rtnMap = new HashMap<String, String>();
        for (String key : paramMap.keySet()) {
            rtnMap.put(key, paramMap.get(key)[0]);
        }
        return rtnMap;
    }

    /**
     * 轉(zhuǎn)換異常信息為字符串
     * @param exceptionName    異常名稱
     * @param exceptionMessage 異常信息
     * @param elements         堆棧信息
     */
    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
        StringBuffer strbuff = new StringBuffer();
        for (StackTraceElement stet : elements) {
            strbuff.append(stet + "\n");
        }
        String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
        return message;
    }
}

具體的日志數(shù)據(jù)處理分別封裝在 saveOperLogsaveExceptionLog 方法中,這里主要說下他們共同的一些字段數(shù)據(jù)的獲取千康。用戶的賬號是通過請求頭參數(shù) accessToken 獲取的 token 信息然后解析出來的享幽,這里為了獲取未登錄情況也就是請求登錄接口的時候,直接獲取登錄提交的用戶賬號

RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
OperationLog operlog = new OperationLog();
String token = request.getHeader("accessToken");
String employeeAccount = "";

if (token != null && !token.equals("")) {
    employeeAccount = JwtUtil.getUserId(token);
} else {
    Map argMap = (Map) params;
    String resp = JSON.toJSONString(keys);
    if (argMap.get("account") != null && argMap.get("passWord") != null && resp.indexOf("用戶登錄成功") > -1) {
        employeeAccount = argMap.get("account").toString();
    }
}

獲取接口類名和方法名

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 獲取請求的類名
String className = joinPoint.getTarget().getClass().getName();
// 獲取請求的方法名
String methodName = method.getName();
methodName = className + '.' + methodName;
operlog.setOperaMethod(methodName);

這里直接獲取的注解攜帶的參數(shù)拾弃,也就是我們在接口端添加的代碼

OperLog opLog = method.getAnnotation(OperLog.class);
if (opLog != null) {
    String operaModule = opLog.operModule();
    String operaType = opLog.operType();
    String operaDesc = opLog.operDesc();
    operlog.setOperaModule(operaModule);
    operlog.setOperaType(operaType);
    operlog.setOperaDesc(operaDesc);
}

在業(yè)務(wù)代碼中添加日志注解值桩,給每個參數(shù)賦予帶有一定含義的值:

@OperLog(operModule = "培訓(xùn)模塊", operType = "用戶獲取培訓(xùn)課程列表", operDesc = "用戶獲取培訓(xùn)課程列表")
@RequestMapping(value = "/get-training", method = RequestMethod.POST)
public Result getTrainings(@RequestBody Map<String, Object> params, HttpServletRequest request) {}

3. 前臺日志查詢

前臺拿到日志接口請求過來的數(shù)據(jù),我們用一張表格展示就好豪椿,在該接口中提供了操作人奔坟、操作模塊、操作類型和起始時間等查詢參數(shù)搭盾,前臺可以通過輸入?yún)?shù)值進行過濾咳秉。

為了展示的信息更全,我們在表格中增加展開行的功能鸯隅,點擊表格中的行任何位置澜建,出現(xiàn)該條日志的詳細信息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滋迈,一起剝皮案震驚了整個濱河市霎奢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饼灿,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帝美,死亡現(xiàn)場離奇詭異碍彭,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門庇忌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舞箍,“玉大人,你說我怎么就攤上這事皆疹∈栝希” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵略就,是天一觀的道長捎迫。 經(jīng)常有香客問我,道長表牢,這世上最難降的妖魔是什么窄绒? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮崔兴,結(jié)果婚禮上彰导,老公的妹妹穿的比我還像新娘。我一直安慰自己敲茄,他們只是感情好位谋,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著堰燎,像睡著了一般掏父。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上爽待,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天损同,我揣著相機與錄音,去河邊找鬼鸟款。 笑死膏燃,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的何什。 我是一名探鬼主播组哩,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼处渣!你這毒婦竟也來了伶贰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤罐栈,失蹤者是張志新(化名)和其女友劉穎黍衙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荠诬,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡琅翻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年位仁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片方椎。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡聂抢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出棠众,到底是詐尸還是另有隱情琳疏,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布闸拿,位于F島的核電站空盼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏胸墙。R本人自食惡果不足惜我注,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望迟隅。 院中可真熱鬧但骨,春花似錦、人聲如沸智袭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吼野。三九已至校哎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瞳步,已是汗流浹背闷哆。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留单起,地道東北人抱怔。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像嘀倒,于是被迫代替她去往敵國和親屈留。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容