你的位置:首页 > 操作系统

[操作系统]利用 CocoaLumberjack 搭建自己的 Log 系统(转)


一直需要一个 Log 系统,可以将程序运行过程中打的 log 发送到自己服务器,方便之后数据分析或者除错。之前也尝试过找一些第三方服务,但看来看去,国内貌似没看到专门做这一块的,而国外看了下有 Loggly,似乎满足需求,但它要收费且日志保存时间太短。后来无意间看了下 Loggly 提供的 SDK 源代码,发现了 CocoaLumberjack 这个好东西,而 Loggly 其也不过就是在 CocoaLumberjack 上自定义了 Logger 和 Formatter 而已。自己做的话,也很简单。

先说下需求,我理想中的 Log 系统需要:

  • 可以设定 Log 等级

  • 可以积攒到一定量的 log 后,一次性发送给服务器,绝对不能打一个 Log 就发一次

  • 可以一定时间后,将未发送的 log 发送到服务器

  • 可以在 App 切入后台时将未发送的 log 发送到服务器

其他一些需求,比如可以远程设定发送 log 的等级阀值,还有阀值的有效期等,和本文无关就不写了。

开始动手前,先了解下 CocoaLumberjack 是什么:

CocoaLumberjack 最早是由 Robbie Hanson 开发的日志库,可以在 iOS 和 MacOSX 开发上使用。其简单,快读,强大又不失灵活。它自带了几种log方式,分别是:

  • DDASLLogger 将 log 发送给苹果服务器,之后在 Console.app 中可以查看

  • DDTTYLogger 将 log 发送给 Xcode 的控制台

  • DDFileLogger 讲 log 写入本地文件

CocoaLumberjack 打一个 log 的流程大概就是这样的:

001.png

所有的 log 都会发给 DDLog 对象,其运行在自己的一个GCD队列(GlobalLoggingQueue),之后,DDLog 会将 log 分发给其下注册的一个或多个 Logger,这步在多核下是并发的,效率很高。每个 Logger 处理收到的 log 也是在它们自己的 GCD队列下(loggingQueue)做的,它们询问其下的 Formatter,获取 Log 消息格式,然后最终根据 Logger 的逻辑,将 log 消息分发到不同的地方。

因为一个 DDLog 可以把 log 分发到所有其下注册的 Logger 下,也就是说一个 log 可以同时打到控制台,打到远程服务器,打到本地文件,相当灵活。

CocoaLumberjack 支持 Log 等级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
#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 已经实现了需求中的 1、2、3 点,我们要做的无非是自定义 Logger 和 Formatter,将 log 的最终去处改为发送到我们自己的服务器中。

而第四点,我们可以监听 UIApplicationWillResignActiveNotification 事件,当触发时,手动调用 logger 的 db_save 方法,发送数据给服务器。

废话了半天,现在看下实现。

首先我们设定 log 的消息结构。自定义一个 LogFormatter, 遵从 DDLogFormatter 协议,我们需要重写 formatLogMessage 这个方法,这个方法返回值是 NSString,就是最终 log 的消息体字符串。而输入参数 logMessage 是由 logger 发的一个 DDLogMessage 对象,包含了一些必要的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface DDLogMessage : NSObject {
    // 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 所在文件名,行数还有所在函数名,则可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (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的观察者,用以实现第四个需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (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 是否成功保存进缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (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 就会被调用,我们在这里,将缓存的数据上传到自己的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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 相关的东西:

1
2
3
4
5
6
7
8
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 一样。这几个宏的定义:

1
2
3
4
5
6
//注意,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,别忘了帮他买杯啤酒哦~