客戶端日志方案

背景說明

??對于發布出去的app大部分時候確實都能保證業務正常運行的,但由于版本的迭代,客戶端和服務端業務邏輯一直在更新變化,而且運營數據的配置也會相應更改,這些變化就可能使得客戶端不能按預定邏輯運行或出現異常。如果一旦有檢測(用戶反饋或服務端預警)到有非正常的運行現象,卻沒有對應用戶的日志來分析,而僅通過現象來排查代碼將使得問題解決變得困難。
??日志作為程序運行狀態和路徑的記錄,是跟蹤和重現問題的重要依據,特別是對于線上問題的定位顯得尤為重要。 因此,規范的日志打印和合理的日志獲取流程有利于問題修復的效率提升。

實現目的

  • 增強客戶端app在發布后對可能問題的可追溯性;
  • 方便在開發過程中對提測后bug的重現;
  • 減少打印日志后對程序性能的影響;
  • 規范日志打印,增強可讀性;

日志分類

  • 業務日志:記錄程序運行狀態路徑,是日志的主體信息。一般是在代碼的關鍵點(比如在遇到兩個分支策略使得程序運行結果有較大出入時)來打印業務日志,為后續排查問題提供程序執行路徑依據。
  • 異常日志:不僅包括標識程序crashexception信息日志(攜帶異常堆棧),而且還要包括可預見性的程序運行跟預期不符合的情況。

日志規范

  1. 日志的TAG定義
    在類的首行使用final static String類型定義日志TAG,名稱可以是類名或其他有意義能唯一標示的名稱。如:
//錯誤(混淆會修改類名)
private final static String TAG = TestLog.class.getSimpleName(); 

private final static String TAG = "aa"; //錯誤

private final static String TAG = "TestLog"; //正確
  1. 應嚴格按照日志等級打印,以便提高性能以及后續日志抓取、分析的過濾。
    日志級別從低到高定義:Log.v() < Log.d() < Log.i() < Log.w() < Log.e()
  • A、程序調試階段的調試日志(最好合并代碼到 dev 之前清理)以及不需要在正式發版后打印到文件(終端)的使用Log.v()Log.d()打印,便于后續視情況終端打印還是寫入日志文件或者直接屏蔽。
  • B、關鍵業務執行路徑日志、重要日志信息使用Log.i()打印,此時對于遠程抓取的情況需要存儲文件。
  • C、有異常判斷的條件,但此時跟預期運行有差異的情況使用Log.w() (比如對空指針的有前提判斷,但預期不為空的情況)。
  • D、catch異常使用Log.e()打印。
  1. 所有日志打印開關需要前置。如:
if (Log.LOGED) Log.d(TAG, "message");  //優化字符串拼接損耗

??這里的開關前置,很多人不能理解,其實就是因為調用Log.d()函數的傳參message很有可能是通過復雜運算拼接成的(比如,打印http接口的返回結果等),在動態的關閉/打開日志開關后,能較大程度的提升性能。[參考android的源碼中,發現很多日志打印也是采用了這種開關前置的形式]。

  1. 異常打印信息使用Log.e()打印,且需要帶上Throwable異常堆棧信息。如:
  try {
     //do something...
  } catch (Exception e) {
      Log.e(TAG, e.getMessage());     //錯誤
      Log.e(TAG, "method name()" + e.getMessage()); //錯誤
      Log.e(TAG, "method name()", e); //正確
  }
  1. 代碼中禁止使用System.out.println("message")來打印日志。
  2. 對于容易產生不可預知異常處需打印入口和出口日志,如: http 請求網絡日志、推送等。
  3. 需要直接打印的bean對象或復雜數據結構時,需打印其toString(),并實現該方法,參數使用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循環中盡量不打印日志,確實有必要打印時,可以先在循環里使用 StringBuilder拼湊成可讀性較好的字符串,然后在循環結束后一次性打印寫入文件。
  2. 打印的日志信息不僅僅是簡單的輸出變量值,諸如“變量名=value”等格式的信息,應攜帶其他有可讀性的語句或提煉通用詞句來拼湊日志信息,以使最終的日志文件具有可讀性。比如:
  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. 盡量不寫入重復或多余日志。

日志影響

日志打印存儲文件時,需要盡可能少的影響程序本身的性能,因為性能影響了程序的流暢性,而流暢性直接影響了用戶體驗。
最基本的流暢性保證是使用了日志方案后不會導致app的卡頓,但是流暢性不僅包括了系統沒有卡頓,還要盡量保證沒有CPU峰值等。

方案一:簡易本地版

??如果不需要考慮日志的存儲、上傳等與服務端連通的操作而只是在終端輸出時,問題也將變得簡單,我們直接使用android.util.Log進行日志打印即可。剩下的問題就只剩下如何控制日志開關、如何打印其他輔助信息(如線程、堆棧等)了。
??經常看到很多app對這種本地日志開關都會使用BuildConfig.DEBUG這個gradle幫我們生成的變量來作為日志開關的標志(debug版本打開日志,release版本關閉日志),在實際開發過程中會發現以這個變量為日志的開關標志其實有一定局限性。比如,某天xx領導拿著自己的手機給我們現場反饋剛上線的版本出現了某個偶現嚴重問題,當你想看下日志分析的時候,發現release版本日志被關掉了,看不了!!!尷尬啊。。。
??于是琢磨著有沒有什么其他手段能動態修改這個日志開關呢?想到前幾年開發中經常用到SystemProperties來存儲一些全局配置信息,剛好能配合這個日志開關的使用,不管什么版本的app,需要看日志的時候,我們通過命令行修改這個配置就可以打開這個開關了,如輸入:adb shell setprop ro.tech.log true

先簡單介紹一下屬性系統:
??屬性系統是android的一個重要特性,它作為一個服務運行,管理系統配置和狀態;所有這些配置和狀態都是屬性;每個屬性是一個鍵值對(key/value pair),其類型都是字符串。這些屬性可能是有些資源的使用狀態、進程的執行狀態、系統的特有屬性等。
系統給的注釋:Gives access to the system properties store. The system properties store contains a list of string key-value pairs.
??對開發者來說更重要的是SystemProperties@hide起來了,意思是普通應用開發不能使用屬性系統。經過測試驗證(在root手機和非root手機上驗證過,不排除某些rom有限制),反射這個類的set()/get()接口仍是有效的,只是谷歌沒有開放給開發者使用而已。有了這樣的前提,我們就可以使用命令行設置屬性打開(關閉)日志開關,app代碼反射讀取屬性來獲取開關來實現日志的動態開關了。

另外,SystemPropertieskey的命名有一些規則限制,比較常用的是以ro開頭和persist開頭的屬性:

  • 如果屬性名稱以ro.開頭,那么這個屬性被視為只讀屬性。一旦設置,屬性值不能改變,需要重啟還原。
  • 如果屬性名稱以persist.開頭,當設置這個屬性時,其值將寫入/data/property,且可以重復寫入。

首先,定義反射屬性系統的接口函數(參見文章《反射相關知識及jOOR反射庫介紹》):

    private static boolean getBoolean(String propName, boolean def) {
        return Reflect.on("android.os.SystemProperties").call("getBoolean", propName, def).get();
    }

根據屬性系統key的命名規則定義日志開關key

    /**
     * 日志開關設置
     *
     * adb shell setprop ro.tech.log true
     */
    private static final String LOG_ENABLE_PROP = "ro.tech.log";

定義日志開關

DLog {
    //...
    //LOGED為靜態的final變量,程序啟動時讀取開關
    public final static boolean LOGED = BuildConfig.DEBUG || getBoolean(LOG_ENABLE_PROP, false);
    //...
}

打印日志,讀取DLog.LOGED屬性(在啟動app前,通過命令行輸入修改屬性指令:adb shell setprop ro.tech.log true

if (DLog.LOGED) DLog.i(TAG, "MainActivity is oncreated!!");

其他輔助信息,如是否使用默認tag打印、是否打印線程信息等設置均可采用該方式實現,具體參見GHDemo-DLog.java的實現。

方案二:服務端預警版

使用場景

  • 服務端檢測http業務接口調用異常(調用量偏高或偏低、接口參數有誤等),獲取客戶端網絡日志分析http調用邏輯的可能問題。(典型用例:消息提醒需求的紅點接口調用量大問題)。
  • 線上用戶反饋但難以復現的嚴重問題,通過獲取用戶反饋問題的描述及用戶日志分析解決此類問題。(典型用例:排查用戶反饋偷跑流量問題)
  • app灰度期間借助第三方的崩潰統計平臺(友盟、bugly等)統計崩潰情況,客戶端的業務日志能方便重現崩潰的操作路徑有利于找到crash的本質原因。

方案描述

??服務端借助push系統主動發出日志開關或日志拉取的透傳指令,客戶端app收到推送后,解析該日志指令并執行相應處理(包括操作日志開關、壓縮并上傳已有日志文件),在服務端收到日志文件后提供日志文件存儲下載服務,開發人員下載對應用戶日志分析問題。
??該方案完全由服務端的指令觸發自動完成,整個流程不需要用戶實際參與,相比要求用戶配合開發人員獲取日志的方式大大縮短了解決問題的時間和整體流程。

方案實現

該方案實現有服務端和客戶端的工作量,因此需要兩端協助完成,具體時序流程如下圖所示:

日志方案時序圖

時序圖中包含了手動拉取自動批量拉取兩種場景:

  • 手動拉取:用戶或預警反饋問題并攜帶客戶端imei,開發人員通過后臺日志管理平臺以imei為唯一標識觸發單條日志指令。
  • 自動批量拉取:大數據生成報表時,獲取存在可能異常用戶樣本列表,通過業務接口自動獲取樣本列表日志。

具體細節功能

服務端

  • 定義日志指令參數格式。
  • 對接push系統,實現查詢在線狀態、日志指令(包括開關控制、日志拉取、指令有效期控制)推送接口,且支持批量推送。
  • 實現日志指令操作的后臺操作界面。
  • 實現日志文件存儲、生成日志下載鏈接。

客戶端

  • 實現日志打印文件和終端輸出,包括日志壓縮、文件大小、有效期及存儲空間控制(可接入第三方日志庫實現,如:xloglog4j 等)
  • 實現push推送的透傳日志指令(包括日志開關、日志上傳等)解析和相應執行操作。
  • 實現日志文件http上傳。
  • http請求接口增加基本參數(如:版本號、imei、安卓版本、系統版本等),便于后續出現問題是的過濾分析。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374