Why you should use Logger over print for logging your app’s data.
I’m sure many of you had this problem when you stick some logging into your code and suddenly the problem stops happening because you’ve changed the timing. You want a very fast logging interface that has minimal impact on your code. And another thing you should take into account is privacy.
Apple’s unified Logging API, also know as os_log makes our lives easier. Apple introduced os_log in 2016 and Logger in 2020. If the device is connected to your Mac, you can stream log messages as they happen in Console app. If your app is launched from Xcode, you will also see them in Xcode’s console. Also you can collect logs after you app has finished running.
It will help you to correct the bug when you cannot reproduce it
When your app logs a message, the operating system stores it on device in a compressed form. You can use the “log collect” command on your Mac to retrieve those logs:
- Connect your device to your Mac
- Run the “log collect” command from the terminal with device option. Start time from when you need logs and provide file name for storing the log.
- Open log file in Console app. You can browse and filter here logs and understand what was the cause of the bug.
Filter and categorize Logs
When you create a Logger
structure, assign an optional subsystem and category string to add context to all messages you log. A subsystem corresponds to a large functional area of your app, and a category corresponds to a specific area within a particular subsystem. When diagnosing problems, use those strings to filter out unrelated messages.
let logger = Logger(subsystem: "com.example.Wallet", category: "networking")
Log Levels:
You can choose importance of your message from these log levels. The error and fault levels are highlighted with yellow and red bubbles in Console app.
logger.log(level: .error, "Network error")
Persistence
Debug: not persisted, which means they cannot be retrieved after the app has completed execution.
Info: mostly not persisted, except when they are generated a few moments before a log collect command.
Messages logged at every other level are persisted, and you can retrieve them later.
There is, however, a storage limit on how many messages are archived. Once that limit is exceeded, the older ones are purged and become unavailable. The error and fault level messages are persisted even longer than notice level messages. Typically, the messages will be persisted for a few days. However, it depends on the storage space on your device.
The thing you should keep in mind: the log levels also affect performance.
Even though logging in general has low overhead, the log levels have different performance relative to each other. The levels that are less important are faster. The fault level is the slowest, and the debug level is the most performant.
Logging at the debug level is so fast because debug messages are not persisted at all. They are discarded when the logs aren’t being streamed. Further, the Swift compiler uses sophisticated optimisations to ensure that the code that creates the messages is not even executed when the debug messages are discarded. This means that you can log verbose messages at the debug level and call expensive functions to construct messages. Your users won’t pay the cost for them. Which is really cool thing, you don’t need to worry about performance anymore.
Privacy
You can use privacy options to control the visibility of data in the logs. It is really important to take seriously the privacy of the logged data. This is because logging happens all the time, even after your app has shipped and is in the hands of your users. Logs can be collected by anyone who has physical access to the device and also its passcode. Therefore, it is important that the log messages do not mark any personal information public, which could expose it in the logs. You can also choose .private with hash function, this will help you see when two messages are equal without revealing their values.
logger.log("Request: \(request.body, privacy: .private)")
logger.log("Account Number: \(accountNumber, privacy: .private(mask: .hash))")
String interpolation
You can log runtime data in the message. Unlike with print, the log message is not fully converted to a string, as that would be too slow. Instead, the compiler and the logging library work together to produce a heavily optimised representation of the log message that leverages the type of the logged data. With the optimised representation, you pay the cost of converting to a string only when the log message is actually displayed. Log messages can contain a wide variety of data types. You can log numeric types like Int and Double, Objective-C objects, as well as any type that conforms to Swift’s CustomStringConvertible protocol. This means to add your own type to a log message, all you need to do is make it conform to CustomStringConvertible.
Formatting
You can use the optional “format” and “align” parameter to format data. Since formatting data using the Logging APIs doesn’t add to the cost of a log call, you can use formatting as much as you like to make your data look pretty and easy to understand.
You can see the full range of options using Xcode’s code completion, including formatting numbers as hexadecimal, octal, exponential, and others.
Benefits using Logger over print are privacy, string interpolation, low performance overhead, different logging levels for different usage, formatting without a cost, easy filtering and persistence.