利用 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,别忘了帮他买杯啤酒哦~

相关文章
相关标签/搜索