iOS开发-CocoaLumberjack日志库实现Logger日志功能

iOS开发-Logger日志功能实现

在iOS开发中,常用CocoaLumberjack来作为日志功能实现的日志框架

一、CocoaLumberjack是什么?

CocoaLumberjack 是 支持 iOS 和 Mac 平台的日志框架,使用简单,功能强大且不失灵活,它的主要功能就是:支持不同 Level 的 Log 信息输出到各个渠道,包括苹果日志系统、Xcode 控制台、本地文件或者数据库。

二、简单实用CocoaLumberjack

在工程的Podfile文件中引入CocoaLumberjack与SSZipArchive

  pod 'SSZipArchive' # 压缩
  pod 'CocoaLumberjack'   # 日志库

CocoaLumberjack是通过DDLog的addLogger:(id )logger方法添加Log数据输出的渠道
有一下三种渠道

  • DDTTYLogger(Xcode 控制台)
  • DDASLLogger(苹果日志系统)
  • DDFileLogger(本地文件)

示例代码如下

[DDLog addLogger:[DDTTYLogger sharedInstance]]; // TTY = Xcode console
[DDLog addLogger:[DDASLLogger sharedInstance]]; // ASL = Apple System Logs

DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; // File Logger
fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
[DDLog addLogger:fileLogger];

当然日志还需要我们区分Logger Level,CocoaLumberjack也为我们提供了相应的宏定义方法

如:

DDLogVerbose(@"Verbose"); // Log 信息
DDLogDebug(@"Debug");
DDLogInfo(@"Info");
DDLogWarn(@"Warn");
DDLogError(@"Error");

三、实现Logger输出文件及archive

有时候我们遇到应用出现问题,需要将一些关键点进行埋点,将关键信息保存到文件,带需要的时候将日志archive上传到服务器,以便分析出现的问题。

定义日志输出的格式代码

SDLogFormatter.h

#import <UIKit/UIKit.h>
#import "CocoaLumberjack.h"

NS_ASSUME_NONNULL_BEGIN

@interface SDLogFormatter : NSObject<DDLogFormatter>

@end

NS_ASSUME_NONNULL_END

SDLogFormatter.m

#import "SDLogFormatter.h"

@implementation SDLogFormatter


- (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 *formatStr = [NSString stringWithFormat:@"[APP:%@ VERSION:%@] > [T:%@] > %@[%@ %@][line %lu] %@", [self appName], [self appVersion], [self currentDate],
                           logLevel, logMessage.fileName, logMessage.function,
                           (unsigned long)logMessage.line, logMessage.message];
    return formatStr;
}

#pragma mark - App常用设置
- (NSString *)appName {
    return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
}

- (NSString *)appVersion {
    return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
}

- (NSString *)currentDate {
    static NSDateFormatter *formatter = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        formatter = [[NSDateFormatter alloc] init];
        formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
    });
    return [formatter stringFromDate:[NSDate date]];
}

@end

实现日志本地输出log文件中及打包上传代码

SDLogger.h

#import <Foundation/Foundation.h>
#import "CocoaLumberjack.h"
#import "SDLogFormatter.h"
#import "SDLoggerDefine.h"
#import "SDSystemCatchCrash.h"

@interface SDLogger : NSObject

+ (instancetype)sharedInstance;

/**
 初始化logger
 */
- (void)initLogger;

/**
 删除压缩上传后的zip目录
 
 @param zipPath zipPath目录
 */
- (void)deleteZip:(NSString *)zipPath;

/**
 获取zip地址,从那一天到哪一天的日志的zip
 
 @param fromDateString fromDateString 格式为“2022-01-19”
 @param toDateString toDateString  格式为“2022-01-19”
 */
- (NSString *)getLogsZipPath:(NSString *)fromDateString toDate:(NSString *)toDateString;

/**
 沙盒document目录
 
 @return document目录
 */
- (NSString *)documentPath;

/**
 沙盒cache目录
 
 @return cache目录
 */
- (NSString *)cachePath;

/**
 临时文件路径
 
 @return 临时文件路径
 */
- (NSString *)dirTmpPath;

/**
 创建目录文件路径
 
 @return 创建目录
 */
- (NSString *)creatFolder:(NSString *)folderName at:(NSString *)dirPath;

@end

SDLogger.m

/**
 CocoaLumberjack包含几个对象分别可以把Log输出到不同的地方:

 DDASLLogger 输出到Console.app
 DDTTYLogger 输出到Xcode控制台
 DDLogFileManager 输出到文件
 DDAbstractDatabaseLogger 输出到DB
 通过ddLogLevel的int型变量或常量来定义打印等级

 LOG_LEVEL_OFF 关闭Log
 LOG_LEVEL_ERROR 只打印Error级别的Log
 LOG_LEVEL_WARN 打印Error和Warning级别的Log
 LOG_LEVEL_INFO 打印Error、Warn、Info级别的Log
 LOG_LEVEL_DEBUG 打印Error、Warn、Info、Debug级别的Log
 LOG_LEVEL_VERBOSE 打印Error、Warn、Info、Debug、Verbose级别的Log
 使用不同的宏打印不同级别的Log

 DDLogError(frmt, …) 打印Error级别的Log
 DDLogWarn(frmt, …) 打印Warn级别的Log
 DDLogInfo(frmt, …) 打印Info级别的Log
 DDLogDebug(frmt, …) 打印Debug级别的Log
 DDLogVerbose(frmt, …) 打印Verbose级别的Log
 */

#import "SDLogger.h"
#import "SSZipArchive.h"
#import "SDSystemCatchCrash.h"

static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler;

@interface SDLogger ()

@property (nonatomic, strong) SDLogFormatter *logFormatter;

@property (nonatomic, strong) DDFileLogger *fileLogger;

@end

@implementation SDLogger

+ (instancetype)sharedInstance {
    static SDLogger *logger;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        logger = [[SDLogger alloc] init];
        logger.logFormatter = [[SDLogFormatter alloc] init];
        logger.fileLogger = [[DDFileLogger alloc] init];
    });
    return logger;
}

/**
 初始化logger
 */
- (void)initLogger {
    //新版本的OSlog,输出到到Console
    if (@available(iOS 10.0, *)) {
        [[DDOSLogger sharedInstance] setLogFormatter:self.logFormatter];
        [DDLog addLogger:[DDOSLogger sharedInstance] withLevel:DDLogLevelDebug];
    } else {
        //添加输出到Console
        [[DDASLLogger sharedInstance] setLogFormatter:self.logFormatter];
        [DDLog addLogger:[DDASLLogger sharedInstance] withLevel:DDLogLevelDebug];
    } // Uses os_log
    
    //添加文件输出
    self.fileLogger.rollingFrequency = 60 * 60 * 24; // 一个LogFile的有效期长,有效期内Log都会写入该LogFile
    self.fileLogger.logFileManager.maximumNumberOfLogFiles = 7;//最多LogFile的数量
    [self.fileLogger setLogFormatter:self.logFormatter];
    [DDLog addLogger:self.fileLogger withLevel:DDLogLevelInfo];
    
    //保存之前的Handler
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    // 将下面C函数的函数地址当做参数
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
}

/**
 删除压缩上传后的zip目录
 
 @param zipPath zipPath目录
 */
- (void)deleteZip:(NSString *)zipPath {
    // 判断文件夹是否存在 不存在创建
    BOOL exits = [[NSFileManager defaultManager] fileExistsAtPath:zipPath];
    if (exits) {
        // 文件存在, 删除文件
        NSError *deError;
        [[NSFileManager defaultManager] removeItemAtPath:zipPath error:&deError];
        NSLog(@"deleteZipPath:%@", deError);
    }
}

/**
 获取zip地址,从那一天到哪一天的日志的zip
 
 @param fromDateString fromDateString
 @param toDateString toDateString
 */
- (NSString *)getLogsZipPath:(NSString *)fromDateString toDate:(NSString *)toDateString {
    if (!((fromDateString && [fromDateString isKindOfClass:[NSString class]] && fromDateString.length > 0) &&
        (toDateString && [toDateString isKindOfClass:[NSString class]] && toDateString.length > 0))) {
        return nil;
    }
    
    NSDate *fromDate = [[self dateFormatter] dateFromString:fromDateString];
    NSDate *toDate = [[self dateFormatter] dateFromString:toDateString];
    NSArray *dateList = [self getMonthArrayWithBeginDate:fromDate EndDate:toDate];
    NSLog(@"dateList:%@", dateList);
    
    // 根据日期,获取时间间隔from、to之间的log,将其存到tmp或者cache目录后中的tmpLog目录,将tmpLog目录压缩到tmp或者cache目录后的ziplog目录
    NSArray *logFilePaths = [self.fileLogger.logFileManager sortedLogFilePaths];
    NSArray *logFileNames = [self.fileLogger.logFileManager sortedLogFileNames];
    
    NSLog(@"logFilePaths:%@", logFilePaths);
    NSLog(@"logFileNames:%@", logFileNames);

    // 创建zip日志目录
    NSString *zipDir = [self creatFolder:@"logZips" at:[self cachePath]];
    
    // 创建tmpLog目录
    NSString *tmpLogDir = [self creatFolder:@"tmpLog" at:[self cachePath]];
    
    NSMutableArray *toFilePaths = [NSMutableArray arrayWithCapacity:0];

    for (NSString *day in dateList) {
        for (NSString *filePath in logFilePaths) {
            // filePath
            NSString *content = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
            NSLog(@"content:%@", content);

            if ([filePath containsString:day]) {
                [toFilePaths addObject:filePath];
            }
        }
    }
    
    for (NSString *filePath in toFilePaths) {
        NSString *fileName = [filePath lastPathComponent];
        NSString *toFilePath = [tmpLogDir stringByAppendingPathComponent:fileName];
        
        // 判断文件夹是否存在 不存在创建
        BOOL exits = [[NSFileManager defaultManager] fileExistsAtPath:toFilePath];
        if (exits) {
            // 文件存在, 删除文件
            NSError *dError;
            [[NSFileManager defaultManager] removeItemAtPath:toFilePath error:&dError];
            NSLog(@"removeItemAtPath:%@", dError);
        }

        BOOL copyResult = [[NSFileManager defaultManager] copyItemAtPath:filePath toPath:toFilePath error:nil];
        NSLog(@"copyItemAtPath:%@", (copyResult?@"YES":@"NO"));
    }

    //获取日志本地路径
    NSString *zipNameString = [NSString stringWithFormat:@"%@-%@.zip",fromDateString, toDateString];
    NSString *zipPath = [zipDir stringByAppendingPathComponent:zipNameString];
    [self deleteZip:zipPath];
    
    //压缩用户日志
    BOOL success = [SSZipArchive createZipFileAtPath:zipPath withContentsOfDirectory:tmpLogDir withPassword:nil];
    if (success) {
        // 压缩成功
        return zipPath;
    }
    
    return nil;
}

/**
 沙盒document目录
 
 @return document目录
 */
- (NSString *)documentPath {
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    return path;
}

/**
 沙盒cache目录
 
 @return cache目录
 */
- (NSString *)cachePath {
    NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
    return path;
}

/**
 临时文件路径
 
 @return 临时文件路径
 */
- (NSString *)dirTmpPath {
    return  NSTemporaryDirectory();
}

/**
 创建目录文件路径
 
 @return 创建目录
 */
- (NSString *)creatFolder:(NSString *)folderName at:(NSString *)dirPath {
    
    NSString *temDirectory = [dirPath stringByAppendingPathComponent:folderName];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    // 创建目录
    BOOL res=[fileManager createDirectoryAtPath:temDirectory withIntermediateDirectories:YES attributes:nil error:nil];
    if (res) {
        return temDirectory;
    }else{
        return temDirectory;
    }
}

#pragma mark - Private
- (NSMutableArray *)getMonthArrayWithBeginDate:(NSDate *)beginDate EndDate:(NSDate *)endDate {
    // 计算两个时间的差值
    NSCalendar *calendar = [NSCalendar currentCalendar];
    // 需要对比的时间数据 NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
    NSCalendarUnit unit = NSCalendarUnitDay;
    // 对比时间差
    NSDateComponents *dateCom = [calendar components:unit fromDate:beginDate toDate:endDate options:0];
    
    // 两个日期之间所有天数组成的数组
    NSMutableArray *allDayArr = [NSMutableArray new];
    for (int i = 0 ; i < dateCom.day+1 ; i ++) {
        // n天后的天数
        int days = i;
        // 一天一共有多少秒
        NSTimeInterval oneDay = 24 * 60 * 60;
        // 指定的日期
        NSDate *appointDate = [NSDate dateWithTimeInterval:oneDay * days sinceDate:beginDate];
        // 转字符串
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy-MM-dd"]; // @"yyyy-MM-dd"
        NSString *appointTime = [dateFormatter stringFromDate:appointDate];
        [allDayArr addObject:appointTime];
    }
    return allDayArr;
}

- (NSDateFormatter *)dateFormatter {
    static NSDateFormatter *formatter = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        formatter = [[NSDateFormatter alloc] init];
        formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        formatter.dateFormat = @"yyyy-MM-dd";
    });
    return formatter;
}

@end

四、获取crash一场并且输出到日志

当应用出现奔溃时候,出现crash,需要获取crash的错误信息,输出到日志Logger中。

这里使用的是uncaughtExceptionHandler方法

SDSystemCatchCrash.h

#import <Foundation/Foundation.h>
#import "SDLoggerDefine.h"

NS_ASSUME_NONNULL_BEGIN

@interface SDSystemCatchCrash : NSObject

void uncaughtExceptionHandler(NSException *exception);

@end

NS_ASSUME_NONNULL_END

SDSystemCatchCrash.m

#import "SDSystemCatchCrash.h"
#import "CocoaLumberjack.h"

@implementation SDSystemCatchCrash

//在AppDelegate中注册后,程序崩溃时会执行的方法
void uncaughtExceptionHandler(NSException *exception)
{
    //获取系统当前时间,(注:用[NSDate date]直接获取的是格林尼治时间,有时差)
    NSDateFormatter *formatter =[[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    NSString *crashTime = [formatter stringFromDate:[NSDate date]];
    //异常的堆栈信息
    NSArray *stackArray = [exception callStackSymbols];
    //出现异常的原因
    NSString *reason = [exception reason];
    //异常名称
    NSString *name = [exception name];

    //拼接错误信息
    NSString *exceptionInfo = [NSString stringWithFormat:@"crashTime: %@ Exception reason: %@\nException name: %@\nException stack:%@", crashTime, name, reason, stackArray];

    DDLogError(@"exceptionInfo:%@", exceptionInfo);
    //把错误信息保存到本地文件,设置errorLogPath路径下
    //并且经试验,此方法写入本地文件有效。
//    NSString *errorLogPath = [NSString stringWithFormat:@"%@/Documents/error.log", NSHomeDirectory()];
//    NSError *error = nil;
//    BOOL isSuccess = [exceptionInfo writeToFile:errorLogPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
//    if (!isSuccess) {
//        DLog(@"将crash信息保存到本地失败: %@", error.userInfo);
//    }
}

@end

五、为了方便使用不同level Logger输出

为了方便使用不同level Logger输出,这里使用宏定义方法

SDLoggerDefine.h

#ifndef SDLoggerDefine_h
#define SDLoggerDefine_h

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import "DDLog.h"
#endif

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

#define SDErrorLogger(frmt, ...)   LOG_MAYBE(NO,                LOG_LEVEL_DEF, DDLogFlagError,   0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define SDWarnLogger(frmt, ...)    LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagWarning, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define SDInfoLogger(frmt, ...)    LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagInfo,    0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define SDDebugLogger(frmt, ...)   LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagDebug,   0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define SDVerboseLogger(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)

#endif /* SDLoggerDefine_h */

六、小结

iOS开发-Logger日志功能实现。常用CocoaLumberjack来作为日志功能实现的日志框架,输出到DDTTYLogger(Xcode 控制台)、DDASLLogger(苹果日志系统)、DDFileLogger(本地文件)。通过不同的Logger Level如Verbose、Debug、Info、Warn、Error等日志输出到日志以便分析出现问题的代码。

学习记录,每天不停进步。

你可能感兴趣的:(移动开发,iphone开发,Objective-c,ios,cocoa,macos,CocoaLumberjack,Logger,控制台,写入文件)