文档章节

利用 CocoaLumberjack 搭建自己的 Log 系统

小芒果他爹
 小芒果他爹
发布于 2015/03/03 11:21
字数 1989
阅读 485
收藏 0

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

  1. 可以设定 Log 等级

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

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

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

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

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

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

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

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

  • 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 已经实现了需求中的 1、2、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,别忘了帮他买杯啤酒哦~


本文转载自:http://nonomori.farbox.com/post/li-yong-cocoalumberjack-da-jian-zi-ji-de-log-xi-tong

共有 人打赏支持
小芒果他爹
粉丝 4
博文 7
码字总数 3169
作品 0
浦东
高级程序员
使用CocoaLumberjack的一些问题记录

想在Xcode中整一个彩色日志显示,按照GettingStarted.md 一文中的步骤将CocoaLumberjack 2.x整合进我的项目中来,遇到一些问题,当然不乏一些坑,作个记录。 整合步骤: Drag into your pro...

yoyoso
2015/03/02
0
1
CocoaLumberjack 的使用

安装XcodeColors插件 下载地址:https://github.com/robbiehanson/XcodeColors安装方法:下载并解压缩XcodeColors-master.zip打开XcodeColors项目,编译项目可以自动将插件安装至~/Library/A...

哥特复心
2014/03/04
0
1
那些在学习iOS开发前就应该知道的事(part 2)

英文原文:Things I wish I had known before starting iOS development—Part 2 如果你还没读这篇文章的第一部分,请先读完了再来看第二部分。 那些在学习iOS开发前就应该知道的事(part 1)...

TomatosX
2015/06/12
0
0
Xcode6下新建XMPP项目导入框架详解

1、xcode版本 2、xmpp 版本: https://github.com/robbiehanson/XMPPFramework 开始导入框架:(最好先在show in finder里面建立文件夹,再把xmpp框架里的东西拷贝到刚建立的文件夹,再把这些...

SoulJa
2015/07/04
0
0
Facebook Paper使用的第三方库

第三方库名 简介 链接 ACE code editor https://github.com/ajaxorg/ace Appirater 用户评分组件 https://github.com/arashpayan/appirater Reachability 网络连通测试 https://github.com/t......

hejunbinlan
2015/08/18
0
0

没有更多内容

加载失败,请刷新页面

加载更多

RESTful架构详解

1. 什么是REST REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。 它首次出现在2000年Roy Fielding的博士论文中,Roy Fielding是HTTP规范的主...

kitty1116
14分钟前
0
0
精通Spring Boot——第十篇:Quartz动态配置定时任务

定时任务简述 定时任务,在企业开发中尤其重要,很多业务都是需要定时任务去做的。比如说10点开售某件东西,凌晨0点统计注册人数,统计其他各种等等。这个时候不可能说让人为的去开启某个开关...

developlee的潇洒人生
17分钟前
0
0
将一些内容输出到文件中

看到一个面试题,如下: 第八题: 一个字符串将其输入到一个文件中,代码如下: <?php$a = '[{"teamId": "43", "serial": "1"},{"teamId": "1", "serial": "2"},{"teamId": "14", "serial":...

vinci321
25分钟前
0
0
nginx的简单使用:负载均衡

nginx:反向代理的服务器;用户发送请求到nginx,nginx把请求发送给真正的服务器,等待服务器处理完数据并返回,再把数据发送给用户。 nginx作为一个反向代理服务器,能缓存我们项目的静态文...

osliang
今天
2
0
网站title标题被改并被百度网址安全中心提醒的解决办法

国庆假日期间我们Sine安全接到众多网站站长求助网站标题被改导致在百度搜索中百度安全中心提醒被拦截,导致网站正常用户无法浏览网站被跳转到一些菠菜du博网站,而且很明显的一个特征就是在百...

网站安全
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部