移动应用程序中的日志和远程错误报告

517 阅读12分钟

简介

日志,以及远程崩溃和错误报告框架,已经存在了一段时间。根据不同的情况,这两种框架的使用是完全不同的。

在这篇文章中,我们将介绍这两类框架的用途,包括我们的移动应用发布构建中的问题和一些建议的解决方案。我还包括一个集中的框架,它将帮助我们避免这些问题,并从日志和远程错误报告中获得最大的收益。

日志框架

首先,让我们定义一下日志和错误报告框架到底是做什么的。
有没有使用过Android中的log语句或iOS中的print语句?它们是日志框架。它们允许我们这些开发者在IDE的控制台窗口中打印几乎任何东西。

需要检查一个方法中的变量的值吗?记录它。
需要检查API响应吗?记录下来。
需要检查API的JSON解析错误吗?记录它。
需要检查Catch块中的错误异常吗?记录它。
,这样的例子还在继续。

日志最常见的用途是在调试的时候。目前,所有主要的IDE都配备了内置的调试器。它允许开发者添加断点并浏览代码。它还允许我们在浏览代码时访问变量值。

但是,大量的开发人员仍然依赖于传统的记录方法!不相信?不相信我?自己看看这些备忘录吧。

Meme about debugging Drake meme about debugging

除了Java和Swift中默认提供的记录器外,还有各种建立在它们之上的日志框架。这些框架扩展了记录器的功能和用途。常见的例子有Timber(Android)Willow(iOS)CocoaLumberjack(iOS)

现在我们对什么是日志框架有了相当的了解,让我们继续讨论崩溃和错误报告框架。

崩溃和错误报告框架

我们在应用开发过程中使用日志。开发人员使用它们来访问每个阶段的变量值,识别崩溃,并调试问题。日志输出在IDE的控制台中是可见的。

那么,当应用程序已经在生产中时,如何获得错误和崩溃报告呢?

让我们考虑一个场景。你已经在你的设备上彻底测试了你的应用程序,然后在各自的商店发布了该应用程序。一些用户抱怨应用程序崩溃或功能在他们的设备上不工作。

这时你会怎么做?

因为有大量的设备制造商、操作系统、定制ROM和设备尺寸,几乎不可能在所有这些变化和组合中测试一个应用程序。这就为生产环境中可能出现的错误留下了空间。但是,当你不能访问物理设备时,你怎么能调试这些错误呢?

值得庆幸的是,有些工具可以让我们做到这一点。Firebase Crashlytics是一个流行的工具。一旦集成到一个应用程序中,它就会自动捕捉应用程序的崩溃报告,并将其保存在控制台。然后,开发人员可以很容易地访问这些日志报告,并对错误进行调试。

它还允许我们从我们的应用程序中捕获非致命的错误和日志。这些可以是API错误响应,捕捉异常,或任何我们希望记录的东西。

有什么区别呢?

如果你注意到,这两个框架中都有一些共同点。你看,日志框架和崩溃及错误报告框架的主要目的都是调试错误。主要区别在于,一个是在开发过程中使用,另一个是在生产中使用。

现在我们对这两种框架类型和它们的用途有了了解,让我们来了解一下,一旦我们开始用传统的方法使用它们,我们可能会面临哪些问题。一旦我们理解了这个问题,我们就能更好地设计出一个解决方案。

远程错误报告的问题和解决方案

问题1:在发布构建中暴露敏感的日志信息

如果你的移动应用程序已经通过了漏洞评估和渗透测试(VAPT),你可能已经遇到了这一个漏洞。"日志信息暴露了敏感信息。在生产构建中禁用记录器"。

这在开发过程中是非常常见的。我们记录API响应,捕捉错误和其他变量。我们忘记的是如何在创建生产构建之前删除这些日志命令。

如果有人将他们的设备插入电脑,观察控制台中打印的日志,他们可能会查看我们记录的所有内容。这可能包括敏感参数、整个API响应,或其他私人信息。

即使我们记得删除这些日志命令,我们也必须在整个源代码中手动删除或注释这些记录器。这是一个忙碌而重复的过程!

解决方案1:基于调试和发布环境的日志记录

通过应用程序的构建类型,无论是发布构建还是调试,我们可以控制哪些日志语句需要在控制台中打印,哪些可以被忽略。利用这一点,我们可以不用担心在生产应用中记录敏感信息的问题。

问题2:生产中的API问题和非致命错误

我们的大多数移动应用都是由来自远程API的数据驱动的。如果预期的数据结构与应用中编码的API响应不一致,依赖它的功能可能会失败。

但是,当应用程序在生产中发生类似的API结构变化时,我们的应用程序的功能将无法工作。我们怎样才能更早地知道这种情况,以便在影响到太多的用户之前发布一个修复方案?我们每天都要对应用程序的整个功能进行监控吗?我们会等着有人来报告吗?

不,我们不能这样做!我们需要的是一个流程,在这个流程中,我们可以尽快报告并获得这些问题的通知。

解决方案2:基于日志级别的远程错误报告

Firebase Crashlytics,通过其定制的错误报告,提供了一个解决方案。我们需要确定我们的日志的级别。有些可能只是信息性的,有些可能是错误,有些可能是用于调试的。

例如,API错误就属于 "错误 "类别。我们可以设计一个逻辑,将具有正确级别的日志语句作为 "错误 "分享给我们的Firebase远程错误报告。通过这种方式,我们可以跟踪那些不致命但却破坏功能的问题,并尽快解决它们。

但是,这是否意味着我们必须在整个应用中到处写这些代码?这就把我们带到了下一个问题...

问题3:分散的代码和可维护性

问题一和问题二有几个可行的解决方案。添加构建标志和使用Firebase Crashlytics进行远程错误记录。但在每个日志语句周围实施它们并不是一个好的解决方案。

我们的日志语句散落在整个应用中。在调试的时候,我们最终会将大量的日志语句释放到我们的代码中。我知道这一点,因为我也犯过这样的错误。我们不能继续围绕这些日志语句添加我们的自定义逻辑。

让我们也从代码可维护性的角度来看看。当我们想改变我们的记录器的逻辑时,会发生什么?我们要围绕着整个代码库中的每一条日志语句继续改变它吗?不可能!我们写代码是为了让用户的生活更轻松。为什么不能让我们的生活也变得更轻松呢?

解决方案3:基于构建类型和日志级别的集中式日志框架

现在,缺少的那部分。我们需要上述所有的解决方案携手并进。一个单一的类将控制基于构建类型和基于日志级别的日志,并且在代码库中的每个日志语句周围没有重复的if-else逻辑。这将避免代码的分散,有助于代码的可维护性和可扩展性。

让我们围绕日志级别和构建类型建立一个框架,包括哪些语句应该在哪里执行,什么时候执行。

日志级别日志级别--用法构建类型控制台远程日志
错误发生了一个非致命的错误,并导致应用程序的功能中断,例如,错误的JSON格式。应用程序无法解析这种格式,因此,应用程序的功能停止工作。调试✔
发布✔
警告应用程序中发生了一个意外的错误,而这个错误一开始就不应该发生,例如,在一个函数中出现了一个设备特定的异常,或者代码进入了一个没有预期的catch块。调试✔
发布✔
信息为观察应用程序的行为而添加的日志信息,例如,屏幕打开或关闭,API调用成功返回,或DB查询返回成功。调试✔
发布
调试为调试某一特定错误而添加的日志信息,例如变量值或API响应值。调试✔
发布

现在我们已经设计好了解决方案,让我们快速前进,检查在Android和iOS中的实现。

我们将使用现有的第三方日志框架,它将帮助我们在运行时根据构建类型创建日志。对于远程错误报告,我们将使用Firebase Crashlytics。你可以在这里了解更多关于用Crashlytics定制崩溃报告的信息。

这两种实现的蓝图是这样的。

  1. 使用第三方日志框架创建针对构建类型的日志器
  2. 在发布的日志中添加我们的日志级逻辑
  3. 用我们的自定义语句替换传统的日志语句

安卓

为了创建特定于构建类型的日志,我们将使用Android中最好的日志库之一。Timber。如果你已经在使用它,那太好了!如果没有,我强烈建议你使用这个库。如果没有,我强烈建议在你的项目中使用它。我们将使用Timber提供的功能创建我们基于日志级别的错误报告框架。

请注意,我将跳过Timber和Firebase Crashlytics的整合细节。最好的描述是在他们的官方网页上,我已经在这一节中链接了这些网页。

让我们开始创建我们的框架。

首先,让我们在框架初始化中实现构建型逻辑。我们将使用两个不同的记录器。一个用于调试模式,另一个用于发布模式。发布模式的日志器将是我们的自定义日志器。

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
        }
        else {
            Timber.plant(new LoggingController());
        }
    }
}

现在,让我们为上面提到的释放模式实现我们的自定义远程日志器。这将包含日志级别的逻辑。

public class LoggingController extends Timber.Tree
{
    @Override protected void log(int logLevel, String tag, @NonNull String message, Throwable t)
    {
        if (logLevel == Log.ERROR || logLevel == Log.WARN) {
            FirebaseCrashlytics.getInstance().recordException(t);
        }else{
            return;
        }
    }
}

让我们检查一下使用的例子。

Timber.d("Test debug message");
Timber.i("Test info message");
Timber.w(new RuntimeException(), "Test warning message");
Timber.e(new RuntimeException(),"Test error message");

而不是使用Log.d()Log.e() ,我们现在将不得不使用Timber.d()Timber.e() 。其余的将由我们的框架来处理!

iOS

在iOS中,为了实现构建类型的特定记录器,我们将使用Willow。它是由Nike创建的,是自定义日志器最好的Swift实现之一。

我们将使用Willow提供的功能创建我们基于日志级别的错误报告框架。

请注意,和我们之前的Android实现一样,我跳过了Willow和Firebase Crashlytics的整合细节。最好的描述是在他们的官方网页上,我在这篇文章中已经链接过了。

让我们直接开始创建我们的框架。

首先,让我们在框架配置中实现构建型逻辑。我们将使用两个不同的记录器。一个用于调试模式,另一个用于发布模式。发布模式的记录器将是我们的自定义记录器。

var logger: Logger!
public struct LoggingConfiguration {

func configure() {
        #if DEBUG
        logger = buildDebugLogger()
        #else
        logger = buildReleaseLogger()
        #endif
    }

    private func buildReleaseLogger() -> Logger {
        let consoleWriter = LoggingController.sharedInstance
        let queue = DispatchQueue(label: "serial.queue", qos: .utility)
        return Logger(logLevels: [.error,.warn], writers: [consoleWriter],executionMethod: .asynchronous(queue: queue))
    }

    private func buildDebugLogger() -> Logger {
        let consoleWriter = ConsoleWriter()
        return Logger(logLevels: [.all], writers: [consoleWriter], executionMethod: .synchronous(lock: NSRecursiveLock()))
    }
}

现在,让我们为上面提到的发布模式实现我们的自定义远程日志器。这将有日志级别的逻辑。

open class LoggingController: LogWriter{
    static public var sharedInstance = LoggingController()
    static public var attributeKey = "error"
    private init(){}

    public func writeMessage(_ message: String, logLevel: LogLevel) {
        // Since this is a release logger, we won't be using this...
    }

    public func writeMessage(_ message: LogMessage, logLevel: LogLevel) {
        if logLevel == .error || logLevel == .warn{
            if let error = message.attributes[LoggingController.attributeKey] as? Error{
                 Crashlytics.crashlytics().record(error: error)
            }
        }
    }
}
extension Error{
    func getLogMessage()->LogMessage{
        return ErrorLogMessage(name: "Error", error: self)
    }
}
struct ErrorLogMessage: LogMessage {
    var name: String
    var attributes: [String: Any]

    init(name:String,error:Error) {
        self.name = name
        self.attributes = [LoggingController.attributeKey:error]
    }
}

我们将不得不在AppDelegate 中初始化这个框架。

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        LoggingConfiguration().configure()
        return true
    }
}

你可以在这里看到使用的例子。

// Debug Logs
logger.debugMessage("Logging Debug message")

// Info Logs
logger.infoMessage("Logging Info message")

// Error & Warning Logs
let logMessage = getSampleErrorObj().getLogMessage()
logger.error(logMessage)

func getSampleErrorObj()->Error{
    let userInfo = [] // You can add any relevant error info here to help debug it
    return NSError.init(domain: NSCocoaErrorDomain, code: -1001, userInfo: userInfo)
}

因此,现在我们将不得不使用传统的print() 命令,而不是使用logger.debugMessage()logger.error() ,比如说。其他的事情都由我们的框架来处理!

总结

我们成功了!我们建立了我们的远程错误报告和日志框架。好吧,不完全是一个框架,但更像是一个 "包装 "框架,在现有库的基础上进行了扩展。

因为这是我们的自定义实现,而且整个逻辑都在一个控制器中,所以我们可以随时扩展它的能力,增加更多的过滤器和增强我们的记录器。这也会使我们的代码保持干净,有助于维护。

我希望你今天能学到一些新的、有用的东西。继续学习和建设,并祝你记录愉快

The postLogging and remote error reporting in mobile appsappeared first onLogRocket Blog.