Carrione & Swift

Not just iOS development articles, tips and useful resources.

Share on Twitter

Unified logging: Part 1

Published on 14 Jan 2021

Debugging is an integral part of the software development process. LLDB debugger and logging are common tools for apple platforms developers. This miniseries of articles is about logging.

We have three options for logging:

  • print function
  • NSLog function
  • Unified logging (recommended)

The unified logging uses two APIs:

  • OSLog (from iOS 10/macOS 10.12)
  • Logger (from iOS 14/macOS 11)

Print function

The print function is the simplest solution for printing something to the console. We can use it to print variables, constants, objects, or return values. It's simple, but this is only useful for debugging in the Xcode because this prints only to the Xcode console.

// print string
print("String with string interpolation: \(1 + 2)")
// prints: String with string interpolation: 3


// print constant/variable
let myConstant = 13
var myVariable = "Hi"
print(myConstant) // prints: 13
print(myVariable) // prints: Hi


// print object
let dateObject = Date()
print(dateObject) // prints: 2021-01-14 14:43:03 +0000


// print return value
func getColor() -> String {
    return "red"
}
print(getColor()) // prints: red

NSLog function

Another option is to use the NSLog function instead of the print function. NSLog function adds a timestamp and identifier to the output and prints it to the Xcode console and device. It means that this is the simplest way how to make logs outside of the Xcode. The NSLog function can use string interpolation but only from iOS 14/macOS 11. In other cases, it uses printf style with format and arguments.

From now we'll use this string constant in examples.

let str = "something"

Let's log something. We can use a printf style or a string interpolation. The second option is available from iOS 14/macOS 11.

To see all the available options for the printf style, take a look at the documentation on String Format Specifiers.

// logging with printf style (format + arguments)
NSLog("My message contains %@", str)
// prints:
// 2021-01-14 16:11:57.607612+0100 MyApp[7741:2411083] My message contains something


// logging with string interpolation (only from iOS 14/macOS 11)
NSLog("My message contains \(str)")
// prints:
// 2021-01-14 16:11:57.607652+0100 MyApp[7741:2411083] My message contains something

Nowadays, while the NSLog still works, we should use unified logging that is recommended by Apple.

Unified logging

What are the advantages of using the unified logging over the NSLog function? Thanks to the unified logging, we have the opportunity to use more parameters to filter logs in a console app. We also gain control over the display of sensitive data. And it has low performance overhead.

The unified logging provides two APIs:

  • OSLog (from iOS 10/macOS 10.12)
  • Logger (from iOS 14/macOS 11)

The Logger API isn't available for the Objective-C code. Use the OSLog API instead.

OSLog

First we need to import OSLog.

import os.log

// or

import os

In the simplest case, we can use OSLog the same way as NSLog. It means that message logging is similar. The only difference is that we use the os_log function instead of NSLog function. Here, the same rules apply regarding string interpolation as in the NSLog function, which means that we can use the string interpolation from iOS 14/macOS 11.

// log message
os_log("There is my message to the Xcode and device's consoles.")


// log something in printf style (format + arguments)
os_log("My message contains %@", str)


// log something with string interpolation (only from iOS 14/macOS 11)
os_log("My message contains \(str)")

It is advantageous to use the option to create an instance of the OSLog class. To create it, we'll need two string parameters: a subsystem and a category. The category can use predefined values: pointsOfInterest (from iOS 12/macOS 10.14), dynamicStackTracing (from iOS 13/macOS 10.15) or dynamicTracing (from iOS 13/macOS 10.15). Both parameters are convenient to filter logs in the console app.

let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "myCategory")

Now we can use our OSLog class instance in our logs. But we have to use the printf style even in iOS 14/macOS 11 and newer because the string interpolation isn't available if we don't provide a log type.

os_log("My message contains %@", log: log, str)

When we provide the log type (more information about it below) and our OSLog instance (our constant log), we can use either the printf style

// logging with a default log type and OSLog instance in printf style
os_log("My message contains %@", log: log, type: .default, str)

// other variant (from iOS 12/macOS 10.14)
os_log(.default, log: log, "My message contains %@", str)

or the string interpolation from iOS 14/macOS 11.

// logging with a fault log type, OSLog instance and the string interpolation
// (only from iOS 14/macOS 11)
os_log(.fault, log: log, "My message contains \(str)")

And last logging variant is to provide only the log type and message. In this case we can use either the printf style from iOS 12/macOS 10.14

os_log(.debug, "My message contains %@", str)

or the string interpolation from iOS 14/macOS 11.

os_log(.debug, "My message contains \(str)")

Log type

The log type determines whether the log is shown in the device's log. We can use it as another way of filtering logs in the console app. The log type provides these values:

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

The unified logging behaves slightly differently on the macOS system. The logs are stored in memory. The debug type isn't persisted to disk at all. The info type is persisted only with the use of the log command-line tool. The default, error, and fault logs are persisted after a storage limit is reached. You can override the default storage behavior of each log level using tools or custom configuration profiles. For more information on how to do so, see Customizing Logging Behavior While Debugging.

Data privacy

Why do we need to manage data privacy? The NSLog function and unified logging write logs to the device. It is useful in case of debugging outside the Xcode. But anybody who has physical access to the device can read them. It is the developer's responsibility not to show publicly sensitive data.

Data privacy in OSLog API

The OSLog API has a default behavior that all dynamic values in a device's console are private. However, of course, there is a way to secure data manually. We can set visibility with formatting options {private} or {public}.

os_log("My message contains %{private}@", log: log, type: .info, str)

In this case the console app shows content of the str string as <private>.

os_log("My message contains %{public}@", log: log, type: .info, str)

And now the console app shows content of the str string.

In the case of the app launching from Xcode, the console app shows all sensitive data.

Next

Next time, we will look at the new logger API. The second part you can find it here: Unified logging: Part 2.

If you find any inaccuracies or problems in this article please tweet at me @Carrione4. Thank you for your help.