Unified logging: Part 2
This article continues in our topic about logging. Today we'll look at the new API Logger that comes with iOS 14. If you are interested in what the first part of our topic is about, you can find it here: Unified logging: Part 1.
Unified logging
Let's repeat what we know from the past. Unified logging is a recommended way by Apple for logging into the device memory and log. Of course, all logs are also copied to the Xcode console in debug mode. We can work with these logs in the console app, where we can filter logs by parameters (e.g., a subsystem, category, log type, etc.). We also gain control over the display of sensitive data. And it has a low-performance overhead.
The unified logging provides two APIs:
- OSLog (from iOS 10/macOS 10.12)
- Logger (from iOS 14/macOS 11)
We discussed the first one last time. Today we take a look at the Logger API.
The Logger API is not available for the Objective-C code. Use the OSLog API instead.
Logger
First, we need to import OSLog.
import os.log
// or
import os
We need to have the Logger instance for logging. We can instantiate it in three ways:
- logger for logging to the default subsystem without any category
let defaultLogger = Logger()
- custom logger for logging to a specific subsystem and category
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Networking")
- creation a logger with an OSLog instance
let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Networking")
let logger = Logger(log)
The subsystem
usually represents an area of our application, and the category
represents its specific subarea. Now we are ready to log something.
Log types
The Logger
works with log types differently in comparison with the older OSLog
. But the log types themselves are the same:
- debug: Debug-level logs are intended for use in a development environment while actively debugging. This level will not show in the device logs.
- info: Use this log to capture information that may be helpful but is not essential for troubleshooting. This level will not show in the device logs.
- default: The default log level, which is not really telling anything about the logging. It is better to be more specific by using the other log levels. This level will show in the device logs.
- error: Error-level logs are intended for reporting critical errors and failures. This level will show in the device logs.
- fault: Fault-level messages are intended for capturing system-level or multi-process errors only. This level will show in the device logs.
Logging
The Logger provides 10 methods for logging:
- debug(_:)
- trace(_:)
- info(_:)
- log(_:)
- notice(_:)
- log(level:_:)
- error(_:)
- warning(_:)
- fault(_:)
- critical(_:)
These methods can be divided into groups by their log type.
let str = "something"
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "myCategory")
// debug-level logs
logger.debug("My message contains \(str)")
logger.trace("My message contains \(str)")
// info-level log
logger.info("My message contains \(str)")
// default-level logs
logger.log("My message contains \(str)")
logger.notice("My message contains \(str)")
// error-level logs
logger.error("My message contains \(str)")
logger.warning("My message contains \(str)")
// fault-level logs
logger.fault("My message contains \(str)")
logger.critical("My message contains \(str)")
From the above, it is clear that Logger
offers one or two methods for each log type. The methods of each type are identical.
The Logger
allows determining the type dynamically. In this case, the log method is available that accepts two parameters: log type level
and message
. Below is an example of the error level log.
logger.log(level: .error, "My message contains \(str)")
Format values in message string
The unified logging system formats interpolated variables based on the default settings, but you can apply custom formatting to your variables to make them more readable. You can:
- specify the width of a variable and align the variable’s text inside that space
- format integers as decimal, hex, or octal numbers
- format floating-point numbers using fixed-point, hex, exponential, or hybrid notation
- format boolean values as true/false or yes/no strings
- specify the precision of floating-point numbers
- specify the minimum number of digits
- specify whether a number includes an explicit plus or minus sign
- log binary data contained in a pointer
To specify a formatting option for an interpolated value, include the appropriate format parameter and value.
Examples:
// message with an alignment parameter
logger.fault("My message contains \(str, align: .right(columns: 30))")
// Log: My message contains something
// message with formated floating-point number
let double = 52.2
logger.error("My message with formated floating-point number: \(double,
format: .exponential)")
// Log: My message formated floating-point number: 5.220000e+01
// message with formated number
let bigNumber = 1.0234e12
logger.notice("My number: \(bigNumber, format: .exponential(precision: 8,
explicitPositiveSign: true,
uppercase: true))")
// Log: My number: +1.02340000E+12
// message with boolean in the yes/no format
let color = UIColor.blue
logger.notice("My color is green: \(color == UIColor.green, format: .answer)")
// Log: My color is green: NO
In addition to the preceding formatting options, the unified logging system supports custom formatting modifiers. The following example shows how to format custom bit rates, and time intervals. Both use build-in formatting modifiers for the Int32
data type, namely bitrate
or secondsSince1970
.
let rate: Int32 = Int32.random(in: 0...255)
let timeInterval = Date().timeIntervalSince1970
logger.notice("The rate is \(rate, format: .bitrate)")
logger.notice("Today is \(Int32(timeInterval), format: .secondsSince1970)")
// Logs:
// The rate is 17 bps
// Today is 2021-01-29 00:56:46+0100
For more information see: OSLogStringAlignment, OSLogIntegerFormatting, OSLogFloatFormatting, OSLogBoolFormat, OSLogInt32ExtendedFormat, and OSLogPointerFormat.
Data privacy
Log messages can contain sensitive user data. This is a potential problem for users. Why? Anyone with access to the logs can see the information. To protect the users' privacy, we need to use only our static strings and numbers. If we are in a situation that we can not avoid using sensitive data, we need to redact any values that contain sensitive user information.
The unified logging uses privacy options to hide or show interpolated variables in a message. By default, the system does not redact integer, floating-point, and boolean values, but it does redact the contents of dynamic strings and complex dynamic objects.
logger.notice("My message contains \(str)")
// Log: My message contains <private>
To make a private value public again, we need to configure the privacy of the variable in a message string or interpolated variable.
logger.notice("My message contains \(str, privacy: .public)")
// Log: My message contains something
And we can specify sensitive user information as private explicitly.
let age = 33
logger.notice("Age is: \(age, privacy: .private)")
// Log: Age is: <private>
To diagnose certain problems, you might need to identify when several log messages all refer to the same user. For example, when diagnosing issues with a particular user account, you might want to see all log messages associated with that account number. To allow this behavior and still protect user privacy, configure your privacy setting with an OSLogPrivacy.Mask.hash
value, as shown in the following example:
let accountNumber = 1234
logger.notice("Start transaction for account: \(accountNumber,
privacy: .private(mask: .hash))")
// Log: Start transaction for account <mask.hash: 'I71/ig/R9p3qjcXTsn35ug=='>
In our example, the OSLogPrivacy.Mask.hash
value corresponds to the redacted value but does not provide any identifying information about that value.
In the case of the app launching from Xcode, the console app shows all sensitive data.
If you find any inaccuracies or problems in this article please tweet at me @Carrione4. Thank you for your help.