客戶端日志方案

背景說明

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

實現(xiàn)目的

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

日志分類

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

日志規(guī)范

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

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

  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對象或復雜數(shù)據結構時,需打印其toString(),并實現(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)中盡量不打印日志,確實有必要打印時,可以先在循環(huán)里使用 StringBuilder拼湊成可讀性較好的字符串,然后在循環(huán)結束后一次性打印寫入文件。
  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的卡頓,但是流暢性不僅包括了系統(tǒng)沒有卡頓,還要盡量保證沒有CPU峰值等。

方案一:簡易本地版

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

先簡單介紹一下屬性系統(tǒng):
??屬性系統(tǒng)是android的一個重要特性,它作為一個服務運行,管理系統(tǒng)配置和狀態(tài);所有這些配置和狀態(tài)都是屬性;每個屬性是一個鍵值對(key/value pair),其類型都是字符串。這些屬性可能是有些資源的使用狀態(tài)、進程的執(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起來了,意思是普通應用開發(fā)不能使用屬性系統(tǒng)。經過測試驗證(在root手機和非root手機上驗證過,不排除某些rom有限制),反射這個類的set()/get()接口仍是有效的,只是谷歌沒有開放給開發(fā)者使用而已。有了這樣的前提,我們就可以使用命令行設置屬性打開(關閉)日志開關,app代碼反射讀取屬性來獲取開關來實現(xiàn)日志的動態(tài)開關了。

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

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

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

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

根據屬性系統(tǒng)key的命名規(guī)則定義日志開關key

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

定義日志開關

DLog {
    //...
    //LOGED為靜態(tài)的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打印、是否打印線程信息等設置均可采用該方式實現(xiàn),具體參見GHDemo-DLog.java的實現(xiàn)。

方案二:服務端預警版

使用場景

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

方案描述

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

方案實現(xiàn)

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

日志方案時序圖

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

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

具體細節(jié)功能

服務端

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

客戶端

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容