iOS开发中全量日志的获取

我们在app中对崩溃、卡顿、内存问题进行监控。一旦监控到问题,我们就需要记录下来,但是,很多问题的定位仅靠问题发生的那一刹那记录的信息是不够的,我们需要记录app的全量日志来获取更多的信息。

一,使用NSLog获取全量日志,通过CocoaLumberjack第三方库获取系统日志

对NSLog进行重定向采用Hook方式,因为NSLog时C的函数,使用fishHook实现重定向,具体实现如下:

static void (&orig_nslog)(NSString *format, ...);

 

void redirect_nslog(NSString *format, ...) {

    // 可以在这里先进行自己的处理

    

    // 继续执行原 NSLog

    va_list va;

    va_start(va, format);

    NSLogv(format, va);

    va_end(va);

}

 

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        struct rebinding nslog_rebinding = {"NSLog",redirect_nslog,(void*)&orig_nslog};

 

        NSLog(@"try redirect nslog %@,%d",@"is that ok?");

    }

    return

    可以看到,我在上面这段代码中,利用了fishhook 对方法的符号地址进行了重新绑定,从而

只要是NSL og的调用就都会转向redirect_ nslog 方法调用。

     在redirect_ nslog 方法中,你可以先进行自己的处理,比如将日志的输出重新输出到自己的持

久化存储系统里,接着调用NSLog也会调用的NSL _ogv方法进行原NSLog方法的调用。当

然了,你也可以使用fishhook提供的原方法调用方式orig_ _nslog, 进行原NSLog方法的调

用。上面代码里也已经声明了类orig_ nslog, 直接调用即可。

     NSL og最后写文件时的句柄是STDERR,我先前跟你说了苹果对于NSL og的定义是记录错

误的信息,STDERR的全称是standard error,系统错误日志都会通过STDERR句柄来记

录,所以NSLog最终将错误日志进行写操作的时候也会使用STDERR句柄,而dup2函数是

专门进行文件重定向的,那么也就有了另一个不使用fishhook还可以捕获NSLog日志的方

法。你可以使用dup2重定向STDERR句柄,使得重定向的位置可以由你来控制,关键代码

如下:

int fd = open(path, (O_RDWR | O_CREAT), 0644);

dup2(fd, STDERR_FILENO);

path 就是你自定义的重定向输出的文件地址。

 

二,自己创建日志文件,定期上传,获取日志信息

第三方库 https://github.com/CocoaLumberjack/CocoaLumberjack 具体查看github,现在主要说说自己创建日志文件

1。创建log类

class Log {

//创建成单利,便于全局调用

    static var shareInstance = Log()

    var writeFileQueue: DispatchQueue

    //log文件的存储路径   

    var logFile: Path {

        get {

            let now = Date()

            let fileName = "ErrorLog_\(now.year)_\(now.month)_\(now.day).txt"

            

            if !Path.cacheDir["Logs"].exists {

                _ = Path.cacheDir["Logs"].mkdir()

            }

            

            if !Path.cacheDir["Logs"][fileName].exists {

                _ = Path.cacheDir["Logs"][fileName].touch()

                

                let write = DispatchWorkItem(qos: .background, flags: .barrier) {

                    let file = FileHandle(forUpdatingAtPath: Path.cacheDir["Logs"][fileName].asString)

                    file?.seekToEndOfFile()

                    file?.write(self.getDeviceInfo().data(using: String.Encoding.utf8)!)

                }

                writeFileQueue.async(execute: write)

                

                //删除30天以前的Log文件

                if let files = Path.cacheDir["Logs"].contents {

                    let sortedFiles = files.sorted { (p1, p2) -> Bool in

                        guard let attribute1 = p1.attributes else { return false }

                        guard let attribute2 = p2.attributes else { return false }

                        if let date1 = attribute1[FileAttributeKey.creationDate] as? Date, let date2 = attribute2[FileAttributeKey.creationDate] as? Date {

                            return date1 < date2

                        } else {

                            return false

                        }

                    }

                    

                    if sortedFiles.count > 30 {

                        _ = sortedFiles.first!.remove()

                    }

                }

                

            }

            return Path.cacheDir["Logs"][fileName]

        }

    }

    

    fileprivate init() {

        writeFileQueue = DispatchQueue(label: "写日志线程", qos: DispatchQoS.default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)

        let _ = logFile

    }

    //添加日志的全局方法

    func log(message: String, toCloudKit: Bool = false) {

        let now = Date()

        let m = convertToVisiable(str: message)

        let string = "\(now.string()) : \(m)\n"

        let write = DispatchWorkItem(qos: .background, flags: .barrier) {

            let file = FileHandle(forUpdatingAtPath: self.logFile.asString)

            file?.seekToEndOfFile()

            file?.write(string.data(using: String.Encoding.utf8)!)

        }

        

        writeFileQueue.async(execute: write)

    }

    //获取当前日志的方法

    func readLog() -> String? {

//展示日志信息,添加一些项目需要的信息

        var debugStr = "BaseURL: \(BaseUrl)"

        if let registerID = PalauDefaults.registerid.value {

             debugStr += "\nRegisterID: \(registerID);"

        }

       //log文件中的内容

         if let readStr = logFile.readString() {

            debugStr += "\n \(readStr)"

        }

        return debugStr

    }

}

 

2.在全局添加日志

Log.shareInstance.log(message: “login”)

 

3.查看当前日志(今天的)展示在textview上

if let logString = Log.shareInstance.readLog() {

                let textView = UITextView(frame: CGRect(x: 0, y:0, width: 600, height: 400))

                textView.center = view.center

                view.addSubview(textView)

                textView.text = logString

                if textView.text.count > 0 {

                    let location = textView.text.count - 1

                    let bottom = NSMakeRange(location, 1)

                    textView.scrollRangeToVisible(bottom)

                }

            }

 

4通过通知方式定期上传日志文件

在AppDelegate中上传日志文件到服务器或发送日志文件到相应邮箱

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void

        ) {

            _ = SyncTask.sendLogEmail(email: email)

           

        }

        

  class func sendLogEmail(email: String) -> Promise<Void> {

        var sendEmail = "aldelo@126.com"

        if email.trim().count > 0 {

            sendEmail = email

        }

        

        let syncUrl = PalauDefaults.syncurl.value ?? ""

        

        let url = syncUrl + "/express/email/endofday"

        return firstly {

            uploadDatabase()//上传日志文件到服务器,方法实现在下边

            }.then { fileUrl in

                return Promise<Void> { seal in

                    

                    let storeName = PalauDefaults.storename.value ?? ""

                    let path = Path.temporaryDir["\(storeName)-LogFileAddress.txt"]

                    if path.exists {

                        _ = path.remove()//日志文件已经上传到服务端,删除本地的

                    }

                    let tmpPath = path.asString

                    

                    try? fileUrl.data(using: .utf8, allowLossyConversion: true)?.write(to: URL(fileURLWithPath: tmpPath))

                    

                    

                    Alamofire.upload(multipartFormData: { multipartFormData in

                        multipartFormData.append(URL(fileURLWithPath: tmpPath), withName: "attachments")

                        multipartFormData.append(sendEmail.data(using: .utf8, allowLossyConversion: true)!, withName: "emailaddress")

                    }, usingThreshold: UInt64.init(), to: url, method: .post, headers: ECTicket(), encodingCompletion: { encodingResult in

                        switch encodingResult {

                        case .success(let upload, _, _):

                            upload.responseJSON { response in

                                let json = JSON(response.data as Any)

                                if let errCode = json["err_code"].int , errCode != 0 {

                                    seal.reject(NSError(domain: json["err_msg"].stringValue, code: errCode, userInfo: nil))

                                    return

                                }

                                

                                seal.fulfill(())

                            }

                        case .failure(let encodingError):

                            print(encodingError)

                            seal.reject(encodingError)

                        }

                    })

                }

        }

    }

//上传的方法

class func uploadDatabase() -> Promise<String> {

        return Promise<String> { seal in

            DispatchQueue.global().async {

                let uploadDir = Path.cacheDir["UploadLog"]

                if !uploadDir.exists {

                    _ = uploadDir.mkdir()

                }

                

                var files = [URL]()

                

                let zipFilePath = URL(fileURLWithPath: uploadDir.toString() + “/database.zip”)//压缩文件的名字

                

                if Path.cacheDir["Logs"].exists {

                    if let contents = Path.cacheDir["Logs"].contents {

                        for file in contents {

                            files.append(URL(fileURLWithPath: file.toString()))  //添加每个日志文件路径

                        }

                    }

                }

                do {//压缩所有的日志文件

                    try Zip.zipFiles(paths: files, zipFilePath: zipFilePath, password: nil, progress: { (progress) -> () in

                        print(progress)

                    })

                } catch let error as NSError {

                    seal.reject(error)

                    return

                }

                

                let headers = CUTicket()

                let uploadUrl = BaseUrl + "/express/device/upload/localdata/" + PalauDefaults.storeID.value!

                let deviceGlobalID = PalauDefaults.terminalguid.value!

                let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""

                var deviceNumber = ""

                if let dnumber = getDeviceNumber() {

                    deviceNumber = "\(dnumber)"

                }

//以数据流的方式上传 ,默认的是上传数据的大小大于10M的时候采用数据流的方式上传

                Alamofire.upload(multipartFormData: { multipartFormData in

                    multipartFormData.append(deviceGlobalID.data(using: String.Encoding.utf8, allowLossyConversion: false)!, withName :"DeviceGlobalID")

                    multipartFormData.append(deviceNumber.data(using: String.Encoding.utf8, allowLossyConversion: false)!, withName :"DeviceNumber")

                    multipartFormData.append(version.data(using: String.Encoding.utf8, allowLossyConversion: false)!, withName :"DeviceVersion")

                    multipartFormData.append(zipFilePath, withName :"file")

                    multipartFormData.append(PalauDefaults.storeID.value!.data(using: String.Encoding.utf8)!, withName: "storeid")

                }, usingThreshold: UInt64.init(), to: uploadUrl, method: .post, headers: headers, encodingCompletion: { encodingResult in

                    switch encodingResult {

                    case .success(let upload, _, _):

                        upload.responseJSON { response in

                            guard let value = response.result.value else {

                                seal.reject(NSError(domain: "response value is Null", code: 0, userInfo: nil))

                                return

                            }

                            let json = JSON(value)

                            if let err_code = json["err_code"].int {

                                seal.reject(NSError(domain: json["err_msg"].stringValue, code: err_code, userInfo: nil))

                            } else {

                                if let url = json["url"].string {

                                    seal.fulfill(url)

                                } else {

                                    seal.reject(NSError(domain: "response value is Null", code: 0, userInfo: nil))

                                }

                            }

                        }

                    case .failure(let encodingError):

                        seal.reject(encodingError)

                    }

                })

            }

        }

以上时我们的项目中日志的使用具体流程,可以借鉴一下,实现自己的log获取方式

 

上一篇:mysql自定义函数统计订单状态


下一篇:Unsupervised Deep Image Stitching代码理解