When it comes to logging in Swift and iOS applications in particular, the APIs that first come to mind might be print and NSLog. More recently, however, Apple has introduced a new standard for logging in the form of unified logging, accessed via OSLog. It is the current recommended way of logging, providing an efficient way to capture information across our applications.

Unified logging provides a number of improvements over previous techniques and also some differences to what we are used to.

  1. Each message can be logged at an appropriate level, including: default, error, debug and info. They affect how messages are displayed to us and persisted.
  2. Messages are grouped within subsystems and categories to enable efficient searching and filtering.
  3. There is no need to wrap log statements in conditionals, due to the system being designed for performance and logs being rendered only when read.
  4. User privacy is carefully respected, with dynamic string content needing to be explicitly marked as public, else they are redacted in any logs.
A pile of logs

Let’s get logging

Using unified logging from Swift is as simple as using the os_log function, which we will quickly notice takes a StaticString as an argument rather than a regular String. The easiest way to log messages, is to place the String constant directly in the function call. Extracting the message to a property is possible, however, we will need to define it’s type as StaticString.

os_log("User signed in")

let errorMessage: StaticString = "404 - NOT FOUND"
os_log(errorMessage)

Due to the StaticString requirement, instead of using string interpolation we will need to use format arguments. This may feel jarring initially, however, it isn’t too hard to adapt to and it provides benefits to user privacy that we will discuss a bit later on. We have access to all of the standard format arguments, as well as a number of extra value type decoders. The idea is that the logging system handles as much formatting for you as possible, to make the system even more efficient.

let responseCode = "404 - NOT FOUND"
os_log("HTTP response: %@", responseCode)

let logMessage = "HTTP response: \(responseCode)"
os_log("%@", logMessage)

// error: cannot convert value of type 'String'
// to expected argument type 'StaticString'
os_log("HTTP response: \(responseCode)")

It’s good to be organised

Calls to os_log can specify an OSLog to use, containing a specific subsystem and category. This information is invaluable when filtering, searching and trying to understand our logs later. When the log argument isn’t specified a default one is used, which has no subsystem or category configured.

let uiLog = OSLog(subsystem: "com.lordcodes.chat.ChatApp", category: "UI")
os_log("Contact selected", log: uiLog)

let networkLog = OSLog(subsystem: "com.lordcodes.chat.ChatKit", category: "Network")
os_log("HTTP response: %@", log: networkLog, responseCode)

The subsystem groups all the logs for a particular app or module, allowing us to filter for all of our own logs. From evaluating Apple’s logs, the convention for subsystem is a reverse domain style, such as the Bundle Identifier of the app or framework itself. If the app is modularised into frameworks, it is a good idea to use the Bundle Identifier of the framework to split logs into their corresponding components.

Categories are used to group logs into related areas, to help us narrow down the scope of log messages. The convention for categories is to use human-readable names like UI or User. We could group logs into layers across multiple subsystems or features, such as Network or Contacts. Alternatively, we could group all the logs for a particular class, such as Contacts Repository. It would be perfectly acceptable to combine both approaches in the same project, we should simply use the most appropriate categories to allow us to understand the context of the project’s log messages.

We can add our different categories and subsystems as an extension of OSLog, making them easily accessible across the app. Storing them in one place avoids creating OSLog instances all over the codebase and helps keep the different categories in use nicely organised.

extension OSLog {
    private static var subsystem = Bundle.main.bundleIdentifier!

    static let ui = OSLog(subsystem: subsystem, category: "UI")
    static let network = OSLog(subsystem: subsystem, category: "Network")
}

os_log("Contact selected", log: .ui)
os_log("HTTP response: %@", log: .network, responseCode)

Logging levels

The unified logging system employs a set of different logging levels at which we can target different types of messages. The levels control how messages are displayed to us, how and when they are persisted and whether they are captured in different environments. How the system handles each level can even be customised through the command-line on our machine. It is a good idea to use the most appropriate logging level for each message, to get the most we can out of the logging system.

Default: To capture anything that might result in a failure and essentially a fall-back if no other level seems appropriate. Unless changed, messages are stored in memory buffers and persisted when they fill up.

Info: To capture anything that may be useful, but is not directly used to diagnose or troubleshoot errors. Unless changed, no persistence is used, messages are just stored in memory buffers and purged as they fill up.

Debug: To capture information during development to diagnose a particular issue, whilst actively debugging. They aren’t captured unless enabled by a configuration change.

Error: To capture application errors and failures, in particular anything critical. Messages are always persisted, to ensure they are no lost.

Fault: To capture system-level or multi-process errors only, likely not of use in our app code. As with the error level, messages are always persisted.

Logging at each level is as simple as specifying the corresponding OSLogType as an argument to the os_log call.

os_log("Contact selected", log: .ui, type: .info)
os_log("Saving contact failed", log: .database, type: .error)

User privacy

To ensure private user data is not accidentally persisted to application logs, which may be shared with other people, the unified logging system has a public and private argument process. By default, only scalar (boolean, integer) values are collected and dynamic strings or complex dynamic objects are redacted. If it is necessary, dynamic string arguments may be declared public and scalar arguments could also be declared private.

os_log("Contact %ld selected", 2)
os_log("Contact %{private}ld selected", 2)

os_log("HTTP response: %@", responseCode)
os_log("HTTP response: %{public}@", responseCode)

It is important we resist making all arguments public, as it could easily result in private company or user data being exposed within device logs.

Reading logs

Whilst the debugger is attached, log messages will be shown in the Xcode console. The best way to read our logs, however, is with the Console MacOS application. Here we will be able to sort, filter and search our logs, as well as view them more easily.

  • Display logs in a table, making each piece of data easy to read
  • Search and filter by subsystem and category
  • Show and hide different fields for each log message
  • Turn on and off debug and info level messages
  • Save search patterns to access them more easily in the future

Conclusion

Unified logging is a promising and powerful logging solution, especially when it comes to performance and filtering your log messages. Initially it might seem like it has quirks or differences to what you may be used to from NSLog and print. After looking a bit more and providing os_log with subsystems, categories and making good use of logging levels, you will find it makes working with logs significantly easier. If you want more log coverage with better performance, you can ditch NSLog and print statements and start integrating os_log into your apps.

OSLog is Apple’s current recommended logging approach

There is much more to OSLog than we have explored here, such as signposts to monitor app performance and so it is likely we come back to this topic in another article in the future.

Have you started using OSLog, if so what do you think of it? If you aren’t going to use it, what are your reasons for making that decision? If you have any suggestions for other developers trying to use OSLog I would love to hear about them. Please feel free to reach out to me on Twitter @lordcodes with any questions or thoughts you have, or about anything else.

If you like what you have read, please don’t hesitate to share the article and subscribe to my feed if you are interested.

Thanks for reading and happy coding! 🙏