OSLog and Unified logging as recommended by Apple

OSLog as a replacement of print and NSLog is the recommended way of logging by Apple. It’s a bit harder to write, but it comes with some nice advantages compared to it’s better-known friends.

By writing a small extension you make it fairly easy to replace your print statements. Using the Console app in combination with your logs can help you debug issues in a more efficient way. OSLog has a low-performance overhead and is archived on the device for later retrieval. These are two of the advantages of using OSLog instead of print statements.

Setting up OSLog

OSLog makes it possible to log per category, which can be used to filter logs using the Console app. By defining a small extension you can easily adopt multiple categories.

Note: if you’re using iOS 14 and up, there are new APIs available explained further down this post.

import os.log

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

    /// Logs the view cycles like viewDidLoad.
    static let viewCycle = OSLog(subsystem: subsystem, category: "viewcycle")
}

This extension uses the bundle identifier of the app and creates a static instance for each category. In this case, we have a view cycle category, which we can use to log in our app:

override func viewDidLoad() {
    super.viewDidLoad()
    os_log("View did load!", log: OSLog.viewCycle, type: .info)
}

Log levels

The OSLog API requires to pass in an OSLogType which can be used to automatically send messages at the appropriate level. A log type controls the conditions under which a message should be logged and is another way of filtering in the Console app.

  • default (notice): The default log level, which is not really telling anything about the logging. It’s better to be specific by using the other log levels.
  • info: Call this function to capture information that may be helpful, but isn’t essential, for troubleshooting.
  • debug: Debug-level messages are intended for use in a development environment while actively debugging.
  • error: Error-level messages are intended for reporting critical errors and failures.
  • fault: Fault-level messages are intended for capturing system-level or multi-process errors only.

You can pass in a log level as a type parameter:

/// We're logging an .error type here as data failed to load.
os_log("Failed loading the data", log: OSLog.data, type: .error)

Logging parameters

Parameters can be logged in two ways depending on the privacy level of the log. Private data can be logged using %{private}@ and public data with %{public}@.

In the following example, we’re logging the username in both public and private to show the differences.

override func viewDidLoad() {
    super.viewDidLoad()
    os_log("User %{public}@ logged in", log: OSLog.userFlow, type: .info, username)
    os_log("User %{private}@ logged in", log: OSLog.userFlow, type: .info, username)
}

The Xcode console and the Console.app will show the data as normal when a debugger is attached.

LogExample[7784:105423] [viewcycle] User Antoine logged in
LogExample[7784:105423] [viewcycle] User Antoine logged in

However, opening the app while no debugger is attached will show the following output in the Console.app.

debug   18:58:40.532132 +0100   LogExample  User Antoine logged in
debug   18:58:40.532201 +0100   LogExample  User <private> logged in

The username is logged as <private> instead which prevents your data from being readable by anyone inside the logs.

Reading logs with the Console.app

Using the Console.app in combination with OSLog is recommended to get the most out of this way of logging.

Start by selecting your device on the left in the devices menu. Simulators and connected devices will show up in this list.

Devices menu in the Console.app
Devices menu in the Console.app

After selecting your device you can start entering a keyword in the search field, after which an option appears as any inside a drop-down menu.

This is the place in which you can filter on your category:

Category filtering
Category filtering

We could go even further if this isn’t enough filtering by passing in the subsystem:

Adding a subsystem as a filter
Adding a subsystem as a filter

Make sure to include info and debug messages by enabling them from the action menu, so all your messages show up:

Including info and debug messages to make them show up
Including info and debug messages to make them show up

This should be enough to get you started with reading logs inside the Console.app.

An example of the logs inside the Console.app
An example of the logs inside the Console.app

Saving search patterns

To make your workflow faster, you can save your most common search patterns. They will end up in the subheader to quickly filter out logs and start debugging efficiently.

Saved search patterns in the sub-header
Saved search patterns in the sub-header to quickly filter your OSLog implementation

Improved APIs in iOS 14 and up

WWDC 2020 introduced improved APIs that make it even easier to work with OSLog. The APIs look much more similar to popular frameworks like CocoaLumberjack and are better aligned with other Swift APIs.

All previous covered explanations in this blogpost still apply and the code examples are still working on iOS 14. However, if you’re supporting iOS 14 and up you might want to go with the improved APIs as they look nicer and come with a few new features.

Using a Logger instance

One of the differences is using the newly introduced Logger instance. The initialiser matches the one from OSLog:

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

    /// Logs the view cycles like viewDidLoad.
    static let viewCycle = Logger(subsystem: subsystem, category: "viewcycle")
}

The differences are visible when trying to log messages as you now have to use methods like info(_:) and debug(_:):

Logger.viewCycle.info("View did load!")

Removed restriction of static strings

A big improvements is the support for string interpolation and string literals. With the old API, it isn’t possible to use string interpolation which makes it harder to log values. With the new APIs you can log as you’re used to with print(_:) statements:

Logger.viewCycle.debug("User \(username) logged in")

Setting the right privacy level

When I used the old API it often happened that I forgot about the %{public}@ syntax. In fact, I’ve been often writing it wrong with uppercase PUBLIC, for example.

With the new API we can make use of a better discoverable enum to set the right privacy level:

Logger.viewCycle.debug("User \(username, privacy: .private) logged in")

The string interpolation support is very useful here as we can decide the privacy level per logged value.

New alignment APIs

There can be cases in which you would like to adjust the alignment of your logs a bit to improve readability. Especially when you’re logging multiple values in a line it can be useful to have certain table formatting applied.

For example, the following log statement would not be nicely aligned if printed out without any formatting:

func log(_ person: Person) {
    Logger.statistics.debug("\(person.index) \(person.name) \(person.identifier) \(person.age)")
}

/// [statistics] 14 Antoine 8DA690DD-5D97-4B53-897A-C2D98BA0440D 17.442274
/// [statistics] 54 Jaap 31C442DC-BA95-49D3-BB38-E1DD4483E124 99.916344
/// [statistics] 35 Lady 879378DB-FF29-460A-8CA4-B927233A3AA9 93.896309
/// [statistics] 97 Maaike E0A5396E-2B82-4487-86D5-597A108AE36A 9.242964
/// [statistics] 96 Jacobien BC19603E-B078-4DFB-AE36-FD7592FB2E49 59.958466

You can see that the identifier is aligned directly after the name which results in jumping alignments if the names aren’t having the same length.

We can fix this with the new alignment APIs in Swift:

func log(_ person: Person) {
    Logger.statistics.debug("\(person.index) \(person.name, align: .left(columns: Person.maxNameLength)) \(person.identifier)")
}

/// [statistics] 42 Antoine    71C6B472-6D90-45D2-A7B4-AA3B5A0FE10F 17.442274
/// [statistics] 55 Jaap       6991D0A2-D755-4527-9512-EDE0D431F460 99.916344
/// [statistics] 35 Lady       66129DE6-E874-4854-B2E0-00BBDB2A5FBB 93.896309
/// [statistics] 62 Maaike     D1984459-B67A-44BE-AC83-A43E6460C1E1 9.242964
/// [statistics] 83 Jacobien   24CD3087-91C2-4229-A337-B190D69461BA 59.958466

This improves readability and can help you to more easily digest lots of logs.

Lastly, we can format the age to only show two decimals by using the new formatting string interpolation method:

func log(_ person: Person) {
    Logger.statistics.debug("\(person.index) \(person.name, align: .left(columns: Person.maxNameLength)) \(person.identifier) \(person.age, format: .fixed(precision: 2))")
}

/// [statistics] 95 Antoine    F205DD9C-C92A-4B48-B27A-CF19C6081EB3 85.33
/// [statistics] 84 Jaap       C55C3F42-5C02-43E0-B416-2E0B7356A964 88.70
/// [statistics] 58 Lady       FD25FB54-51CA-4D6D-805E-547D29D5AE34 38.30
/// [statistics] 69 Maaike     4FDE8D73-ECBF-4015-AE5F-2AED7295D6B2 9.72
/// [statistics] 86 Jacobien   E200351B-920F-4351-9752-212912B42ECB 69.23

Further reading

WWDC often includes dedicated sessions to logging, including performance logging APIs. You can watch the sessions here:

For more in-depth documentation, check out the Apple docs on logging.

Conclusion

OSLog is the future of logging in Swift. It’s a great replacement for commonly used print statements and comes with several advantages like reading out logs from the Console app and low-performance overheads.

If you like to learn more tips on debugging, check out the debugging category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!