利用 CocoaLumberjack 搭建本身的 Log 系統

先說下需求,我理想中的 Log 系統須要:git

  1. 能夠設定 Log 等級github

  2. 能夠積攢到必定量的 log 後,一次性發送給服務器,絕對不能打一個 Log 就發一次web

  3. 能夠必定時間後,將未發送的 log 發送到服務器數據庫

  4. 能夠在 App 切入後臺時將未發送的 log 發送到服務器json

其餘一些需求,好比能夠遠程設定發送 log 的等級閥值,還有閥值的有效期等,和本文無關就不寫了。緩存

開始動手前,先了解下 CocoaLumberjack 是什麼:服務器

CocoaLumberjack 最先是由 Robbie Hanson 開發的日誌庫,能夠在 iOS 和 MacOSX 開發上使用。其簡單,快讀,強大又不失靈活。它自帶了幾種log方式,分別是:併發

  • DDASLLogger 將 log 發送給蘋果服務器,以後在 Console.app 中能夠查看app

  • DDTTYLogger 將 log 發送給 Xcode 的控制檯async

  • DDFileLogger 講 log 寫入本地文件

CocoaLumberjack 打一個 log 的流程大概就是這樣的:
Header

全部的 log 都會發給 DDLog 對象,其運行在本身的一個GCD隊列(GlobalLoggingQueue),以後,DDLog 會將 log 分發給其下注冊的一個或多個 Logger,這步在多核下是併發的,效率很高。每一個 Logger 處理收到的 log 也是在它們本身的 GCD隊列下(loggingQueue)作的,它們詢問其下的 Formatter,獲取 Log 消息格式,而後最終根據 Logger 的邏輯,將 log 消息分發到不一樣的地方。

由於一個 DDLog 能夠把 log 分發到全部其下注冊的 Logger 下,也就是說一個 log 能夠同時打到控制檯,打到遠程服務器,打到本地文件,至關靈活。

CocoaLumberjack 支持 Log 等級:

typedef NS_OPTIONS(NSUInteger, DDLogFlag) {
    DDLogFlagError      = (1 << 0), // 0...00001
    DDLogFlagWarning    = (1 << 1), // 0...00010
    DDLogFlagInfo       = (1 << 2), // 0...00100
    DDLogFlagDebug      = (1 << 3), // 0...01000
    DDLogFlagVerbose    = (1 << 4)  // 0...10000};typedef NS_ENUM(NSUInteger, DDLogLevel) {
    DDLogLevelOff       = 0,
    DDLogLevelError     = (DDLogFlagError),                       // 0...00001
    DDLogLevelWarning   = (DDLogLevelError   | DDLogFlagWarning), // 0...00011
    DDLogLevelInfo      = (DDLogLevelWarning | DDLogFlagInfo),    // 0...00111
    DDLogLevelDebug     = (DDLogLevelInfo    | DDLogFlagDebug),   // 0...01111
    DDLogLevelVerbose   = (DDLogLevelDebug   | DDLogFlagVerbose), // 0...11111
    DDLogLevelAll       = NSUIntegerMax                           // 1111....11111 (DDLogLevelVerbose plus any other flags)};

DDLogLevel 定義了全局的 log 等級,DDLogFlag 是咱們打 log 時設定的 log 等級,CocoaLumberjack 會比較二者,若是 flag 低於 level,則不會打 log:

#define LOG_MAYBE(async, lvl, flg, ctx, tag, fnct, frmt, ...) \        do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, tag, fnct, frmt, ##__VA_ARGS__); } while(0)

DDLogger 協議定義了 logger 對象須要聽從的方法和變量,爲了方便使用,其提供了 DDAbstractLogger 對象,咱們只須要繼承該對象就能夠自定義本身的 logger。對於第二點和第三點需求,咱們能夠利用 DDAbstractDatabaseLogger,其也是繼承自 DDAbstractLogger,並在其上定義了 saveThreshold, saveInterval 等控制參數。這個 logger 自己是針對寫入數據庫的 log 設計的,咱們也能夠利用它這幾個參數,實現咱們上面所提的需求的第二和第三點。

對於第二點,設定 _saveThreshold 值便可,好比若是但願積攢1000條 log 再一次性發送,就賦值 1000.
對於第三點,設定 _saveInterval,好比若是但願每分鐘發送一次,就設定 60.

由此,CocoaLumberjack 已經實現了需求中的 一、二、3 點,咱們要作的無非是自定義 Logger 和 Formatter,將 log 的最終去處改成發送到咱們本身的服務器中。

而第四點,咱們能夠監聽 UIApplicationWillResignActiveNotification 事件,當觸發時,手動調用 logger 的 db_save 方法,發送數據給服務器。

廢話了半天,如今看下實現。

首先咱們設定 log 的消息結構。自定義一個 LogFormatter, 聽從 DDLogFormatter 協議,咱們須要重寫 formatLogMessage 這個方法,這個方法返回值是 NSString,就是最終 log 的消息體字符串。而輸入參數 logMessage 是由 logger 發的一個 DDLogMessage 對象,包含了一些必要的信息:

@interface DDLogMessage : NSObject <NSCopying>{
    // Direct accessors to be used only for performance
    @public
    NSString *_message;
    DDLogLevel _level;
    DDLogFlag _flag;
    NSUInteger _context;
    NSString *_file;
    NSString *_fileName;
    NSString *_function;
    NSUInteger _line;
    id _tag;
    DDLogMessageOptions _options;
    NSDate *_timestamp;
    NSString *_threadID;
    NSString *_threadName;
    NSString *_queueLabel;}

能夠利用這些信息構建本身的 log 消息體。好比咱們這裏只須要 log 所在文件名,行數還有所在函數名,則能夠這樣寫:

- (NSString *)formatLogMessage:(DDLogMessage *)logMessage{
    NSMutableDictionary *logDict = [NSMutableDictionary dictionary];

    //取得文件名
    NSString *locationString;
    NSArray *parts = [logMessage->_file componentsSeparatedByString:@"/"];
    if ([parts count] > 0)
        locationString = [parts lastObject];
    if ([locationString length] == 0)
        locationString = @"No file";

    //這裏的格式: {"location":"myfile.m:120(void a::sub(int)"}, 文件名,行數和函數名是用的編譯器宏 __FILE__, __LINE__, __PRETTY_FUNCTION__
    logDict[@"location"] = [NSString stringWithFormat:@"%@:%lu(%@)", locationString, (unsigned long)logMessage->_line, logMessage->_function]

    //嘗試將logDict內容轉爲字符串,其實這裏能夠直接構造字符串,但真實項目中,確定須要不少其餘的信息,不可能僅僅文件名、行數和函數名就夠了的。
    NSError *error;
    NSData *outputJson = [NSJSONSerialization dataWithJSONObject:logfields options:0 error:&error];
    if (error)
        return @"{\"location\":\"error\"}"
    NSString *jsonString = [[NSString alloc] initWithData:outputJson encoding:NSUTF8StringEncoding];
    if (jsonString)
        return jsonString;
    return @"{\"location\":\"error\"}"}

接下來自定義 logger,其繼承自 DDAbstractDatabaseLogger。在初始化方法中,先設定好一些參數,以及添加一個UIApplicationWillResignActiveNotification的觀察者,用以實現第四個需求。

- (instancetype)init {
    self = [super init];
    if (self) {
        self.deleteInterval = 0;
        self.maxAge = 0;
        self.deleteOnEverySave = NO;
        self.saveInterval = 60;
        self.saveThreshold = 500;

        //別忘了在 dealloc 裏 removeObserver
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(saveOnSuspend)
                                                     name:@"UIApplicationWillResignActiveNotification"
                                                   object:nil];
    }
    return self;}- (void)saveOnSuspend {
    dispatch_async(_loggerQueue, ^{
        [self db_save];
    });}

每次打 log 時,db_log: 會被調用,咱們在這個函數裏,將 log 發給 formatter,將返回的 log 消息體字符串保存在緩衝中。 db_log 的返回值告訴 DDLog 該條 log 是否成功保存進緩存。

- (BOOL)db_log:(DDLogMessage *)logMessage{
    if (!_logFormatter) {
        //沒有指定 formatter
        return NO;
    }

    if (!_logMessagesArray)
        _logMessagesArray = [NSMutableArray arrayWithCapacity:500]; // 咱們的saveThreshold只有500,因此通常狀況下夠了

    if ([_logMessagesArray count] > 2000) {
        // 若是段時間內進入大量log,而且遲遲發不到服務器上,咱們能夠判斷哪裏出了問題,在這以後的 log 暫時不處理了。
        // 但咱們依然要告訴 DDLog 這個存進去了。
        return YES;
    }

    //利用 formatter 獲得消息字符串,添加到緩存
    [_logMessagesArray addObject:[_logFormatter formatLogMessage:logMessage]];
    return YES;}

當1分鐘或者未寫入 log 數達到 500 時, db_save 就會被調用,咱們在這裏,將緩存的數據上傳到本身的服務器。

- (void)db_save{
    //判斷是否在 logger 本身的GCD隊列中
    if (![self isOnInternalLoggerQueue])
        NSAssert(NO, @"db_saveAndDelete should only be executed on the internalLoggerQueue thread, if you're seeing this, your doing it wrong.");

    //若是緩存內沒數據,啥也不作
    if ([_logMessagesArray count] == 0)
        return;

    獲取緩存中全部數據,以後將緩存清空
    NSArray *oldLogMessagesArray = [_logMessagesArray copy];
    _logMessagesArray = [NSMutableArray arrayWithCapacity:0];

    //用換行符,把全部的數據拼成一個大字符串 
    NSString *logMessagesString = [oldLogMessagesArray componentsJoinedByString:@"\n"];

    //發送給咱本身服務器(本身實現了)
    [self post:logMessagesString];}

最後,咱們須要在程序某處定義全局 log 等級(我這裏使用 Info),並在 AppDelegate 的 didFinishLaunchingWithOptions 裏初始化全部 Log 相關的東西:

static NSUInteger LOG_LEVEL_DEF = DDLogLevelInfo;- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    MyLogger *logger = [MyLogger new];
    [logger setLogFormatter:[MyLogFormatter new]];
    [DDLog addLogger:logger];
    //....}

而後就能夠利用 DDLogError, DDLogWarning 等宏在程序中打 log 了。使用方法與 NSLog 同樣。這幾個宏的定義:

//注意,DDLogError 是確定同步的#define DDLogError(frmt, ...) LOG_MAYBE(NO, LOG_LEVEL_DEF, DDLogFlagError, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)#define DDLogWarn(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagWarning, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)#define DDLogInfo(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagInfo, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)#define DDLogDebug(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagDebug, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)#define DDLogVerbose(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)

最後感謝 CocoaLumberjack 的做者 Robbie Hanson ,若是你喜歡他開發的庫,好比 XMPPFramework,別忘了幫他買杯啤酒哦~

相關文章
相關標籤/搜索