Logging in Swift: exploring various options

One of the important aspects of development is to fix bugs that come along. Surely the use of debugger is the best option to debug and fix the bugs but it is inevitable to use some kind of console output to see the program behaviour.

During debugging apps, it is useful to have events in their natural sequence along with the data associated with event. Using these events and associated data we can recreate the sequence and reason about where the problem occurred. This type of debugging is required when it is not possible to attach debugger e.g when an issue occurred on user device in past time.

Swift being the focus of app development on Apple platforms offers various API’s for logging. We will explore these in below sections.

NSLog()

People from Objective-C era remember the use of NSLog(). It was the most common logging function available when programming using Objective-C language. Its usage is very simple, from documentation it is declared as

void NSLog(NSString *format, ...);

The arguments are an instance NSString object along with format options variable. When used in swift, it is imported as

public func NSLog(_ format: String, _ args: CVarArg...)

A simple function call such as NSLog(@"Hello %@", @"world"); would produce an output like

2020-07-04 17:44:26.571573+0500 TestApp[26137:159243] Hello world

It’s out put is formatted and shows date along with current time stamp in system timezone.

NSLog is a handy solution but it has a downside of performance issues as it runs synchronously. Meaning it blocks the calling thread until NSLog returns. So too much calling it on main thread causes significant app lag. Although using NSLog during development has benefit, but when shipping the app and in release mode the excessive usage of NSLog will have some performance issues. In order to minimze the performance impact of NSLog we can use preprocessor directives to use it only in debug mode

#if DEBUG
#define Log(...) NSLog(__VA_ARGS__)
#else
#define Log(...)
#endif

Now our log usage will become

Log(@"Hello world");

Another aspect of NSLog is that it only works when device is connected to machine.

print() / debugPrint()

From swift documentation the print function Writes the textual representation of given items into the standard output. The print function is declared as

func print(_ items: Any..., separator: String = " ", terminator: String = "\n")

We use print by calling it with items need to be printed.

print(1, 2, 3, 4, 5)
// The output is 
1 2 3 4 5

The default separator is a space and default terminator is new line. We can easily substitute the separator and terminator parameters.

print(1, 2, 3, 4, 5, separator: ",")
// The output is
1,2,3,4,5

The debugPrint behaves similar to print but is most suitable for showing the debugging info to standard output. It is declared in Swift standard library as

func debugPrint(_ items: Any..., separator: String = " ", terminator: String = "\n")

One thing to note is that debugPrint, contrary to its name has no relation with debug or release build.

We can use print and debugPrint with custom objects as well. print uses description from CustomStringConvertible protocol and debugPrint uses debugDescription from CustomDebugStringConvertible respectively.

struct Person {
    let name: String
    let height: Int // In centimetre
}
extension Person: CustomStringConvertible {
    var description: String {
        "\(name)" + "\n" + "\(height)"
    }
}
extension Person: CustomDebugStringConvertible {
    var debugDescription: String {
        "Name:\(name)" + "\n" + "Height:\(height)"
    }
}
let person = Person(name: "Khurram", height: 173)
print(person)
// output is
Khurram
173

debugPrint(person)
// output is
Name:Khurram
Height:173

Just like NSLog, print and debugPrint are only available when device is connected to computer.

OSLog

OSLog is a unified logging system and is available starting from iOS 10.0, macOS 10.12, watchOS 3.0, tvOS 10.0. It supersedes ASL(Apple Logging System) and SysLog API’s. The unified logging system stores the log messages in memory and in data store. The data store is not a text based log files but an optimized date store for persisting the log messages.

In its simplest form we can use it as

os_log("Hello, world")

It is important to note that os_log function expects a string of type StaticString and not standard Swift String type. A StaticString is a type that is designed to represent text that is know at compile time. That means we cannot do string interpolation like

os_log(.info, log: .network, "New used logged in with name \(user.name)") // Error

Instead we can work around this problem as below

let message = "New used logged in with name \(user.name)"
os_log(.info, log: .network, "%@", message)

However the logging can be customized in many ways that will help in identifying the bugs correctly. Below are some concepts related to the unified logging system.

Subsystem

Create unique subsystem for each component of the app. A subsystem is a logical component of app e.g networking component and data persistence component are perfect candidates for a subsystems. We can create a subsystem matching bundle identifier of target.

import os.log

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

Category

If we need more granularity we can define categories within each subsystem. Categories will help in filtering the messages.

import os.log

extension OSLog {
    private static var subsystem = Bundle.main.bundleIdentifier!
    static let network = OSLog(subsystem: subsystem, category: "network")
}

Log Levels

We log messages for interesting events occurring in app using a suitable log level. A log level dictates the condition under which the messages should be logged. It is the indication of significance of a message. Below are the log level supported by API.

  • default: This is the default log level when we do not define any log level during logging a message. It is better to use and specify a log level explicitly.
  • info: Use this level when the information is helpful but is not required during debugging process.
  • debug: Use this level in active development phase while debugging.
  • error: Used when program encounters an error state such as when network load request failed.
  • fault: Used in conditions when program enters a state which prohibit it to perform any use full task i.e a system level failure.

Parameters

We can specify different parameters during logging. These parameters have two different privacy levels.

  • private: A private parameter is logged using %{private}@ specifier.
  • public: A public parameter is logged using %{public}@ specifier.

By default the privacy level for dynamic string is private.

os_log("Processing user: %{public}@", log: OSLog.network, type: .info, user.name)
os_log("Processing user: %{private}@", log: OSLog.network, type: .info, user.name)
os_log("Processing user: %@", log: OSLog.network, type: .info, user.name)

Both xcode and console app will show output when debugger is attached

2020-07-06 17:44:00.829304+0500 TestApp[715:354321] [network] Processing user: Khurram
2020-07-06 17:44:00.829425+0500 TestApp[715:354321] [network] Processing user: Khurram
2020-07-06 17:44:00.829472+0500 TestApp[715:354321] [network] Processing user: Khurram

However if debugger is not attached the console app will not show the private data.

17:44:07.565336+0500 TestApp Processing user: Khurram
17:44:07.565531+0500 TestApp Processing user: <private>
17:44:07.565557+0500 TestApp Processing user: <private>

SwiftLog

SwiftLog is an open source logging API package for Swift. This is a cross platform API that will work on Apple platforms as well as non-apple platforms(e.g Linux). To use it add

https://github.com/apple/swift-log.git

as dependency in your Swift Package Manager. SwiftLog is a front end for Swift logging API and will pick up required backend automatically depending on platform it is running on. It has also support for different log levels. Its usage is very simple.

let logger = Logger(label: Bundle.main.bundleIdentifier!)
logger.info("An important event")
logger.error("User not authenticated")
logger.critical("Something extra ordinary occurred")
// the output is
2020-07-06T18:19:22+0500 info: An important event
2020-07-06T18:19:22+0500 error: User not authenticated
2020-07-06T18:19:22+0500 critical: Something extra ordinary occurred

Logger

Logger is a new logging API from Apple and is available on platforms starting from iOS 14.0, macOS 10.16, watchOS 7.0 and tvOS 14.0 and later versions. It was revealed in WWDC 2020. Its usage semantics are similar to OSLog.

let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "network")
logger.log("A new event")
logger.info("Another event")
logger.debug("Making network call")

It has also excellent support for categories, log levels and privacy.

logger.log("New user logged in with name \(user.name, privacy: .public)")

Conclusion

  • Do not use NSLog due to its performance overhead.
  • Use print and debugPrint very occasionally.
  • Always use OSLog when developing apps, as it comes with benefits like customisation(subsystems, categories, log levels and privacy)
  • If targeting iOS 14.0, macOS 10.16, watch OS 7.0 and tvOS 14.0 use newer Logger API.
  • If developing a cross-platform product use SwiftLog.

Important Links

  • NSLog https://developer.apple.com/documentation/foundation/1395275-nslog
  • print https://developer.apple.com/documentation/swift/1541053-print
  • debugPrint https://developer.apple.com/documentation/swift/1539920-debugprint
  • OSLog https://developer.apple.com/documentation/os/logging
  • SwiftLog https://github.com/apple/swift-log

If you liked the article please consider sharing by using the share button below.

Please subscribe to mailing list to get latest updates immediately.