日志库`CocoaLumberjack`的整合过程

看了几篇文章,一开始感觉整合第三方的日志库CocoaLumberjack比较简单,不过真正落地,发现还是有几个地方需要注意的。

下载代码

CocoaLumberjack/CocoaLumberjack

  • 日志库CocoaLumberjack目前已经到了3.0.0版本,既支持Object-C,也支持Swift。包管理方面,既支持CocoaPods,也支持Carthage。根据当前工程现状,选择Object-C + CocoaPods的方式
  • 在原有的Podfile中添加一行
    pod 'CocoaLumberjack', '~> 3.0.0'
    然后,将终端切换到工程目录,执行命令。带上--no-repo-update是为了加快更新速度
    pod install --no-repo-update

基本需求

  • 可以设定Log 等级
  • 可以积攒到一定量的log 后,一次性发送给服务器,绝对不能打一个Log就发一次
  • 可以一定时间后,将未发送的log发送到服务器
  • 可以在App 切入后台时将未发送的log 发送到服务器
    利用 CocoaLumberjack 搭建自己的 Log 系统
    这篇文章写得很有代表性,这次也主要是按照这个来做。

接口设计

  • 一般的文章,都介绍在APPdelegate中添加代码,这会导致这个类很乱,不是很好
  • 一版本的工程,都有自己的前缀,在工程里到处使用DDLog和整体氛围不搭调,最好在中间再包一层。
  • 由于是日志,大家都习惯了NSLog(frmt,...)这种可变参数形式的c风格调用,而且一般还是宏定义的方式。

引入一个单独的类,采用(类方法 + 单例)的模式,简化接口,保证只执行一次

  • 日志是一种服务,所以,文件命名为XXXLogService,作为一个中间隔离层。用户不需要知道日志系统是怎么实现的,用了哪个第三方库
  • 提供一个类方法,将初始化的代码放在里面,在APPdelegate中只要一句调用就可以了,比如[XXXLogService start];
  • 以下是接口头文件XXXLogService.h的内容,将这个头文件加入pch文件中,就可以在工程里方便使用XXXLog()
#import <Foundation/Foundation.h>
#import <CocoaLumberjack/CocoaLumberjack.h>

#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif

// 默认的宏,方便使用
#define XXXLog(frmt, ...)           XXXLogInfo(frmt, ...)

// 提供不同的宏,对应到特定参数的对外接口
#define XXXLogError(frmt, ...)      DDLogError(frmt, ##__VA_ARGS__)
#define XXXLogWarning(frmt, ...)    DDLogWarn(frmt, ##__VA_ARGS__)
#define XXXLogInfo(frmt, ...)       DDLogInfo(frmt, ##__VA_ARGS__)
#define XXXLogDebug(frmt, ...)      DDLogDebug(frmt, ##__VA_ARGS__)
#define XXXLogVerbose(frmt, ...)    DDLogVerbose(frmt, ##__VA_ARGS__)


@interface XXXLogService : NSObject

+ (void)start;

@end
  • 以下是接口实现文件XXXLogService.m的内容,对外的类接口start只是一层封装。具体的实现在一个成员函数中,这里用了单例,将只执行一次的内容放在了init函数中。
    这个类只是一层封装,没有做具体的事情。
    内容是从网上抄的,这里准备自定义一个loger往后台传日志,下面那么注释为没必要的内容,实际使用时就直接删除了。至于颜色控件相关设置,其实也没有必要,这里只是做个备忘。
#import "XXXLogService.h"
#import "XXXLogger.h"
#import "XXXLogFormatter.h"

@implementation XXXLogService

+ (void)start {
    [self sharedInstance];
}

+ (instancetype)sharedInstance{
    static id sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        // 自定义的log,要传自己的后台
        XXXLogger *logger = [[XXXLogger alloc] init];
        XXXLogFormatter *formatter = [[XXXLogFormatter alloc] init];
        
        [logger setLogFormatter:formatter];
        [DDLog addLogger:logger];
        
        // 注释中一些不需要的代码只是放在这里,见证一下历史;正式使用时都应该删除。
        // XCode的log
        // XCode8之后不支持插件工作,所以这些设置颜色的代码不需要,否则将会有无用的颜色信息混入log
        //开启使用 XcodeColors
        setenv("XcodeColors", "YES", 0);
        //检测
        char *xcode_colors = getenv("XcodeColors");
        if (xcode_colors && (strcmp(xcode_colors, "YES") == 0)) {
            // XcodeColors is installed and enabled!
            NSLog(@"XcodeColors is installed and enabled");
            //开启DDLog 颜色
            [[DDTTYLogger sharedInstance] setColorsEnabled:YES];
            [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor lightGrayColor] backgroundColor:nil forFlag:DDLogFlagVerbose];
            [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor grayColor] backgroundColor:nil forFlag:DDLogFlagDebug];
            [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor blueColor] backgroundColor:nil forFlag:DDLogFlagInfo];
            [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor yellowColor] backgroundColor:nil forFlag:DDLogFlagWarning];
            [[DDTTYLogger sharedInstance] setForegroundColor:[UIColor redColor] backgroundColor:nil forFlag:DDLogFlagError];
        }
        // XCode的log,也用自定义的输出格式
        [[DDTTYLogger sharedInstance] setLogFormatter:formatter];
        [DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
        
        // DDASLLogger是输出到mac终端,没有必要再手机上用
        [DDLog addLogger:[DDASLLogger sharedInstance]]; // ASL = Apple System Logs

        // DDFileLogger是存在手机上,在Cache目录,一般拿不出来,所以一般也没什么大用
        DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; // File Logger
        fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling
        fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
        [DDLog addLogger:fileLogger];
    }
    return self;
}
  • 删除无用代码,精简过后的init代码如下
- (instancetype)init {
    self = [super init];
    if (self) {
        // 自定义的log,要传自己的后台
        XXXLogger *logger = [[WJSLogger alloc] init];
        XXXLogFormatter *formatter = [[WJSLogFormatter alloc] init];
        
        [logger setLogFormatter:formatter];
        [DDLog addLogger:logger];
        
        // XCode的log,也用自定义的输出格式
        [[DDTTYLogger sharedInstance] setLogFormatter:formatter];
        [DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
    }
    return self;
}

日志级别

  • 需要用一个静态全局变量来定义日志级别
  • 日志级别是用来控制日志输出的
  • 通过日志flag(前面定义的)和日志级别level(这里定义的)比较,决定是否输出日志
  • 日志级别Error最高Verbose最低,flag > level就输出,否则就不输出
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif

这样定义的结果是:
DEBUG模式:所有的日志都输出
其他模式:仅仅DDLogError()DDLogWarn()输出,其他的都没有输出

  • 这个变量的名字ddLogLevel最好命名为ddLogLevel,不然编译不通过。原因是里面的宏定义用到了。
#ifndef LOG_LEVEL_DEF
    #define LOG_LEVEL_DEF ddLogLevel
#endif
  • 结合前面的宏定义,提供的方便方法,可以做到开发环境日志很全,而正式环境只搜集warningerror两种日志
  • 在实现文件统一定义,使用者不需要知道日志级别level这个概念,使用变得更简单。
// 默认的宏,方便使用
#define XXXLog(frmt, ...)       XXXLogInfo(frmt, ...)

自定义格式

  • 如果需要自定义协议格式,那么需要实现DDLogFormatter协议的方法- (NSString *)formatLogMessage:(DDLogMessage *)logMessage;
/**
 *  This protocol describes the behavior of a log formatter
 */
@protocol DDLogFormatter <NSObject>
@required

/**
 * Formatters may optionally be added to any logger.
 * This allows for increased flexibility in the logging environment.
 * For example, log messages for log files may be formatted differently than log messages for the console.
 *
 * For more information about formatters, see the "Custom Formatters" page:
 * Documentation/CustomFormatters.md
 *
 * The formatter may also optionally filter the log message by returning nil,
 * in which case the logger will not log the message.
 **/
- (NSString * __nullable)formatLogMessage:(DDLogMessage *)logMessage;

@end
  • 下面是DDLogMessage的定义,使用时一般用指针符号`->``引用内部变量,而给出的属性都是可读的,不能访问。不是非常理解这种设计意图。
/**
 * The `DDLogMessage` class encapsulates information about the log message.
 * If you write custom loggers or formatters, you will be dealing with objects of this class.
 **/
@interface DDLogMessage : NSObject <NSCopying>
{
    // Direct accessors to be used only for performance
    @public
    NSString *_message;
    DDLogLevel _level;
    DDLogFlag _flag;
    NSInteger _context;
    NSString *_file;
    NSString *_fileName;
    NSString *_function;
    NSUInteger _line;
    id _tag;
    DDLogMessageOptions _options;
    NSDate *_timestamp;
    NSString *_threadID;
    NSString *_threadName;
    NSString *_queueLabel;
}

/**
 *  Default `init` is not available
 */
- (instancetype)init NS_UNAVAILABLE;

/**
 * Standard init method for a log message object.
 * Used by the logging primitives. (And the macros use the logging primitives.)
 *
 * If you find need to manually create logMessage objects, there is one thing you should be aware of:
 *
 * If no flags are passed, the method expects the file and function parameters to be string literals.
 * That is, it expects the given strings to exist for the duration of the object's lifetime,
 * and it expects the given strings to be immutable.
 * In other words, it does not copy these strings, it simply points to them.
 * This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters,
 * so it makes sense to optimize and skip the unnecessary allocations.
 * However, if you need them to be copied you may use the options parameter to specify this.
 *
 *  @param message   the message
 *  @param level     the log level
 *  @param flag      the log flag
 *  @param context   the context (if any is defined)
 *  @param file      the current file
 *  @param function  the current function
 *  @param line      the current code line
 *  @param tag       potential tag
 *  @param options   a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction.
 *  @param timestamp the log timestamp
 *
 *  @return a new instance of a log message model object
 */
- (instancetype)initWithMessage:(NSString *)message
                          level:(DDLogLevel)level
                           flag:(DDLogFlag)flag
                        context:(NSInteger)context
                           file:(NSString *)file
                       function:(NSString *)function
                           line:(NSUInteger)line
                            tag:(id)tag
                        options:(DDLogMessageOptions)options
                      timestamp:(NSDate *)timestamp NS_DESIGNATED_INITIALIZER;

/**
 * Read-only properties
 **/

/**
 *  The log message
 */
@property (readonly, nonatomic) NSString *message;
@property (readonly, nonatomic) DDLogLevel level;
@property (readonly, nonatomic) DDLogFlag flag;
@property (readonly, nonatomic) NSInteger context;
@property (readonly, nonatomic) NSString *file;
@property (readonly, nonatomic) NSString *fileName;
@property (readonly, nonatomic) NSString *function;
@property (readonly, nonatomic) NSUInteger line;
@property (readonly, nonatomic) id tag;
@property (readonly, nonatomic) DDLogMessageOptions options;
@property (readonly, nonatomic) NSDate *timestamp;
@property (readonly, nonatomic) NSString *threadID; // ID as it appears in NSLog calculated from the machThreadID
@property (readonly, nonatomic) NSString *threadName;
@property (readonly, nonatomic) NSString *queueLabel;

@end
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
    NSString *logLevel = nil;
    switch (logMessage->_flag) {
        case DDLogFlagError:
            logLevel = @"[ERROR] >  ";
            break;
        case DDLogFlagWarning:
            logLevel = @"[WARN]  >  ";
            break;
        case DDLogFlagInfo:
            logLevel = @"[INFO]  >  ";
            break;
        case DDLogFlagDebug:
            logLevel = @"[DEBUG] >  ";
            break;
        default:
            logLevel = @"[VBOSE] >  ";
            break;
    }
    
    NSString *formatLog = [NSString stringWithFormat:@"%@[%@ %@][line %ld] %@",
                           logLevel, logMessage->_fileName, logMessage->_function,
                           logMessage->_line, logMessage->_message];
    return formatLog;
}

自定义的logger

根据网上内容利用 CocoaLumberjack 搭建自己的 Log 系统修改而来

  • 从类DDAbstractDatabaseLogger继承而来,需要包含头文件#import <CocoaLumberjack/DDAbstractDatabaseLogger.h>
  • 这个类的作用是将log保存在数据库中,这个类没有暴露出来。相对来说,数据库比文件系统操作要方便。所以用这个类,而不用文件类DDFileLogger
  • 保持默认设置就好了,达到500条或者间隔1分钟就保存;磁盘数据库保留7天,删除操作间隔5分钟。
  • 保存在数据库中内容取得不方便,所以手机数据库中的内容我们不关心,他能正常工作就好了。
  • 每一次执行log,函数db_log就会执行。在这里,我们把每条log都保存在一个数组中。比如@property (nonatomic, strong) NSMutableArray *logs;
  • 每一次保存log,函数db_save就会执行。在这里,我们把缓存在数组中的log拼接成一个大字符串(\n分隔),发送给后台。向后台发送成功后,清空这个缓存数组。
  • 至于数据库中的内容,我们不用关心。
  • 监听系统消息UIApplicationWillResignActiveNotification,在应用回到后台前,保存一下,向服务器发送一次。
#import "XXXLogger.h"
#import "MyAFNetWorking.h"

// 达到500条就发送,所以缓存的数组最大容量达到2000的话,说明网络出了问题
#define kLogCacheCapacity        2000

@interface XXXLogger ()

@property (nonatomic, strong) NSMutableArray *logs;

@end

@implementation XXXLogger

// 生命周期函数
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (instancetype)init {
    self = [super init];
    if (self) {
        self.logs = [NSMutableArray array];
        // 使用默认的配置。达到500条或者间隔1分钟就保存;磁盘数据库保留7天,删除操作间隔5分钟,这两个数据不关心,用基类的就可以了
        self.saveThreshold = 500; // 达到500条就保存传后台
        self.saveInterval = 60;   // 60s定时到就保存传后台
        // 监听UIApplicationWillResignActiveNotification消息,在程序进入后台前保存log
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
    }
    return self;
}

// 重写父类函数
- (BOOL)db_log:(DDLogMessage *)logMessage {
    // _logFormatter只能用下划线变量访问,不能用self的方式,否则会触发断言
    if (!_logFormatter) {
        //没有指定 formatter
        return NO;
    }
    
    if ([self.logs count] > kLogCacheCapacity) {
        // 如果段时间内进入大量log,并且迟迟发不到服务器上,我们可以判断哪里出了问题,在这之后的 log 暂时不处理了。
        // 但我们依然要告诉 DDLog 这个存进去了。
        return YES;
    }
    //利用 formatter 得到消息字符串,添加到缓存
    @synchronized (self) {
        // _logFormatter只能用下划线变量访问,不能用self的方式,否则会触发断言
        [self.logs addObject:[_logFormatter formatLogMessage:logMessage]];
    }
    
    return YES;
}

- (void)db_save {
    //如果缓存内没数据,啥也不做
    if (0 == [self.logs count]) {
        return;
    }
    
    // 用换行符,把所有的数据拼成一个大字符串
    NSString *logsString = [self.logs componentsJoinedByString:@"\n"];
    // 发送给服务器,将AFNetworking包一层作为网络传输
    NSString *url = @"";  // 根据实际修改
    NSDictionary *logs = @{@"log": logsString}; // key值跟后台商量好
    __weak __typeof(self) weakSelf = self;
    [[MyAFNetWorking shareAfnetworking] performRequestWithPath:url formDataDic:logs success:^(NSDictionary *responseObject) {
        // 已经成功传到服务器,之后将缓存清空
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf clearLogs];
    } failure:^(NSDictionary *responseObject) {
        // 啥也不做
    }];
}

// selector
- (void)onWillResignActive:(NSNotification *)notification {
    dispatch_async(self.loggerQueue, ^{
        [self db_save];
    });
}

// 清空缓存
- (void)clearLogs {
    @synchronized (self) {
        [self.logs removeAllObjects];
    }
}

@end

日志颜色

支持XCodeColors插件,根据日志等级显示不同的颜色。不过XCode8之后插件都不能用了,所以就不用折腾了。
下面是一些文章链接,可以看看
robbiehanson/XcodeColors
Xcode8 插件失效不能用
Xcode升级后插件失效解决办法
CocoaLumberjack使用

日志保存到阿里云

  • 日志可以发送到自己的后台,怎么发送,和后台商量好就行
  • 阿里云提供了日志服务器,提供了日志发送的API,可以将客户端的日志直接发送到阿里云。运维可以通过工具查看,这样就绕过后台,也减轻了后台的压力。
  • lujiajing1126/AliyunLogObjc
    这个是阿里提供的log上传接口,目前的热度还很低
  • 支持Carthage,不支持CocoaPods,这个有点特别。
  • 如果工程还要支持iOS7,还是直接将源码导入工程比较好,不需要库管理工具。如果是Swift开发的工程,用Carthage就很方便。
#import <AliyunLogObjc/AliyunLogObjc.h> 
LogClient *client = [[LogClient alloc] initWithApp: @"endpoint" accessKeyID:@"" accessKeySecret:@"" projectName:@""];
LogGroup *logGroup = [[LogGroup alloc] initWithTopic: @"" andSource:@""];
Log *log1 = [[Log alloc] init];
[log1 PutContent: @"Value" withKey: @"Key"];
[logGroup PutLog:log1];
[client PostLog:logGroup logStoreName: @"" call:^(NSURLResponse* _Nullable response,NSError* _Nullable error) {
    if (error != nil) {
    }
}];
  • 仅有日志上传功能,并没有日志本地管理功能。可以配合CocoaLumberjack使用。
  • 如果只是简单将日志发送阿里云,单独使用也是可以的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 我以一种最安静的姿势坐下 头靠玻璃窗的上的月光 看着路灯 或是更高的某个地方 其实我第一次发现 燕大的夜景这么美 ...
    Rainer_zhang阅读 236评论 0 0
  • 多年来,一直喜欢听歌,听着不同的曲风,歌声里有年代,也有属于那些年代里,尘封的过往,人们管这种情绪,叫做怀旧,而我...
    林桉阅读 904评论 0 1