寫在開頭
本文討論如何優雅的記錄操作日志,並且實現了一個SpringBoot Starter(取名log-record-starter),方便的使用注解記錄操作日志,並將日志數據推送到指定數據管道(消息隊列等)
本文靈感來源於美團技術團隊的文章:如何優雅地記錄操作日志?。文中使用的部分定義描述和示例來源於美團原文,請知悉。
本文作為《萌新寫開源》的開篇,先把項目成品介紹給大家,之后的文章會詳細介紹,如何一步步將個人項目做成一個大家都能參與的開源項目(如何寫SpringBoot Starter,如何上傳到Maven倉庫,如何設計和使用注解和切面等),麻煩大家多多點贊支持,這是我更新的動力。請大家放心,公眾號還會持續更新,我沒有忘掉密碼。:)——蠻三刀醬
本文目錄:
- 什么是操作日志?
- Java中常見的操作日志實現方式
- 實戰:通過注解實現操作日志的記錄
什么是操作日志?
定義:操作日志主要是指對某個對象進行新增操作或者修改操作后記錄下這個新增或者修改,操作日志要求可讀性比較強,因為它主要是給用戶看的,比如訂單的物流信息,用戶需要知道在什么時間發生了什么事情。再比如,客服對工單的處理記錄信息。
以我們系統內部使用的一個CRM系統舉例,里面每個聯系人的資料都會有操作歷史:
這些數據就是操作系統日志,這些數據通常會以結構化數據的形式存儲在數據庫中,對於開發來說,這種日志的代碼邏輯通常是非常規律,比如讀取變化前和變化后的數據,獲取當前操作人和操作時間等等。
常見的操作日志實現方式
在小型項目中,這種日志記錄的操作通常會以提供一個接口或整個日志記錄Service來實現。那么放到多人共同開發的項目中,除了封裝一個方法,還有什么更好的辦法來統一實現操作日志的記錄?下面就要討論下在Java中,常見的操作日志實現方式。
當你需要給一個大型系統從頭到尾加上操作日志,那么除了上述的手動處理方式,也有很多種整體設計方案:
1. 使用Canal監聽數據庫記錄操作日志
Canal應運而生,它通過偽裝成數據庫的從庫,讀取主庫發來的binlog,用來實現數據庫增量訂閱和消費業務需求。可以看我的這篇文章:
這個方式有點是和業務邏輯完全分離,缺點也很大,需要使用到MySQL的Binlog,向DBA申請就有點困難。如果涉及到修改第三方接口,那么就無法監聽別人的數據庫了。所以調用RPC接口時,就需要額外的在業務代碼中增加記錄代碼,破壞了“和業務邏輯完全分離”這個基本原則,局限性大。
2. 通過日志文件的方式記錄
log.info("訂單已經創建,訂單編號:{}", orderNo)
log.info("修改了訂單的配送地址:從“{}”修改到“{}”, "金燦燦小區", "銀盞盞小區")
這種方式,需要手動的設定好操作日志和其他日志的區別,比如給操作日志單獨的Logger。並且,對於操作人的記錄,需要在函數中額外的寫入請求的上下文中。后期這種日志還需要在SLS等日志系統中做額外的抽取。
3. 通過 LogUtil 的方式記錄日志
LogUtil.log(orderNo, "訂單創建", "小明")
LogUtil.log(orderNo, "訂單創建,訂單號"+"NO.11089999", "小明")
String template = "用戶%s修改了訂單的配送地址:從“%s”修改到“%s”"
LogUtil.log(orderNo, String.format(tempalte, "小明", "金燦燦小區", "銀盞盞小區"), "小明")
這種方式會導致業務的邏輯比較繁雜,最后導致 LogUtils.logRecord() 方法的調用存在於很多業務的代碼中,而且類似 getLogContent() 這樣的方法也散落在各個業務類中,對於代碼的可讀性和可維護性來說是一個災難。
4. 方法注解實現操作日志
@OperationLog(bizType = "bizType", bizId = "#request.orderId", pipeline = DataPipelineEnum.QUEUE)
public Response<BaseResult> function(Request request) {
// 方法執行邏輯
}
我們可以在注解的操作日志上記錄固定文案,這樣業務邏輯和業務代碼可以做到解耦,讓我們的業務代碼變得純凈起來。
美團的原文給出了注解記錄日志的詳細架構設計方案,並且貼出了部分源碼。但是文中並沒有完整的開源項目,由於自己也很感興趣,並且公司的業務正好也有類似需求,所以我花了點時間,實現了一版最簡易的版本,支持將操作日志傳遞到消息隊列中。
實戰:通過注解實現操作日志的記錄
大樓不是一天建成的,美團博客中描述的方案應該在公司內部已經非常成熟了,我也沒有那么多精力一口氣吃成一個胖子,我們從最基礎的版本寫起。
我給自己的這個項目,或者說依賴包起名為log-record-starter,一方面遵循springboot-starter命名規范,一方面也表明項目的用處,記錄日志。
開啟項目之前,先問問自己
Q:你這個依賴包,又是一個冗余的造輪子吧?市面上這種東西是不是已經夠多了?
A:本着有現成輪子絕不造輪子的原則,我在Github和其他網站進行了一系列的相關搜索,Github有幾個類似的實現項目,不過都以個人實現為主,沒有一個具有一定影響力的成熟項目。基於我在自己的業務項目中擁有實際的場景需求,並且目前還沒有滿足我需求的現成可接入依賴,我才開始這個依賴包的代碼編寫。
Q:我用了你這個依賴包,是不是很復雜?之后你不維護了的話,是不是坑我們這些吃螃蟹的?
A:依賴包的維護問題一直是一個大問題,本着最小依賴,盡量可擴展的原則。本庫特點如下:
- 使用SpringBoot Starter,接入只需要簡單引入一個依賴。
- 通過Spring Spel表達式拿到參數,對你的業務邏輯沒有侵入性。
- 默認使用RabbitMq傳遞日志消息,日志操作解耦。
- 之后會引入其他數據源,例如Kafka等(畢竟還要給三歪的項目用)。
好了,這就是我想說在前面的話。下面就是該項目的使用介紹和應用場景介紹。
Log-record-starter 一句話介紹
本項目支持用戶使用注解的方式從方法中獲取操作日志,並推送到指定數據源
只需要簡單的加上一個@OperationLog便可以將方法的參數,返回結果甚至是異常堆棧通過消息隊列發送出去,統一處理。
@OperationLog(bizType = "bizType", bizId = "#request.orderId", pipeline = DataPipelineEnum.QUEUE)
public Response<BaseResult> function(Request request) {
// 方法執行邏輯
}
使用方法
只需要簡單的三步:
第一步: SpringBoot項目中引入依賴
<dependency>
<groupId>cn.monitor4all</groupId>
<artifactId>log-record-starter</artifactId>
<version>1.0.0</version>
</dependency>
這里先打斷一下,由於Maven公共倉庫,是全球唯一托管的,個人開發的項目要提交上去,需要復雜的審核流程,我搞了一會沒搞定,就先將包傳到了Github Package上(實際就是Github的私有Maven庫),所以大家引入依賴后,是不會直接拉到包的,需要配置下你的Maven settings.xml文件。(之后我肯定想辦法發到公共倉庫,嗚嗚嗚~)
配置很簡單,兩步,一步是去Github登錄,到自己的Settings中,申請一個token,拿到一串字符串。
第二步,找到你的settings.xml文件,添加上:
activeProfiles>
<activeProfile>github</activeProfile>
</activeProfiles>
<profiles>
<profile>
<id>github</id>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2</url>
</repository>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/OWNER/REPOSITORY</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<servers>
<server>
<id>github</id>
<username>這里填寫你的Github用戶名</username>
<password>這里填寫你剛才申請的token</password>
</server>
</servers>
還搞不定的同學,這里是Github官方中文教程:
重啟下你的IDEA,能看到下面這個,應該你的settings.xml生效了。
目前我的版本號是1.0.0,之后會更新,未來最新版本號在我倉庫查詢:
https://github.com/qqxx6661/logRecord
第二步: 在Spring配置文件中添加RabbitMq數據源配置
在自己公司里由於阿里封裝了自己的MQ叫做MetaQ,並沒有對外開源,所以這里先接入了RabbitMQ,也算是比較通用,圖個方便。未來會接其他數據源。RabbitMq的安裝在這里不展開了,實在是不想把篇幅拉得太大,大家可以自行谷歌下,比如“Docker安裝RabbitMq”類似的文章,幾分鍾就可以設置安裝好。
log-record.rabbitmq.host=localhost
log-record.rabbitmq.port=5672
log-record.rabbitmq.username=admin
log-record.rabbitmq.password=xxxxxxxx
log-record.rabbitmq.queue-name=logrecord
log-record.rabbitmq.routing-key=
log-record.rabbitmq.exchange-name=logrecord
第三步: 在你自己的項目中,在需要記錄日志的方法上,添加注解。
@OperationLog(bizType = "bizType", bizId = "#request.orderId", pipeline = DataPipelineEnum.QUEUE)
public Response<BaseResult> function(Request request) {
// 方法執行邏輯
}
- (必填)bizType:業務類型
- (必填)bizId:唯一業務ID(支持SpEL表達式)
- (必填)pipeline:數據管道,目前只有QUEUE一個數據管道,后續可考慮接入更多數據源
- (非必填)msg:需要傳遞的其他數據(支持SpEL表達式)
- (非必填)tag:自定義標簽
代碼工作原理
由於采用的是SpringBoot Starter方式,所以只要你是用的是SpringBoot,會自動掃描到依賴包中的類,並自動通過Spring進行配置和管理。
該注解通過在切面中解析SpEL參數(啥事SpEL?快去谷歌下,之后要講),將數據發往數據源。目前僅支持RabbitMq,發送的消息體如下:
方法處理正常發送消息體:
[LogDTO(logId=3771ff1e-e5ff-4251-a534-31dab5b666b3, bizId=str, bizType=testType1, exception=null, operateDate=Sat Nov 06 20:08:54 CST 2021, success=true, msg={"testList":["1","2","3"],"testStr":"str"}, tag=operation)]
方法處理異常發送消息體:
[LogDTO(logId=d162b2db-2346-4144-8cd4-aea900e4682b, bizId=str, bizType=testType1, exception=testError, operateDate=Sat Nov 06 20:09:24 CST 2021, success=false, msg={"testList":["1","2","3"],"testStr":"str"}, tag=operation)]
LogDTO是定義的消息結構:
logId:生成的UUID
bizId:注解中傳遞的bizId
bizType:注解中傳遞的bizType
exception:若方法執行失敗,寫入執行的異常信息
operateDate:操作執行的當前時間
success:方式是否執行成功
msg:注解中傳遞的tag
tag:注解中傳遞的tag
我還加上了重復注解的支持,可以在一個方法上同時加多個@OperationLog,下圖是最終使用效果,可以看到,有幾個@OperationLog,就能同時發送多條日志:
項目具體的實現原理和細節,放在下一篇文章詳細講。(肯定會填坑)
應用場景
以下羅列了一些實際的應用場景,包括我業務中實際使用,並且已經上線使用的場景。
一、特定操作記錄日志:如文章最上面一張CRM系統的圖描述的那樣,在用戶進行了編輯操作后,拿到用戶操作的數據,執行日志寫入。
二、特定操作觸發通知:由於我的業務是接手了好幾個倉庫,並且這幾個倉庫的操作串成了一條完成鏈路,我需要在鏈路的某個節點觸發給用戶的提醒,如果寫硬編碼也可以實現,但是遠不如在方法上使用注解發送消息來得方便。例如下方在下單方法調用后發送消息。
三、特定操作更新數據表:我的業務中,幾個系統互相吞吐數據,訂單的一部分數據存留在外部系統里,我們最終目標想要將其中一個系統替代掉,所以需要攔截他們的數據,恰好幾個系統是使用LINK作為網關的,我們將數據請求攔截一層,並將攔截的方法使用該二方庫進行全部參數的發送,將數據同步寫入我們自己的數據庫中,實現”雙寫“。
四、跨多應用數據聚合操作:和”三“類似,在多個應用中,如果需要做行為相同的業務邏輯,完全可以在各個系統中將數據發送到同一個消息隊列中,再進行統一處理。
附錄:Demo
最后,肯定有小伙伴希望有一個完整的使用Demo,這就奉上!
完整Demo項目:
https://github.com/qqxx6661/systemLog
log-record-starter:
https://github.com/qqxx6661/logRecord
總結
本文帶大家了解了操作日志在Java中的幾種實現方式,並且初步介紹了自己的實現代碼,在之后的文章里,我會把實現的細節,包括如何部署到Maven倉庫等一一和大家嘮嘮,記得留下你的點贊和收藏~
我是目前在阿里搬磚的工程師蠻三刀醬。
公眾號:后端技術漫談
持續的創作離不開你的點贊和轉發分享!