文档章节

利用 CocoaLumberjack 搭建自己的 Log 系统

小芒果他爹
 小芒果他爹
发布于 2015/03/03 11:21
字数 1989
阅读 470
收藏 0
点赞 0
评论 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
浦东
高级程序员
iOS 基于CocoaLumberjack构建自己的Log系统

前言 对于一个已经上线的产品,如果项目没有自己的Log系统,产品在线上出现问题,那就只能抓瞎了,所以项目中有一套自己成熟的Log系统是至关重要的。本文主要利用CocoaLumberjack来教大家如何...

上升的羽毛 ⋅ 2017/10/28 ⋅ 0

使用CocoaLumberjack的一些问题记录

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

yoyoso ⋅ 2015/03/02 ⋅ 1

CocoaLumberjack 的使用

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

哥特复心 ⋅ 2014/03/04 ⋅ 1

IOS开发常用插件(二)

SQLite是一个开源的嵌入式关系数据库,它在2000年由D. Richard Hipp发布,它的减少应用程序管理数据的开销,SQLite可移植性好,很容易使用,很小,高效而且可靠。 SQLite嵌入到使用它的应用程...

lotteFu ⋅ 2015/06/03 ⋅ 0

Android NDK GPIO操作(pcduino 装Android系统)

开发工具: Eclipse 开发语言: Java和c++ Pcduino跑Android系统让我兴奋了很长时间,也由此幻想了很多有趣的应用。例如 结合Yeelink做远程加电控制;结合微信开放接口与传感器向好友推送信息...

伽罗kapple ⋅ 2014/11/01 ⋅ 0

iOS开发调试和问题解决策略

iOS开发调试和问题解决策略 经常会听到有人抛出一些很信息很不全的问题,比如界面显示不正确、后端网络请求不通之类的问题,然后开始瞎猜。 我觉得很有必要分享一下解决问题的范式,因为靠猜...

陈圣晗 ⋅ 2015/12/08 ⋅ 0

轻松入门Android直播相关技术 从0搭建直播系统

本文已在我的公众号hongyangAndroid原创首发。 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/77937483 本文出自张鸿洋的博客 本文已在我的公众号hongyangAndroid原创...

lmj623565791 ⋅ 2017/09/12 ⋅ 0

CocoaLumberjack framework 自定义日志管理

github英文链接: https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted 入门翻译全文: 想要学习这个框架只需要三个步骤 1 把框架添加到你的应用程序 2 配置框架 3 用Lumbe...

AmoyAI ⋅ 2012/12/19 ⋅ 0

日志框架--CocoaLumberjack

CocoaLumberjack 是一个快速、简单,但很强大的日志框架,用于 Mac OS X 和 iOS 系统。 要求: Xcode 4.4 or later is required iOS 5 or later OS X 10.7 or later...

红薯 ⋅ 2013/11/25 ⋅ 0

GitLab 8.3.2 正式发布,代码托管平台

GitLab 8.3.2 正式发布,更新如下: - Disable --follow in to avoid loading duplicate commit data in infinite scroll (Stan Hu) - Add support for Google reCAPTCHA in user registrat......

oschina ⋅ 2015/12/30 ⋅ 13

没有更多内容

加载失败,请刷新页面

加载更多

下一页

用ZBLOG2.3博客写读书笔记网站能创造今日头条的辉煌吗?

最近两年,著名的自媒体网站今日头条可以说是火得一塌糊涂,虽然从目前来看也遇到了一点瓶颈,毕竟发展到了一定的规模,继续增长就更加难了,但如今的今日头条规模和流量已经非常大了。 我们...

原创小博客 ⋅ 今天 ⋅ 0

MyBatis四大核心概念

本文讲解 MyBatis 四大核心概念(SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Mapper)。 MyBatis 作为互联网数据库映射工具界的“上古神器”,训有四大“神兽”,谓之:Sql...

waylau ⋅ 今天 ⋅ 0

以太坊java开发包web3j简介

web3j(org.web3j)是Java版本的以太坊JSON RPC接口协议封装实现,如果需要将你的Java应用或安卓应用接入以太坊,或者希望用java开发一个钱包应用,那么用web3j就对了。 web3j的功能相当完整...

汇智网教程 ⋅ 今天 ⋅ 0

2个线程交替打印100以内的数字

重点提示: 线程的本质上只是一个壳子,真正的逻辑其实在“竞态条件”中。 举个例子,比如本题中的打印,那么在竞态条件中,我只需要一个方法即可; 假如我的需求是2个线程,一个+1,一个-1,...

Germmy ⋅ 今天 ⋅ 0

Springboot2 之 Spring Data Redis 实现消息队列——发布/订阅模式

一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式,这里利用redis消息“发布/订阅”来简单实现订阅者模式。 实现之前先过过 redis 发布订阅的一些基础概念和操...

Simonton ⋅ 今天 ⋅ 0

error:Could not find gradle

一.更新Android Studio后打开Project,报如下错误: Error: Could not find com.android.tools.build:gradle:2.2.1. Searched in the following locations: file:/D:/software/android/andro......

Yao--靠自己 ⋅ 昨天 ⋅ 0

Spring boot 项目打包及引入本地jar包

Spring Boot 项目打包以及引入本地Jar包 [TOC] 上篇文章提到 Maven 项目添加本地jar包的三种方式 ,本篇文章记录下在实际项目中的应用。 spring boot 打包方式 我们知道,传统应用可以将程序...

Os_yxguang ⋅ 昨天 ⋅ 0

常见数据结构(二)-树(二叉树,红黑树,B树)

本文介绍数据结构中几种常见的树:二分查找树,2-3树,红黑树,B树 写在前面 本文所有图片均截图自coursera上普林斯顿的课程《Algorithms, Part I》中的Slides 相关命题的证明可参考《算法(第...

浮躁的码农 ⋅ 昨天 ⋅ 0

android -------- 混淆打包报错 (warning - InnerClass ...)

最近做Android混淆打包遇到一些问题,Android Sdutio 3.1 版本打包的 错误如下: Android studio warning - InnerClass annotations are missing corresponding EnclosingMember annotation......

切切歆语 ⋅ 昨天 ⋅ 0

eclipse酷炫大法之设置主题、皮肤

eclipse酷炫大法 目前两款不错的eclipse 1.系统设置 Window->Preferences->General->Appearance 2.Eclipse Marketplace下载【推荐】 Help->Eclipse Marketplace->搜索‘theme’进行安装 比如......

anlve ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部