背景說明
??對于發(fā)布出去的app大部分時(shí)候確實(shí)都能保證業(yè)務(wù)正常運(yùn)行的,但由于版本的迭代乖阵,客戶端和服務(wù)端業(yè)務(wù)邏輯一直在更新變化对蒲,而且運(yùn)營數(shù)據(jù)的配置也會(huì)相應(yīng)更改鸣驱,這些變化就可能使得客戶端不能按預(yù)定邏輯運(yùn)行或出現(xiàn)異常。如果一旦有檢測(用戶反饋或服務(wù)端預(yù)警)到有非正常的運(yùn)行現(xiàn)象,卻沒有對應(yīng)用戶的日志來分析,而僅通過現(xiàn)象來排查代碼將使得問題解決變得困難。
??日志作為程序運(yùn)行狀態(tài)和路徑的記錄贺拣,是跟蹤和重現(xiàn)問題的重要依據(jù),特別是對于線上問題的定位顯得尤為重要陨瘩。 因此,規(guī)范的日志打印和合理的日志獲取流程有利于問題修復(fù)的效率提升卡者。
實(shí)現(xiàn)目的
- 增強(qiáng)客戶端app在發(fā)布后對可能問題的可追溯性;
- 方便在開發(fā)過程中對提測后bug的重現(xiàn);
- 減少打印日志后對程序性能的影響;
- 規(guī)范日志打印,增強(qiáng)可讀性;
日志分類
- 業(yè)務(wù)日志:記錄程序運(yùn)行狀態(tài)和路徑脸侥,是日志的主體信息外遇。一般是在代碼的關(guān)鍵點(diǎn)(比如在遇到兩個(gè)分支策略使得程序運(yùn)行結(jié)果有較大出入時(shí))來打印業(yè)務(wù)日志,為后續(xù)排查問題提供程序執(zhí)行路徑依據(jù)山上。
-
異常日志:不僅包括標(biāo)識(shí)程序
crash
的exception
信息日志(攜帶異常堆棧)鸯屿,而且還要包括可預(yù)見性的程序運(yùn)行跟預(yù)期不符合的情況婶恼。
日志規(guī)范
- 日志的TAG定義
在類的首行使用final static String
類型定義日志TAG
,名稱可以是類名或其他有意義能唯一標(biāo)示的名稱。如:
//錯(cuò)誤(混淆會(huì)修改類名)
private final static String TAG = TestLog.class.getSimpleName();
private final static String TAG = "aa"; //錯(cuò)誤
private final static String TAG = "TestLog"; //正確
- 應(yīng)嚴(yán)格按照日志等級打印,以便提高性能以及后續(xù)日志抓取割择、分析的過濾萎河。
日志級別從低到高定義:Log.v()
<Log.d()
<Log.i()
<Log.w()
<Log.e()
。
- A玛歌、程序調(diào)試階段的調(diào)試日志(最好合并代碼到 dev 之前清理)以及不需要在正式發(fā)版后打印到文件(終端)的使用
Log.v()
或Log.d()
打印,便于后續(xù)視情況終端打印還是寫入日志文件或者直接屏蔽擎椰。 - B达舒、關(guān)鍵業(yè)務(wù)執(zhí)行路徑日志、重要日志信息使用
Log.i()
打印,此時(shí)對于遠(yuǎn)程抓取的情況需要存儲(chǔ)文件昨登。 - C塔猾、有異常判斷的條件,但此時(shí)跟預(yù)期運(yùn)行有差異的情況使用
Log.w()
(比如對空指針的有前提判斷,但預(yù)期不為空的情況)。 - D糯俗、catch異常使用
Log.e()
打印睦擂。
- 所有日志打印開關(guān)需要前置。如:
if (Log.LOGED) Log.d(TAG, "message"); //優(yōu)化字符串拼接損耗
??這里的開關(guān)前置淘正,很多人不能理解臼闻,其實(shí)就是因?yàn)檎{(diào)用Log.d()
函數(shù)的傳參message
很有可能是通過復(fù)雜運(yùn)算拼接成的(比如述呐,打印http接口的返回結(jié)果等),在動(dòng)態(tài)的關(guān)閉/打開日志開關(guān)后思犁,能較大程度的提升性能。[參考android的源碼中棉磨,發(fā)現(xiàn)很多日志打印也是采用了這種開關(guān)前置的形式]学辱。
- 異常打印信息使用
Log.e()
打印,且需要帶上Throwable
異常堆棧信息馅扣。如:
try {
//do something...
} catch (Exception e) {
Log.e(TAG, e.getMessage()); //錯(cuò)誤
Log.e(TAG, "method name()" + e.getMessage()); //錯(cuò)誤
Log.e(TAG, "method name()", e); //正確
}
- 代碼中禁止使用
System.out.println("message")
來打印日志着降。 - 對于容易產(chǎn)生不可預(yù)知異常處需打印入口和出口日志拗军,如: http 請求網(wǎng)絡(luò)日志、推送等发侵。
- 需要直接打印的
bean
對象或復(fù)雜數(shù)據(jù)結(jié)構(gòu)時(shí)刃鳄,需打印其toString()
,并實(shí)現(xiàn)該方法挪鹏,參數(shù)使用StringBuilder
拼湊愉烙。如:
public class TestBean {
private String key;
private int value;
private boolean success;
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{key:")
.append(key)
.append(", value:")
.append(value)
.append(", sucess:")
.append(success)
.append("}");
return builder.toString();
}
}
TestBean bean = new TestBean();
bean.key = "test-key";
bean.value = 100;
bean.success = true;
if (Log.LOGED) Log.d(TAG, "test: " + bean.toString());
-
for
循環(huán)中盡量不打印日志,確實(shí)有必要打印時(shí)步责,可以先在循環(huán)里使用StringBuilder
拼湊成可讀性較好的字符串,然后在循環(huán)結(jié)束后一次性打印寫入文件蔓肯。 - 打印的日志信息不僅僅是簡單的輸出變量值遂鹊,諸如“變量名=value”等格式的信息,應(yīng)攜帶其他有可讀性的語句或提煉通用詞句來拼湊日志信息蔗包,以使最終的日志文件具有可讀性秉扑。比如:
Log.i(TAG, "request >> url: " + task.getUrl() + ", code: " + task.hashCode()
+ ", type: " + task.getRequestType() + ", expire: " + sAcExpired);
Log.i(TAG, "response << http: " + task.getHttpName() + ", code: " + task.hashCode()
+ ", ac:" + ac + ", result:" + formatJson(content));
- 盡量不寫入重復(fù)或多余日志。
日志影響
日志打印存儲(chǔ)文件時(shí)气忠,需要盡可能少的影響程序本身的性能邻储,因?yàn)樾阅苡绊懥顺绦虻牧鲿承愿逞剩鲿承灾苯佑绊懥擞脩趔w驗(yàn)。
最基本的流暢性保證是使用了日志方案后不會(huì)導(dǎo)致app的卡頓吨娜,但是流暢性不僅包括了系統(tǒng)沒有卡頓,還要盡量保證沒有CPU峰值等宦赠。
方案一:簡易本地版
??如果不需要考慮日志的存儲(chǔ)陪毡、上傳等與服務(wù)端連通的操作而只是在終端輸出時(shí),問題也將變得簡單勾扭,我們直接使用android.util.Log
進(jìn)行日志打印即可毡琉。剩下的問題就只剩下如何控制日志開關(guān)、如何打印其他輔助信息(如線程妙色、堆棧等)了桅滋。
??經(jīng)常看到很多app對這種本地日志開關(guān)都會(huì)使用BuildConfig.DEBUG
這個(gè)gradle
幫我們生成的變量來作為日志開關(guān)的標(biāo)志(debug
版本打開日志身辨,release
版本關(guān)閉日志)丐谋,在實(shí)際開發(fā)過程中會(huì)發(fā)現(xiàn)以這個(gè)變量為日志的開關(guān)標(biāo)志其實(shí)有一定局限性。比如煌珊,某天xx領(lǐng)導(dǎo)拿著自己的手機(jī)給我們現(xiàn)場反饋剛上線的版本出現(xiàn)了某個(gè)偶現(xiàn)嚴(yán)重問題号俐,當(dāng)你想看下日志分析的時(shí)候,發(fā)現(xiàn)release版本日志被關(guān)掉了定庵,看不了@舳觥!蔬浙!尷尬啊猪落。。敛滋。
??于是琢磨著有沒有什么其他手段能動(dòng)態(tài)修改這個(gè)日志開關(guān)呢许布?想到前幾年開發(fā)中經(jīng)常用到SystemProperties
來存儲(chǔ)一些全局配置信息,剛好能配合這個(gè)日志開關(guān)的使用绎晃,不管什么版本的app蜜唾,需要看日志的時(shí)候,我們通過命令行修改這個(gè)配置就可以打開這個(gè)開關(guān)了庶艾,如輸入:adb shell setprop ro.tech.log true
袁余。
先簡單介紹一下屬性系統(tǒng):
??屬性系統(tǒng)是android
的一個(gè)重要特性,它作為一個(gè)服務(wù)運(yùn)行咱揍,管理系統(tǒng)配置和狀態(tài)颖榜;所有這些配置和狀態(tài)都是屬性;每個(gè)屬性是一個(gè)鍵值對(key/value pair),其類型都是字符串掩完。這些屬性可能是有些資源的使用狀態(tài)噪漾、進(jìn)程的執(zhí)行狀態(tài)、系統(tǒng)的特有屬性等且蓬。
系統(tǒng)給的注釋:Gives access to the system properties store. The system properties store contains a list of string key-value pairs.
??對開發(fā)者來說更重要的是SystemProperties
被@hide
起來了欣硼,意思是普通應(yīng)用開發(fā)不能使用屬性系統(tǒng)。經(jīng)過測試驗(yàn)證(在root手機(jī)和非root手機(jī)上驗(yàn)證過恶阴,不排除某些rom有限制)诈胜,反射這個(gè)類的set()
/get()
接口仍是有效的,只是谷歌沒有開放給開發(fā)者使用而已冯事。有了這樣的前提焦匈,我們就可以使用命令行設(shè)置屬性打開(關(guān)閉)日志開關(guān),app代碼反射讀取屬性來獲取開關(guān)來實(shí)現(xiàn)日志的動(dòng)態(tài)開關(guān)了昵仅。
另外缓熟,SystemProperties
對key
的命名有一些規(guī)則限制,比較常用的是以ro
開頭和persist
開頭的屬性:
- 如果屬性名稱以
ro.
開頭岩饼,那么這個(gè)屬性被視為只讀屬性荚虚。一旦設(shè)置薛夜,屬性值不能改變籍茧,需要重啟還原。 - 如果屬性名稱以
persist.
開頭梯澜,當(dāng)設(shè)置這個(gè)屬性時(shí)寞冯,其值將寫入/data/property
,且可以重復(fù)寫入晚伙。
首先吮龄,定義反射屬性系統(tǒng)的接口函數(shù)(參見文章《反射相關(guān)知識(shí)及jOOR反射庫介紹》):
private static boolean getBoolean(String propName, boolean def) {
return Reflect.on("android.os.SystemProperties").call("getBoolean", propName, def).get();
}
根據(jù)屬性系統(tǒng)key的命名規(guī)則定義日志開關(guān)key
/**
* 日志開關(guān)設(shè)置
*
* adb shell setprop ro.tech.log true
*/
private static final String LOG_ENABLE_PROP = "ro.tech.log";
定義日志開關(guān)
DLog {
//...
//LOGED為靜態(tài)的final變量,程序啟動(dòng)時(shí)讀取開關(guān)
public final static boolean LOGED = BuildConfig.DEBUG || getBoolean(LOG_ENABLE_PROP, false);
//...
}
打印日志咆疗,讀取DLog.LOGED
屬性(在啟動(dòng)app前漓帚,通過命令行輸入修改屬性指令:adb shell setprop ro.tech.log true
)
if (DLog.LOGED) DLog.i(TAG, "MainActivity is oncreated!!");
其他輔助信息,如是否使用默認(rèn)tag打印午磁、是否打印線程信息等設(shè)置均可采用該方式實(shí)現(xiàn)尝抖,具體參見GHDemo-DLog.java的實(shí)現(xiàn)。
方案二:服務(wù)端預(yù)警版
使用場景
- 服務(wù)端檢測http業(yè)務(wù)接口調(diào)用異常(調(diào)用量偏高或偏低迅皇、接口參數(shù)有誤等)昧辽,獲取客戶端網(wǎng)絡(luò)日志分析http調(diào)用邏輯的可能問題。(典型用例:消息提醒需求的紅點(diǎn)接口調(diào)用量大問題)登颓。
- 線上用戶反饋但難以復(fù)現(xiàn)的嚴(yán)重問題搅荞,通過獲取用戶反饋問題的描述及用戶日志分析解決此類問題。(典型用例:排查用戶反饋偷跑流量問題)
- app灰度期間借助第三方的崩潰統(tǒng)計(jì)平臺(tái)(友盟、bugly等)統(tǒng)計(jì)崩潰情況咕痛,客戶端的業(yè)務(wù)日志能方便重現(xiàn)崩潰的操作路徑有利于找到crash的本質(zhì)原因痢甘。
方案描述
??服務(wù)端借助push系統(tǒng)主動(dòng)發(fā)出日志開關(guān)或日志拉取的透傳指令,客戶端app收到推送后茉贡,解析該日志指令并執(zhí)行相應(yīng)處理(包括操作日志開關(guān)产阱、壓縮并上傳已有日志文件),在服務(wù)端收到日志文件后提供日志文件存儲(chǔ)下載服務(wù)块仆,開發(fā)人員下載對應(yīng)用戶日志分析問題构蹬。
??該方案完全由服務(wù)端的指令觸發(fā)自動(dòng)完成,整個(gè)流程不需要用戶實(shí)際參與悔据,相比要求用戶配合開發(fā)人員獲取日志的方式大大縮短了解決問題的時(shí)間和整體流程庄敛。
方案實(shí)現(xiàn)
該方案實(shí)現(xiàn)有服務(wù)端和客戶端的工作量,因此需要兩端協(xié)助完成科汗,具體時(shí)序流程如下圖所示:
時(shí)序圖中包含了手動(dòng)拉取和自動(dòng)批量拉取兩種場景:
- 手動(dòng)拉取:用戶或預(yù)警反饋問題并攜帶客戶端imei藻烤,開發(fā)人員通過后臺(tái)日志管理平臺(tái)以imei為唯一標(biāo)識(shí)觸發(fā)單條日志指令。
- 自動(dòng)批量拉取:大數(shù)據(jù)生成報(bào)表時(shí)头滔,獲取存在可能異常用戶樣本列表怖亭,通過業(yè)務(wù)接口自動(dòng)獲取樣本列表日志。
具體細(xì)節(jié)功能
服務(wù)端
- 定義日志指令參數(shù)格式坤检。
- 對接push系統(tǒng)兴猩,實(shí)現(xiàn)查詢在線狀態(tài)、日志指令(包括開關(guān)控制早歇、日志拉取倾芝、指令有效期控制)推送接口,且支持批量推送箭跳。
- 實(shí)現(xiàn)日志指令操作的后臺(tái)操作界面晨另。
- 實(shí)現(xiàn)日志文件存儲(chǔ)、生成日志下載鏈接谱姓。
客戶端