🪵 Replacing your logging framework with OSLog
Eager to jump on the OSLog bandwagon (Apple Unified System Log), but not sure how? Look no further!
Ever since this year’s WWDC, especially after watching the session Debug with structured logging, I’ve been super excited to stop using 3rd party log utilities. First-class citizen treatment for logs is something taken for granted for developers that use other IDEs, like Android Studio, or any other JetBrains IDE. But for developers that dedicate their professional and sometimes personal lives to building software for Apple platforms, we were still in stone age… until now!
In this article I won’t dive into details about why you’d want to use OSLog; instead, you can check the awesome features that OSLog provides in the link above, which is a summary I wrote about that WWDC session.
The information shared in this article is a summary of multiple sources, from developers of open source logging utilities, to Apple employees.
I want to use OSLog! What now?
Despite the excitement around OSLog, it's essential to understand and acknowledge its capabilities and limitations.
Apple's unified system log offers a powerful and efficient logging solution. It's designed for classification and long-term persistence, aiding in debugging issues over extended periods. The log entries are categorized by subsystem, category, and type, with customizable settings for each. This level of detail is beneficial, especially when combined with tools like the Console app and Instruments, which provide sophisticated searching and log entry correlation capabilities.
However, it's crucial to be aware of some limitations of OSLog. For example, the system log API's deep integration with the compiler makes it challenging to be wrapped efficiently. Additionally, in iOS, the system log's reading capabilities are restricted: apps can only access log entries created by their current process. This means that if an app crashes, the subsequent launch, being a different process, won't have access to the logs from the previous instance. This limitation can be a significant drawback if you’re relying on these logs to debug crashes.
Because it pretty much can’t be wrapped, this means we can’t extend its functionality beyond what Apple offers out-of-the-box. Thus, we’re left with no remote logging, no logging to multiple destinations, nor custom logging format, etc.
If you’re starting a new project now, or if you don’t have anything in place and are still using good ol’ print(…)
statements, you can probably simply start using OSLog directly.
Now, if you’re interested in those features that OSLog doesn’t offer, you’ll have to choose between your current logging framework and OSLog, unfortunately.
The middle-ground solution
As someone who has been using logs mostly to log messages to the console while debugging features/bugs, I could easily replace all my logging utilities with the OSLog API directly. However, I wouldn’t like to give up on the ability to enable other features in the future, such as remote logging, exporting logs, etc. — features usually available in robust logging frameworks.
To help solve this, I found that a good middle-ground for me was to use a function identical to the official OSLog API on release builds, and use OSLog’s actual API in debug builds. In practice, this means:
#if DEBUG
// In debug builds, we use the system-provided os_log function.
@_exported import OSLog
#else
/// A clone of OSLogType. Don't extend these types as they must match the OSLogType values.
enum MyLogType {
/// The debug log level.
case debug
/// The informative log level.
case info
/// The default log level.
case `default`
/// The error log level.
case error
/// The fault log level.
case fault
}
/// This overloaded function helps solve a compiler error when using the os_log function with an implicit logType.
/// This will be used in non-debug builds.
func os_log(
_ message: StaticString,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line,
args: Any...
) {
os_log(.default, message, file: file, function: function, line: line, args: args)
}
/// This will be used in non-debug builds.
func os_log(
_ logType: MyLogType = .default,
_ message: StaticString,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line,
args: Any...
) {
let stringMessage = String(describing: message)
switch logType {
case .debug: myCustomDebugLog(stringMessage, file: file, function: function, line: line, params: args)
case .info, .default: myCustomInfoLog(stringMessage, file: file, function: function, line: line, params: args)
case .error, .fault: myCustomErrorLog(stringMessage, file: file, function: function, line: line, params: args)
}
}
#endif
Usage will be identical between debug and release builds, e.g.:
os_log(.debug, "This is a log")
Yes, this means you’ll have to replace all those
myCustomDebugLog(…)
spread throughout your codebase withos_log(…)
, but this would need to be done only once, and then from then on you’d start using justos_log(…)
everywhere.
This will result in good usability with Xcode’s console during development (debug builds), but using your own logging framework in release builds, which may include e.g. logging to multiple destinations, exporting logs to a file, etc.
Conclusion
While OSLog brings much-needed improvements and integrations for Apple's logging ecosystem, it is not a complete replacement for third-party robust logging frameworks like CocoaLumberjack, SwiftyBeaver, XCGLogger, and many others, especially for developers requiring advanced logging features and cross-platform support. But if you’re interested in just improving the DevX and productivity while implementing and debugging features locally, there’s a good middle-ground alternative you may consider!
References
Not mentioned in this article but a good read: Exporting data from Unified Logging System in Swift