iOS開發-全量日誌捕獲CocoaLumberjack

前言

全量日誌就是app的運行日誌打印等等。有時候光憑Crash日誌並不能找到並解決問題,如果有CrashApp的日誌輸出,則會事半功倍。

CocoaLumberjackOSXiOS平臺優秀的全量日誌抓取第三方庫。github鏈接

此篇文章更着重於分析其實現以及結構組成。

日誌重定向

我們通過日誌重定向可以進行將控制檯的輸出日誌存儲到文件中

- (void)redirectLogToDocumentFolder
{
    // 獲取沙盒路徑
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
    NSString *documentDirectory = [paths objectAtIndex:0];
    // 獲取打印輸出文件路徑
    NSString *fileName = [NSString stringWithFormat:@"myData.log"];
    NSString *logFilePath = [documentDirectory stringByAppendingPathComponent:fileName];
    // 先刪除已經存在的文件
    NSFileManager *defaultManager = [NSFileManager defaultManager];
    [defaultManager removeItemAtPath:logFilePath error:nil];
    // 將NSLog的輸出重定向到文件,因爲C語言的printf打印是往stdout打印的,這裏也把它重定向到文件
    freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding],"a+", stdout);
    freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding],"a+", stderr);
}

這樣的壞處是,當重定向之後,控制檯不再打印日誌輸出了,雖然我們可以判斷xcode是否連接,然後再進行重定向,來解決連接xcode調試的問題。

但是還是有不足,就是當你的日誌輸出你只想自己看到,而不想影響控制檯的輸出時,顯然重定向不能做到,我們需要自己的日誌輸出入口,同時還要能監聽到系統的日誌輸出,而不影響控制檯的日誌。

CocoaLumberjack 就可以幫我們實現這個功能。一般來說我們需要創建3個logger,分別是

  • ASL 用於記錄系統的日誌輸出,這個輸出xcode有些不會打印出來
  • TTY 控制檯會打印的日誌輸出
  • File 寫入文件中,存爲log文件,然後上傳,便於分析問題。

Lumberjack組成

傳統的NSLog()函數將它的輸出指向兩個地方:

  • 蘋果系統日誌ASL (Apple System Logs)
  • StdErr(如果StdErr是一個TTY),所以日誌語句顯示在Xcode控制檯

Capture 捕捉

DDASLLogCaptureCocoaLumberjack中唯一的一個Capture類,用與捕獲ASL日誌

Logger 輸出

logger用於輸出日誌,有

  • DDASLLogger 用於輸出到ASL
  • DDOSLoggeriOS 10之後公開的日誌輸出方式,用於取代ASL,你可以在官方文檔 中查看接口
  • DDTTYLogger 該類爲終端輸出Xcode控制檯輸出提供一個日誌記錄器
  • DDFileLogger 用於將日誌輸出到文件中,我們一般存儲日誌文件之後進行壓縮。

要實現替換 NSLog() 的功能,您可以簡單地添加DDASLLogger和一個DDTTYLogger
但是,如果您選擇使用文件記錄器(DDFileLogger)(以獲得更快的性能),
你可以選擇只使用一個文件記錄器(DDFileLogger)和一個tty記錄器(DDTTYLogger)

message and formatter 消息以及格式化

DDLogMessage是封裝的消息實體
DDLogFormatter 是對輸出的字符串格式化的類別

你可以對照CocoaLumberjack源碼中的Demos進行更好的理解
在這裏插入圖片描述
裏面各種場景都很有參考意義。

ASL 日誌系統

ASL (Apple system logger)是蘋果公司自己實現的一套輸出日誌的接口。

通過DDASLLogger.m文件,我們瞭解到captureAslLogs做了捕捉日誌輸出的功能

+ (void)captureAslLogs {
    @autoreleasepool
    {
        /*
           We use ASL_KEY_MSG_ID to see each message once, but there's no
           obvious way to get the "next" ID. To bootstrap the process, we'll
           search by timestamp until we've seen a message.
         */

        struct timeval timeval = {
            .tv_sec = 0
        };
        gettimeofday(&timeval, NULL);
        unsigned long long startTime = (unsigned long long)timeval.tv_sec;
        __block unsigned long long lastSeenID = 0;

        /*
           syslogd posts kNotifyASLDBUpdate (com.apple.system.logger.message)
           through the notify API when it saves messages to the ASL database.
           There is some coalescing - currently it is sent at most twice per
           second - but there is no documented guarantee about this. In any
           case, there may be multiple messages per notification.

           Notify notifications don't carry any payload, so we need to search
           for the messages.
         */
        int notifyToken = 0;  // Can be used to unregister with notify_cancel().
        notify_register_dispatch(kNotifyASLDBUpdate, &notifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token)
        {
            // At least one message has been posted; build a search query.
            @autoreleasepool
            {
                aslmsg query = asl_new(ASL_TYPE_QUERY);
                char stringValue[64];

                if (lastSeenID > 0) {
                //格式化一個64位的字符串
                    snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID);
                   //進行查找 - 按 > seenID 查找
                    asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC);
                } else {
                //時間查找
                    snprintf(stringValue, sizeof stringValue, "%llu", startTime);
                    asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC);
                }

                [self configureAslQuery:query];

                // Iterate over new messages.
                aslmsg msg;
                aslresponse response = asl_search(NULL, query);
                
                while ((msg = asl_next(response)))
                {
                    [self aslMessageReceived:msg];

                    // Keep track of which messages we've seen.
                    lastSeenID = (unsigned long long)atoll(asl_get(msg, ASL_KEY_MSG_ID));
                }
                asl_release(response);
                asl_free(query);

                if (_cancel) {
                    notify_cancel(token);
                    return;
                }

            }
        });
    }
}

  • timeval
    timeval表示時間的一個結構體,
struct timeval {
        long    tv_sec;         /* 秒 */
        long    tv_usec;        /* 毫秒 */
};

通過gettimeofday(&timeval, NULL);能夠獲得當前系統時間


  • notify_register_dispatch用於註冊進程間的系統通知,kNotifyASLDBUpdate是一個通知,當日志消息被添加到ASL數據庫的時候發出的跨進程通知。
/*
 * ASL notifications
 * Sent by syslogd to advise clients that new log messages have been
 * added to the ASL database.
 */
#define kNotifyASLDBUpdate "com.apple.system.logger.message"

通過/usr/include/notify_keys.h文件可以查看更多相關通知內容。


  • aslMessageReceived入參是aslmsg類型,aslget將其轉爲char字符串類型後,再轉爲NSString
	NSString *message = @(messageCString);
	//這裏獲取秒和毫微秒(十億分之一秒)
    const char* secondsCString = asl_get( msg, ASL_KEY_TIME );
    const char* nanoCString = asl_get( msg, ASL_KEY_TIME_NSEC );
    NSTimeInterval seconds = secondsCString ? strtod(secondsCString, NULL) : [NSDate timeIntervalSinceReferenceDate] - NSTimeIntervalSince1970;
    double nanoSeconds = nanoCString? strtod(nanoCString, NULL) : 0;
    //1e9 = 1000000000
    NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9);

    NSDate *timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds];
	//生成message
    DDLogMessage *logMessage = [[DDLogMessage alloc]initWithMessage:message
                                                              level:_captureLevel
                                                               flag:flag
                                                            context:0
                                                               file:@"DDASLLogCapture"
                                                           function:nil
                                                               line:0
                                                                tag:nil
                                                            options:0
                                                          timestamp:timeStamp];
    //記錄到文件
    [DDLog log:async message:logMessage];

這裏跟蹤log:message:方法最終也是通過寫入日誌到文件句柄方式

NSFileHandle *handle = [self lt_currentLogFileHandle];
[handle seekToEndOfFile];
[handle writeData:data];

瞭解到其實現之後,我們就可以自己編寫簡單的ASL日誌捕獲工具類,或者使用DDASLLogger輸出到ASLDDASLLogger能夠將DDLogMessage轉化爲aslmsg進行賦值之後輸出到ASL系統。

TTY 控制檯輸出

前面我們說到了日誌重定向,目的是將本來寫入到控制檯的輸出,轉而寫入到文件中。

NSLog其實就是寫入到文件syslog中,既然要往文件中寫,那麼肯定就有文件的句柄了,C語言中,有3個句柄,也是我們進行重定向時用到的。

  #define stdin __stdinp
  #define stdout __stdoutp
  #define stderr __stderrp

在iOS平臺中,有以下3個:

  #define STDIN_FILENO 0 /* standard input file descriptor */
  #define STDOUT_FILENO 1 /* standard output file descriptor */
  #define STDERR_FILENO 2 /* standard error file descriptor */

NSLog 是在向STDERR_FILENO中寫入,你可以使用c語言的輸出到文件的fprintf來驗證一下:

NSLog(@"ViewController viewDidLoad");
fprintf (stderr, "%s\n", "ViewController viewDidLoad222");

控制檯可見輸出爲:

2016-06-15 12:57:17.286 TestNSlog[68073:1441419] ViewController viewDidLoad
ViewController viewDidLoad222

關於重定向的更多內容你可以查看 這篇博文

好了,這下說說TTY的實現,要想實現控制檯的輸出,那麼就輸出到STDERR_FILENO就行了,源碼中也是這樣的實現,值得注意的是,控制檯的輸出可以輸出顏色,所以DDTTYLogger實現中包含了很多顏色的處理,你以搭配CLIColor來了解。

下面是TTYLogger的輸出部分:

// Write the log message to STDERR

        if (isFormatted) {
            // The log message has already been formatted.
            int iovec_len = (_automaticallyAppendNewlineForCustomFormatters) ? 5 : 4;
            struct iovec v[iovec_len];

            if (colorProfile) {
                v[0].iov_base = colorProfile->fgCode;
                v[0].iov_len = colorProfile->fgCodeLen;

                v[1].iov_base = colorProfile->bgCode;
                v[1].iov_len = colorProfile->bgCodeLen;

                v[iovec_len - 1].iov_base = colorProfile->resetCode;
                v[iovec_len - 1].iov_len = colorProfile->resetCodeLen;
            } else {
                v[0].iov_base = "";
                v[0].iov_len = 0;

                v[1].iov_base = "";
                v[1].iov_len = 0;

                v[iovec_len - 1].iov_base = "";
                v[iovec_len - 1].iov_len = 0;
            }

            v[2].iov_base = (char *)msg;
            v[2].iov_len = msgLen;

            if (iovec_len == 5) {
                v[3].iov_base = "\n";
                v[3].iov_len = (msg[msgLen] == '\n') ? 0 : 1;
            }

            writev(STDERR_FILENO, v, iovec_len);
        } else {
            // The log message is unformatted, so apply standard NSLog style formatting.
            ...
		}

os_log 新日誌系統

iOS 10之後的os_log更爲簡單

常用接口有:

  • os_log_with_type 將特定日誌級別的消息(如default、info、debug、error)發送到日誌系統。
  • os_log_debug 向日志系統發送調試級別消息。
  • os_log_info 向日志系統發送信息級消息。
  • os_log_error 向日志系統發送錯誤級消息。
  • os_log_fault 向日志系統發送默認級別的消息。
  • os_log 向日志系統發送一個默認級別的消息。與os_log_fault一樣

你可以看看 官方文檔

- (void)logMessage:(DDLogMessage *)logMessage {
    // Skip captured log messages
    if ([logMessage->_fileName isEqualToString:@"DDASLLogCapture"]) {
        return;
    }

    if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) {
        NSString * message = _logFormatter ? [_logFormatter formatLogMessage:logMessage] : logMessage->_message;
        if (message != nil) {
            const char *msg = [message UTF8String];
            __auto_type logger = [self logger];
            switch (logMessage->_flag) {
                case DDLogFlagError  :
                    os_log_error(logger, "%{public}s", msg);
                    break;
                case DDLogFlagWarning:
                case DDLogFlagInfo   :
                    os_log_info(logger, "%{public}s", msg);
                    break;
                case DDLogFlagDebug  :
                case DDLogFlagVerbose:
                default              :
                    os_log_debug(logger, "%{public}s", msg);
                    break;
            }
        }
    }
}

file logger 文件輸出

文件loggerCocoaLumberjack算是比較重要的部分了,其實現全部在DDFileLogger

DDLogFileManager 文件輸出協議

對於文件管理協議DDLogFileManager,主要是下面的4個屬性

  • maximumNumberOfLogFiles 要保存在磁盤上的歸檔日誌文件的最大數量。如果這個屬性設置爲3,將只保留3個歸檔日誌文件(加上當前活動的日誌文件)在磁盤上,你可以設置0將其禁用

  • logFilesDiskQuota 日誌佔用的最大空間。在滾動日誌文件時,所有超過logFilesDiskQuota的舊日誌文件都將被刪除。你可以設置0將其禁用

  • logsDirectory
    所有日誌文件都放在logsDirectory中。
    如果沒有指定特定的logsDirectory,則使用默認目錄。

  1. Mac上,這是在~/Library/Logs/<Application Name>

  2. iPhone上,這是在 ~/Library/Caches/Logs.

    日誌文件被命名爲<bundle identifier> <date> <time>.log,例如 Example: com.organization.myapp 2013-12-03 17-14.log

    存檔的日誌文件會根據“maximumNumberOfLogFiles”屬性自動刪除。

  • maximumFileSize 允許日誌文件增長的最大大小(以字節爲單位)。 如果日誌文件大於這個值, 將會生成一個新的日誌文件進行繼續寫入。

  • rollingFrequency 滾日誌文件的頻率。 頻率以NSTimeInterval的形式給出,它是一個雙精度浮點數,指定以秒爲單位的間隔。 一旦日誌文件變得這麼舊,它就會被重新生成。例如10min = 60x10就重新生成一個日誌文件

您可以通過將“maximumFileSize”設置爲0來選擇性地禁用由於文件大小而導致的滾動。 如果你這樣做,滾動是完全基於“rollingFrequency”。

您可以選擇通過將“rollingFrequency”設置爲0(或任何非正數)來禁用由於時間而導致的滾動。 如果你這樣做了,滾動僅僅是基於“maximumFileSize”。

如果您同時禁用“maximumFileSize”和“rollingFrequency”,那麼日誌文件將永遠不會被滾動。 這是強烈不鼓勵的。

這些值默認值爲

// maximumFileSize         -> kDDDefaultLogMaxFileSize
// rollingFrequency        -> kDDDefaultLogRollingFrequency
// maximumNumberOfLogFiles -> kDDDefaultLogMaxNumLogFiles
// logFilesDiskQuota       -> kDDDefaultLogFilesDiskQuota

unsigned long long const kDDDefaultLogMaxFileSize      = 1024 * 1024;      // 1 MB
NSTimeInterval     const kDDDefaultLogRollingFrequency = 60 * 60 * 24;     // 24 Hours
NSUInteger         const kDDDefaultLogMaxNumLogFiles   = 5;                // 5 Files
unsigned long long const kDDDefaultLogFilesDiskQuota   = 20 * 1024 * 1024; // 20 MB

文件壓縮

可以參考Demos中的LogFileCompressor,使用zlib壓縮成了gz壓縮文件。

具體你可以查看CompressingLogFileManager.m的實現。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章