简介
日志,以及远程崩溃和错误报告框架,已经存在了一段时间。根据不同的情况,这两种框架的使用是完全不同的。
在这篇文章中,我们将介绍这两类框架的用途,包括我们的移动应用发布构建中的问题和一些建议的解决方案。我还包括一个集中的框架,它将帮助我们避免这些问题,并从日志和远程错误报告中获得最大的收益。
日志框架
首先,让我们定义一下日志和错误报告框架到底是做什么的。
有没有使用过Android中的log语句或iOS中的print语句?它们是日志框架。它们允许我们这些开发者在IDE的控制台窗口中打印几乎任何东西。
需要检查一个方法中的变量的值吗?记录它。
需要检查API响应吗?记录下来。
需要检查API的JSON解析错误吗?记录它。
需要检查Catch块中的错误异常吗?记录它。
,这样的例子还在继续。
日志最常见的用途是在调试的时候。目前,所有主要的IDE都配备了内置的调试器。它允许开发者添加断点并浏览代码。它还允许我们在浏览代码时访问变量值。
但是,大量的开发人员仍然依赖于传统的记录方法!不相信?不相信我?自己看看这些备忘录吧。

除了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定制崩溃报告的信息。
这两种实现的蓝图是这样的。
- 使用第三方日志框架创建针对构建类型的日志器
- 在发布的日志中添加我们的日志级逻辑
- 用我们的自定义语句替换传统的日志语句
安卓
为了创建特定于构建类型的日志,我们将使用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.
