【iOS】CocoaLumberJack日志库集成

前言

CocoaLumberjack是适用于Mac和iOS的快速,简单,功能强大且灵活的日志记录框架。

最近想要升级项目中的Log库,比较了一番之后,发现CocoaLumberjack这个库可自定义性特别强,能够满足我们的所有要求,而且使用人数很多,可维护性强,性能也不错。于是最终选择了这个库进行集成。集成这个库的过程也花了一段时间,下面会用一步一步介绍这个库的使用。最后会将这些功能集成到一个文件里面,方便使用。(话说好想吐槽下这个库的名字。。。)
本文Demo

基本使用

使用cocoapod安装库:pod 'CocoaLumberjack','~> 3.6.1'
由于Log库在所有的文件中都可能用到,故最好是在项目中的全局头文件PCH文件中导入Log库的头文件。另外,这个库有个挺特别的地方,需要我们自己定义一个ddLogLevel变量,用来定义当前日志的输出等级。

//项目的pch文件中
#import 
//定义Log等级
static const DDLogLevel ddLogLevel = DDLogLevelDebug;

如上所示,上面定义了ddLogLevel等级为DDLogLevelDebug。

然后我们在项目入口进行初始化,就可以正常使用日志库了。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    //添加控制台logger
    [DDLog addLogger:[DDOSLogger sharedInstance]];
    
    //打印,跟NSLog用法一致
    DDLogError(@"DDLogError");
    DDLogWarn(@"DDLogWarn");
    DDLogInfo(@"My name is = %@",@"谦言忘语");
    DDLogDebug(@"I am %d years old",18);
    DDLogVerbose(@"DDLogVerbose");
    
    return YES;
}
控制台显示结果

只需要将我们熟悉的NSLog替换为CocoaLumberjack提供的DDLogInfo等就可以了。所以使用起来还是挺简单的,符合我们以往的使用习惯。

跟NSLog不一样的地方是,CocoaLumberjack提供了不同等级的Log输出,一共有5种等级,以下为5种等级的宏定义。

DDLogError(frmt, ...)      //错误      当ddLogLevel = DDLogLevelError及以上等级时会输出
DDLogWarn(frmt, ...)       //警告      当ddLogLevel = DDLogLevelWarning及以上等级时会输出
D
DDLogInfo(frmt, ...)       //信息      当ddLogLevel = DDLogLevelInfo及以上等级时会输出
D
DDLogDebug(frmt, ...)      //调试      当ddLogLevel = DDLogLevelDebug及以上等级时会输出
D
DDLogVerbose(frmt, ...)    //详细      当ddLogLevel = DDLogLevelVerbose及以上等级时会输出

输出的等级由我们刚开始定义的ddLogLevel决定。比如我们上面定义了ddLogLevel = DDLogLevelDebug,那么debug及以下等级的Log都能够输出,而使用DDLogVerbose的Log就不会输出。

Logger

CocoaLumberjack内置了以下几种Logger。

DDFileLogger:此类提供了一个记录器,可将日志语句写入文件。
DDOSLogger:此类提供了Apple os_log工具的记录器。
DDASLLogger:此类为Apple系统日志工具提供了一个记录器。
DDTTYLogger:此类为终端输出或Xcode控制台输出提供了记录器。

有必要解释以下几种Logger的区别。
DDFileLogger:很容易理解,是将log写入到文件中。
DDOSLogger:在iOS10开始使用,在将Log输出到 控制台.app 和 Xcode控制台。跟NSLog的输出方式一致。当然,经过处理之后,性能会比直接使用NSLog要好。
DDASLLogger:将日志写入到控制台.app中。在iOS10开始过时
DDTTYLogger:将日志写入到Xcode控制台。

而我们常用给的NSLog会将日志写入到控制台.app和Xcode控制台。
所以,想要替换NSLog,官方推荐的做法是:
在iOS10及以上系统版本,使用DDOSLogger。
在iOS10以下版本,使用DDASLLogger+DDTTYLogger。

另外,如果你在系统 控制台.app 里面查看log的时候,看不到对应的log,那很可能是因为你没有把 控制台.app 的【包括简介信息]【包括调试信息】勾选上。

DDOSLogger需要在控制台勾选上简介信息和调试信息

日志等级修改

由于项目需要,我们需要动态修改日志的等级。
就是说我们需要在APP运行过程中修改日志等级。比如启动的时候,默认等级为DDLogLevelWarning。当我们需要查看更详细日志的时候,就将日志等级改为DDLogLevelVerbose
怎么做呢?
之前做的时候在网上搜索了一圈,发现相关的资料并不算多,有些做到了,但是搞得很复杂。
其实这个功能实现起来不需要那么复杂。CocoaLumberjack是通过上面我们定义的ddLogLevel这个变量来控制当前输出的日志等级的。只要我们在APP运行的某个时刻修改ddLogLevel的值就可以了。
所以我们需要对ddLogLevel修改下,去掉const修饰

//定义Log等级,默认为DDLogLevelWarning
static DDLogLevel ddLogLevel = DDLogLevelDebug;

然后适当的地方修改ddLogLevel的值

//修改Log等级为DDLogLevelVerbose
ddLogLevel = DDLogLevelVerbose

这样就可以实现动态修改等级的需求了。
但是这种方案不能实现我们的要求,因为这种方案只能修改某一个文件的内的log等级,就是说我们AppDelegate.m里面修改了log等级为DDLogLevelVerbose,那么就只有在这个类里面生效,在ViewController.m是不生效的。
于是采用了另外一种方案。

更好的日志等级动态修改方案

首先我们新建一个类(名字可以为SQLogManager),在SQLogManager.h文件中声明一个ddLogLevel外部变量,再然后在SQLogManager.m文件里面定义ddLogLevel的初始化值。

//定义外部变量
extern DDLogLevel ddLogLevel;

@interface SQLogManager : NSObject

@end
#import "SQLogManager.h"

DDLogLevel ddLogLevel = DDLogLevelDebug;

@implementation SQLogManager
@end

然后pch文件导入该头文件

#import 
#import "SQLogManager.h"

这样在外面任何的地方修改了ddLogLevel的值,就能够全局生效了。

修改输出格式

我们平常用的NSLog的输出格式是这样的

2020-05-03 16:31:07.418620+0800 CocoaLumberjackDemo[2943:118557] DDLogVerbose

DDOSLogger的输出默认是这样子的

2020-05-03 16:31:07.420348+0800 CocoaLumberjackDemo[2943:118557] DDOSLogger格式test

可以看到两个是基本一样的。
但是,很多时候我们需要修改这个东西,比如我的需求就是在我们的打印前面统一加上前缀[SQ],这样在系统的 控制台.app 里面查看log的时候一搜索就能够轻易定位到我们APP自己的输出,而不是系统或者其他的输出,排除干扰(话说系统的日志控制台的log真多,坑)。当然,更多的人想要在前面输出当前的Log所在的类名-方法名-行数等等,便于定位问题。
CocoaLumberjack提供了很好的修改输出格式的方式。

  • 首先,我们需要新建一个继承NSObject的类(比如命名为SQLogFormatter)
  • 然后遵循DDLogFormatter协议
  • 实现DDLogFormatter协议里面的- (NSString *)formatLogMessage:(DDLogMessage *)logMessage方法
    在方法里面就可以定义你所想要的格式。例如:
@implementation SQLogFormatter

 - (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 = @"Verbose";
             break;
     }
     NSString *formatLog = [NSString stringWithFormat:@"[SQ]%@%@-%ld %@",logLevel,logMessage->_function,logMessage->_line,logMessage->_message];
     return formatLog;
 }

@end

DDLogMessage这个对象里面有很多可以用到的成员变量,这些都是可以利用的。

@interface DDLogMessage : NSObject 
{
    // 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;
    NSUInteger _qos;
}
  • 接着我们需要设置DDOSLoggerlogFormatter属性
    将我们刚才创建的SQLogFormatter设置进去,就可以修改输出格式了。
    //添加控制台logger
    DDOSLogger *osLogger = [DDOSLogger sharedInstance];
    osLogger.logFormatter = [[SQLogFormatter alloc] init];
    [DDLog addLogger:osLogger];

来个对比结果如下:

2020-05-03 17:21:19.567059+0800 CocoaLumberjackDemo[3453:137167] NSLog格式test
2020-05-03 17:21:19.570009+0800 CocoaLumberjackDemo[3453:137215] [SQ]Info-[AppDelegate application:didFinishLaunchingWithOptions:]-49 DDOSLogger格式test

值得一说的是,每次打印日志之前,都会访问这个方法,所以不要在这个方法里面做太多的逻辑,不然会影响性能。

log写入到文件

之前介绍过,DDFileLogger可以将日志写入到文件。所以,添加一个DDFileLogger就可以将日志记录到文件里面了。
我们来个简单的,创建一个DDFileLogger对象,然后添加到DDLog

    //添加文件写入logger
    DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
    [DDLog addLogger:fileLogger];

默认的log日志在沙盒的Library/Caches/Logs目录中,是一个以.log为后缀的文本文件。如下图所示,下面那个就是文本文件的内容

log文件以及内容

log文件的属性自定义

根据项目需求,我们需要对log文件做很多的自定义设置。
默认情况下,log文件在多次启动的时候是会重用的,24小时内将log写入到同一个文件中,当文件大小超过1MB或者创建时间超过24小时,会新生成一个log文件,后面的log会写入到新的文件中。文件的个数超过5个的时候,会删除旧的文件。logs文件夹大小超过20M的时候,也会删除旧的文件以释放磁盘空间。
CocoaLumberjack本身提供了这些属性让我们自定义。直接代码解释:

    DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
    //重用log文件,不要每次启动都创建新的log文件(默认值是NO)
    fileLogger.doNotReuseLogFiles = NO;
    //log文件在24小时内有效,超过时间创建新log文件(默认值是24小时)
    fileLogger.rollingFrequency = 60*60*24;
    //log文件的最大3M(默认值1M)
    fileLogger.maximumFileSize = 1024*1024*3;
    //最多保存7个log文件(默认值是5)
    fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
    //log文件夹最多保存10M(默认值是20M)
    fileLogger.logFileManager.logFilesDiskQuota = 1014*1024*20;
    
    //添加文件写入logger
    [DDLog addLogger:fileLogger];

Log文件夹与文件名的获取

有些时候我们想要获取到log文件所在的路径,用来进行一些操作。比如将log文件读取出来查看,或者将log文件上传到服务器去。这些路径可以从fileLogger对象里面能够获取到。

    //logs文件夹路径
    DDLogInfo(@"logsDirectory=%@",fileLogger.logFileManager.logsDirectory);
    //logs文件夹的所有log文件路径
    DDLogInfo(@"sortedLogFilePaths=%@",fileLogger.logFileManager.sortedLogFilePaths);
    //当前活跃的log文件路径
    DDLogInfo(@"currentFilePath=%@",fileLogger.currentLogFileInfo.filePath);

log文件夹和文件名的自定义

我们项目中需要将log文件夹的内容上传到服务器。默认的Log文件存放在沙盒的Library/Caches/Logs目录中,我想要在Logs文件夹之前加一个文件夹,便于存放上传的相关设置。也就是说想要把log文件的目录改为沙盒的Library/Caches/SQLog/Logs目录中。
这个在创建DDFileogger的时候可以进行设置。

    //修改Logs文件夹的位置
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *baseDir = paths.firstObject;
    NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"SQLog/Logs"];
    DDLogFileManagerDefault *defaultManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:logsDirectory];
    
    DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:defaultManager];

还有个问题,我想要修改log文件名字,默认的log名字是 ,例如com.organization.myapp 2013-12-03 17-14.log。中间会有个空格,我担心在处理的过程中空格可能会出什么不可预知的问题,所以不想要这个空格(就看这个空格不爽,哈哈)。该如何修改这个log的名字呢。
这个就没有那么容易了,需要

  • 1.新建一个继承于DDLogFileManagerDefault的类(姑且命名为SQFileManager),
  • 2.然后重写- (NSString *)newLogFileName方法和- (BOOL)isLogFile:(NSString *)fileName方法。
#import "SQLogFileManager.h"

@implementation SQLogFileManager

//重写方法(log文件名生成规则)
- (NSString *)newLogFileName {
    
    NSString *timeStamp = [self getTimestamp];
    
    return [NSString stringWithFormat:@"%@.log", timeStamp];
}

//重写方法(是否是log文件)
- (BOOL)isLogFile:(NSString *)fileName {
    
    BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
    
    return hasProperSuffix;
}

- (NSString *)getTimestamp {
    static dispatch_once_t onceToken;
    static NSDateFormatter *dateFormatter;
    dispatch_once(&onceToken, ^{
        dateFormatter = [NSDateFormatter new];
        [dateFormatter setDateFormat:@"YYYY.MM.dd-HH.mm.ss"];
    });
    return [dateFormatter stringFromDate:NSDate.date];
}

@end

另外,在这个SQLogFileManager类里面,我们还可以重写- (NSString *)defaultLogsDirectory方法,来修改Log文件夹路径

//重写方法(log文件夹路径)
- (NSString *)defaultLogsDirectory {

#if TARGET_OS_IPHONE
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *baseDir = paths.firstObject;
    NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"SQLog/Logs"];
#else
    NSString *appName = [[NSProcessInfo processInfo] processName];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *basePath = ([paths count] > 0) ? paths[0] : NSTemporaryDirectory();
    NSString *logsDirectory = [[basePath stringByAppendingPathComponent:@"SQLog/Logs"] stringByAppendingPathComponent:appName];
#endif

    return logsDirectory;
}
  • 3.在创建fileLogger对象的时候设置我们自定义的logFileManager
    这样就可以修改文件夹目录位置和log文件名字了。
    //使用自定义的logFileManager
    SQLogFileManager *fileManager = [[SQLogFileManager alloc] init];
    DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:fileManager];

自定义log文件的log输出格式

跟之前的DDOSLogger自定义输出格式一样,我们想要自定义log文件的输出格式。我们可以用之前的SQLogFormatter类,但是,这个格式里面并没有时间戳。。。尴尬了,所以还是得自定义一个。方法跟之前一样,创建一个类,遵循DDLogFormatter协议,然后实现- (NSString *)formatLogMessage:(DDLogMessage *)logMessage方法

#import "SQFileLogformatter.h"

@implementation SQFileLogformatter

- (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 = @"Verbose";
            break;
    }
    NSString *formatLog = [NSString stringWithFormat:@"%@[SQ]%@%@-%ld %@",logMessage->_timestamp, logLevel,logMessage->_function,logMessage->_line,logMessage->_message];
    return formatLog;
}
- (NSString *)getTimeStringWithDate:(NSDate *)date {
    static NSDateFormatter *dateFormatter;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dateFormatter = [NSDateFormatter new];
        [dateFormatter setDateFormat:@"YYYY.MM.dd-HH.mm.ss.SSS"];
    });
    return [dateFormatter stringFromDate:date];
}

@end

然后设置fileLogger的formatter就可以了。

    DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:fileManager];
    fileLogger.logFormatter = [[SQFileLogformatter alloc] init];

清空日志

项目有个需求,就是测试期间需要清空文件里面的日志,便于查看后面某个操作的日志。这个功能也可以实现,只是实现起来会跟想象中不太一样。清空是一个破坏性的操作,删除一个东西之后想要找回来就不容易了。所以没有找到CocoaLumberjack有类似的接口提供。但是灵活一点,我们不就是不想看到前面的日志吗?直接生成一个新的日志文件不就可以了?清清白白、干干净净,完美。

    //生成一个新的日志文件,之后的日志写入到新的文件
    [_fileLogger rollLogFileWithCompletionBlock:^{
        DDLogInfo(@"rollLogFileWithCompletionBlock");
    }];

关闭log显示

在APP运行过程中,我们想要关闭log。
有两种方案:一种方案是将ddLogLevel的值改为DDLogLevelOff。这样对所有的Logger都会生效,不会有输出。
另外一种方案就是删除Logger。

    //删除某个Logger
    [DDLog removeLogger:osLogger];
    //删除所有Logger
    //[DDLog removeAllLoggers];

整合

经过上面的积累,我们就可以将这些功能都整合到一个管理类里面进行处理了。

#import 
#import 

//声明外部变量
extern DDLogLevel ddLogLevel;

// 默认的宏,方便使用
#define Log(frmt, ...)        LogI(frmt, ##__VA_ARGS__)

// 提供不同的宏,对应到特定参数的对外接口
#define LogE(frmt, ...)       DDLogError(frmt, ##__VA_ARGS__)
#define LogW(frmt, ...)       DDLogWarn(frmt, ##__VA_ARGS__)
#define LogI(frmt, ...)       DDLogInfo(frmt, ##__VA_ARGS__)
#define LogD(frmt, ...)       DDLogDebug(frmt, ##__VA_ARGS__)
#define LogV(frmt, ...)       DDLogVerbose(frmt, ##__VA_ARGS__)


@interface SQLogManager : NSObject

//初始化
+ (void)start;
+ (instancetype)sharedInstance;
- (instancetype)init NS_UNAVAILABLE;

//添加控制台logger
- (void)addOSLogger;
//添加文件写入Logger
- (void)addFileLogger;
//移除控制台logger
- (void)removeOSLogger;
//移除文件写入Logger
- (void)removeFileLogger;

#pragma mark - 等级切换

//切换log等级
- (void)switchLogLevel:(DDLogLevel)logLevel;

#pragma mark - 文件Log操作

- (void)createAndRollToNewFile;
//所有log文件路径
- (NSArray *)filePaths;
//当前活跃的log文件路径
- (NSString *)currentFilePath;

@end
#import "SQLogManager.h"
#import "SQLogFormatter.h"
#import "SQFileLogFormatter.h"
#import "SQLogFileManager.h"

//设置默认的log等级
#ifdef DEBUG
DDLogLevel ddLogLevel = DDLogLevelDebug;
#else
DDLogLevel ddLogLevel = DDLogLevelWarning;
#endif


@interface SQLogManager ()

@property (nonatomic, strong)DDFileLogger *fileLogger;//控制台logger
@property (nonatomic, strong)DDOSLogger *osLogger;//文件写入Logger

@end

@implementation SQLogManager

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

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




#pragma mark - Logger添加及删除

//添加控制台logger
- (void)addOSLogger {
    [DDLog addLogger:self.osLogger];
}
//添加文件写入Logger
- (void)addFileLogger {
    [DDLog addLogger:self.fileLogger];
}
//移除控制台logger
- (void)removeOSLogger {
    [DDLog removeLogger:_osLogger];
}
//移除文件写入Logger
- (void)removeFileLogger {
    [DDLog removeLogger:_fileLogger];
}


#pragma mark - 等级切换

//切换log等级
- (void)switchLogLevel:(DDLogLevel)logLevel {
        
    ddLogLevel = logLevel;
}


#pragma mark - 文件Log操作

- (void)createAndRollToNewFile {
    //achive现在的文件,新生成一个文件,并将以后的log写入新文件
    [_fileLogger rollLogFileWithCompletionBlock:^{
        NSLog(@"rollLogFileWithCompletionBlock");
    }];
}

//所有log文件路径
- (NSArray *)filePaths {
    return _fileLogger.logFileManager.sortedLogFilePaths;
}

//当前活跃的log文件路径
- (NSString *)currentFilePath {
    
    NSString *filePath = _fileLogger.currentLogFileInfo.filePath;
    return filePath;
}


#pragma mark - 懒加载

//控制台logger
- (DDOSLogger *)osLogger {
    if (!_osLogger) {
        
        _osLogger = [DDOSLogger sharedInstance];
        //自定义输出格式
        _osLogger.logFormatter = [[SQLogFormatter alloc] init];
    }
    return _osLogger;
}

//文件写入Logger
- (DDFileLogger *)fileLogger {
    if (!_fileLogger) {
        
        //使用自定义的logFileManager
        SQLogFileManager *fileManager = [[SQLogFileManager alloc] init];
        _fileLogger = [[DDFileLogger alloc] initWithLogFileManager:fileManager];
        //使用自定义的Logformatter
        _fileLogger.logFormatter = [[SQFileLogFormatter alloc] init];
        
        //重用log文件,不要每次启动都创建新的log文件(默认值是NO)
        _fileLogger.doNotReuseLogFiles = NO;
        //log文件在24小时内有效,超过时间创建新log文件(默认值是24小时)
        _fileLogger.rollingFrequency = 60*60*24;
        //log文件的最大3M(默认值1M)
        _fileLogger.maximumFileSize = 1024*1024*3;
        //最多保存7个log文件(默认值是5)
        _fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
        //log文件夹最多保存10M(默认值是20M)
        _fileLogger.logFileManager.logFilesDiskQuota = 1014*1024*10;
    }
    return _fileLogger;
}

@end

参考

本文Demo
CocoaLumberjack Github地址
浅谈iOS日志收集系统
日志库CocoaLumberjack的整合过程
iOS平台常见日志库简介

你可能感兴趣的:(【iOS】CocoaLumberJack日志库集成)