This blog post is about how to use the new os
Logger
class in iOS 14.0+ to create a useful, privacy-friendly, scalable logging solution for Swift apps without resorting to 3rd Party logging services.
OS Logging is now preferred over NSLog
and print
statements due to multiple reasons which we outline. os_log
has also been further improved and makes OS Logging more appealing because we can support devices > iOS 10.
Release
mode, but we can't test out how we'd view the logs from an actual device).The screenshot below shows device log messages from a real iPhone 8 Plus:
The screenshot shows the Console.app
macOS Application running on a Catalina machine connected to an iPhone via USB-C (we could also use wireless debugging though). We can see timestamps and associated log messages from various iPhone processes (think of these as running apps or daemons). As we can infer from the title, this post focuses on best practices for these types of log messages (focusing on OS Logging rather than print
).
End-users are able to view these logs by simply connecting the device to a Mac and opening Console.app
. These logs cannot be viewed on the device itself, except in sysdiagnose logs.
TIP: Trigger Spotlight with CMD Space and type Cons
to jump quickly to Console.
os_log
and Logger
instead of print
statements?Apple recommend using OS Logging which is why I have used this approach rather than print
statements. The new Logger
API in iOS 14 is really easy to use with support for string interpolation. os_log
is still needed for devices running iOS 10 - 13 however.
OS Logging is also called the Unified Logging System.
print
or NSLog
. That being said, the .fault
and .error
messages have higher overhead than .debug
because they are persisted.print
statements. Thus they perform the same function but are more customisable..fault
and .error
. They are not uploaded to a server, and thus maintain user privacy..public
and .private
modifiers. This prevents malicious actors from gaining personal information even with the device in hand.os
framework?os
is a framework produced by Apple in 2014, which has been steadily been improving every year with more features. In case you have never heard of this framework, that's fine. Most iOS developers should never need to use this framework because as it's name suggests, it is a low-level library. os
is also more useful in macOS development rather than iOS.
From the os documentation:
Coordinate the scheduling and synchronization of your appโs tasks, and log information to the console to diagnose issues.
We will be using the os
framework to enable OS Logging for our apps with os_log
and Logger
.
"OS Logging sounds cool. Can you share some code?" - You
I'm glad you asked. Let's go through an example.
Create a file called LoggingService.swift
with the following code:
import os.log
@available(macOS 11.0, iOS 14.0, *)
extension Logger {
private static var subsystem = {
Bundle(for: ViewController.self).bundleIdentifier!
}()
@available(macOS 11.0, iOS 14.0, *)
static let payments = Logger(subsystem: subsystem, category: "payments")
}
extension OSLog {
static let payments: String = "payments"
}
This is an extension on Logger
that we can use for logging payment flows in our app e.g. Apple Pay. Notice how we've used the bundle of a class in our desired target:
Bundle(for: ViewController.self)
If you haven't seen .self
before that's a way of getting the type of a class. self
is an instance of the type, and Self
is the type itself. The return type of ViewController.self
is a metatype named Type
.
Below the above extension, we can define custom Debug
logging for our payments
flow:
@available(macOS 10.12, iOS 10.0, *)
func DLog(message: StaticString, file: StaticString = #file,
function: StaticString = #function, line: UInt = #line,
column: UInt = #column, type: OSLogType = .info,
bundle: Bundle = .main, category: String = OSLog.payments) {
// This method is only for iOS 14+
let customLog = OSLog(subsystem: bundle.bundleIdentifier!,
category: category)
Logger.payments.debug("File: \(file) :: Function: \(function) :: Line: \(line) :: Column: \(column) - \(message, privacy: .private)")
// Fallback on earlier versions
os_log("%@", log: customLog, type: type, ["File: \(file)", "Function: \(function)", "Line: \(line)", "Column: \(column)", message] as CVarArg)
}
Notice how we have only used Logger
for iOS 14+ and used OSLog
for versions above iOS 10.
Logger.payments.debug("File: \(file) :: Function: \(function) :: Line: \(line) :: Column: \(column) - \(message, privacy: .private)")
We could also use the os_log
method with string interpolation as below for the same log:
os_log(log: customLog, "File: \(file) :: Function: \(function) :: Line: \(line) :: Column: \(column) - \(message)")
However, this function only supports iOS 14+. We have used the string format version above because we want to support iOS 10.0+ as well rather than only iOS 14.
The older version of os_log
was cumbersome to write and also doesn't provide the same clean formatting.
OLD:
NEW:
We can see below that to print out lots of arguments, we needed to use format specifiers, and Objective-C (CVarArg
):
os_log("%@", log: customLog, type: type, ["File: \(file)", "Function: \(function)", "Line: \(line)", "Column: \(column)", message] as CVarArg)
The above function prints out the filename, function name, line number and column for every message automatically. I've added these extra bells and whistles to make it easier to debug bugs. We only need to call:
DLog(message: "Payment accepted")
to print out a log message with lots of juicy debug information.
Debug logging means the message doesn't get printed in live apps. The .private
modifier makes message
unreadable even in Debug
schemes if the Xcode debugger isn't attached. Let's now look at how to log sensitive information while protecting user privacy.
Let's now look at how to redact sensitive information for a Logger
instance and os_log
.
@available(macOS 10.12, iOS 10.0, *)
func ELog(message: StaticString, userID: Int, file: StaticString = #file,
function: StaticString = #function, line: UInt = #line,
column: UInt = #column, type: OSLogType = .error,
bundle: Bundle = .main, category: String = OSLog.payments) {
// This method is only for iOS 14+
let customLog = OSLog(subsystem: bundle.bundleIdentifier!,
category: category)
Logger.payments.error("File: \(file) :: Function: \(function) :: Line: \(line) :: Column: \(column) - \(message) :: UserID : \(userID, privacy: .private)")
// Fallback on earlier versions
os_log("%{public}@ :: UserID: %{private}d", log: customLog, type: type, ["File: \(file)", "Function: \(function)", "Line: \(line)", "Column: \(column)", message] as CVarArg, userID as CVarArg)
}
Notice how we only have the userID
marked with the .private
modifier when we log using Logger
. The newer APIs are clearer and simpler to hide sensitive information.
With the new os_log
overloads in iOS 14, we could also write:
os_log("File: \(file) :: Function: \(function) :: Line: \(line) :: Column: \(column) - \(message) :: UserID : \(userID, privacy: .private)")
The os_log
prior to iOS 14 is more cumbersome to use, as seen above:
os_log("%{public}@ :: UserID: %{private}d", log: customLog, type: type, ["File: \(file)", "Function: \(function)", "Line: \(line)", "Column: \(column)", message] as CVarArg, userID as CVarArg)
The key modifier to use is %{private}d
to redact sensitive information in the legacy API.
To see Info level and Debug level logs in console, we need to select Enable debug logging
and Enable info logging
in the Console.app
View
menu.
This won't be necessary since we are testing .error
level logs, but info logs can be helpful to view. They have a shorter lifetime in memory than .error
logs. Note that if we don't specify a public
visibility for messages, then the system decides whether to redact the information automatically.
In order to test redaction, we need to use a physical device without the debugger attached. Run the app from Xcode once, wait a few seconds, then stop the app in Xcode. Now, re-launch it by finger launching it on the home screen of the device.
Simulators are essentially debug devices, so the logs won't be redacted in the Console.
First, let's select our device on the left sidebar. We can then set the subsystem to our bundle identifier to filter through the logs.
The Logger
error message can also be hashed for sensitive information like the UserID
. Hashes can be useful to hide sensitive information but still enable us to correlate the same UserID
via the same hash value:
Logger.payments.error("File: \(file, privacy: .public) :: Function: \(function, privacy: .public) :: Line: \(line) :: Column: \(column) - \(message, privacy: .public) :: UserID : \(userID, privacy: .sensitive(mask: .hash))")
We've all been in that situation where we have a bug but can't understand how to reproduce it. In these edge cases, it can be helpful to view logs in addition to crash reports. While it could be cumbersome to ask the user for a sysdiagnose
, the advantages of protecting user privacy and adhering to GDPR outweigh the minor inconvenience IMHO.
That being said, we shouldn't log too much to avoid 3 things:
os
library (required for OS Logging) is fairly sizable and thus increases the binary size further because it is linked statically.General advice for OS logging differs from debugging locally because the logs persist in production. We need to consider several factors when OS Logging because we don't want to disable logs in production (via preprocessor directives for example).
.default
have more overhead. We need to use these levels sparingly and effectively.Console
(as we saw above), we see log entries from all device processes.There is no official guidance on this, so I will focus on what I think about when naming.
1. By feature (product-specific): For example, if you are a shopping app, maybe organise the subsystem
by the payment
, login
or other app flows. If you aren't already using a modular codebase for organisation into features (or frameworks for shared code) then I can recommend this tutorial series.
2. By technology: What I mean by this is the domain of the code, is it for Push Notifications, Deep Links, User Defaults (or persistence) logic. This could be helpful for the category
parameter.
3. By target: I like to also use the bundleIdentifier
of the current Bundle
if I can for the subsystem
. This makes even more sense if you have Extension Targets (Push Notifications), multiple Targets, or different Platforms (like WatchOS, SiriKit extensions and similar). You can get the bundle of a class using this code:
let myBundle = Bundle(for: MyClass.self)
If you are an Apple Early-Adopter then you know how buggy some of the iOS Betas can be ๐งช. When posting bug reports to Apple, the OS Logs get included in sysdiagnose
files, so you may get a faster turnaround time if your bug reports have less noise.
Depends if you can afford it and whether you really need it. I prefer to minimise the number of third-party dependencies I import in my code if I can control this. The native Apple Logging APIs are designed to be efficient and work well even without Internet access.
These services charge extra per month, but the advantage is that malicious actors cannot view the logs even if they want to by hooking the device to Console
. Note that this is NOT an issue if you apply the .private
log levels, so I don't use third-party services.
Three reasons to go for web-logging are simplicity for fetching logs, more security against reverse-engineering and lower restrictions for the amount of logging. In general, I would still recommend redacting or encrypting user data. Even if you trust the third-party vendor against data breaches, storing user data on servers is more risky. The important thing is to ensure user privacy and anonymisation as much as possible.
However, web logging is not as eco-friendly. It is possible to roll your own Logging server without a third-party, however this takes a lot of setup and is non-trivial. I personally use native logging in my apps, but I have worked on larger apps where third-party services helped speeded up bug-fixes (SwiftyBeaver).
You should find that diagnosing bugs just became a whole lot easier!
os_log(5)
and os_log(3)
man pagessysdiagnose
P.S Maybe I'll see you at WWDC 2022 ๐ or I/O 22 ๐ฑ. ๐