客戶端日志方案

背景說明

??對于發(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í)程序crashexception信息日志(攜帶異常堆棧)鸯屿,而且還要包括可預(yù)見性的程序運(yùn)行跟預(yù)期不符合的情況婶恼。

日志規(guī)范

  1. 日志的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"; //正確
  1. 應(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()打印睦擂。
  1. 所有日志打印開關(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)前置的形式]学辱。

  1. 異常打印信息使用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); //正確
  }
  1. 代碼中禁止使用System.out.println("message")來打印日志着降。
  2. 對于容易產(chǎn)生不可預(yù)知異常處需打印入口和出口日志拗军,如: http 請求網(wǎng)絡(luò)日志、推送等发侵。
  3. 需要直接打印的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());
  1. for循環(huán)中盡量不打印日志,確實(shí)有必要打印時(shí)步责,可以先在循環(huán)里使用 StringBuilder拼湊成可讀性較好的字符串,然后在循環(huán)結(jié)束后一次性打印寫入文件蔓肯。
  2. 打印的日志信息不僅僅是簡單的輸出變量值遂鹊,諸如“變量名=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));
  1. 盡量不寫入重復(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)了昵仅。

另外缓熟,SystemPropertieskey的命名有一些規(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í)序圖

時(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ǔ)、生成日志下載鏈接谱姓。

客戶端

  • 實(shí)現(xiàn)日志打印文件和終端輸出借尿,包括日志壓縮、文件大小屉来、有效期及存儲(chǔ)空間控制(可接入第三方日志庫實(shí)現(xiàn)路翻,如:xloglog4j 等)
  • 實(shí)現(xiàn)push推送的透傳日志指令(包括日志開關(guān)奶躯、日志上傳等)解析和相應(yīng)執(zhí)行操作帚桩。
  • 實(shí)現(xiàn)日志文件http上傳。
  • http請求接口增加基本參數(shù)(如:版本號(hào)嘹黔、imei账嚎、安卓版本莫瞬、系統(tǒng)版本等),便于后續(xù)出現(xiàn)問題是的過濾分析郭蕉。

參考文檔
xlog: http://dev.qq.com/topic/581c2c46bef1702a2db3ae53

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疼邀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子召锈,更是在濱河造成了極大的恐慌旁振,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涨岁,死亡現(xiàn)場離奇詭異拐袜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)梢薪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門蹬铺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秉撇,你說我怎么就攤上這事甜攀。” “怎么了琐馆?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵规阀,是天一觀的道長。 經(jīng)常有香客問我瘦麸,道長谁撼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任瞎暑,我火速辦了婚禮彤敛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘了赌。我一直安慰自己,他們只是感情好玄糟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布勿她。 她就那樣靜靜地躺著,像睡著了一般阵翎。 火紅的嫁衣襯著肌膚如雪逢并。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天郭卫,我揣著相機(jī)與錄音砍聊,去河邊找鬼。 笑死贰军,一個(gè)胖子當(dāng)著我的面吹牛玻蝌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼俯树,長吁一口氣:“原來是場噩夢啊……” “哼帘腹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起许饿,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤阳欲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后陋率,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體球化,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年瓦糟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赊窥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狸页,死狀恐怖锨能,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芍耘,我是刑警寧澤址遇,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站斋竞,受9級特大地震影響倔约,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜坝初,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一浸剩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鳄袍,春花似錦绢要、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哀九,卻和暖如春剿配,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阅束。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工呼胚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人息裸。 一個(gè)月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓蝇更,卻偏偏與公主長得像沪编,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子簿寂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353