Clear and searchable logging in Swift with OSLog
When it comes to logging in Swift and iOS applications in particular, the APIs that first come to mind might be
NSLog. More recently, however, Apple has introduced a new standard for logging in the form of unified logging, accessed via
OSLog. It is the current recommended way of logging, providing an efficient way to capture information across our applications.
Unified logging provides a number of improvements over previous techniques and also some differences to what we are used to.
- Each message can be logged at an appropriate level, including: default, error, debug and info. They affect how messages are displayed to us and persisted.
- Messages are grouped within subsystems and categories to enable efficient searching and filtering.
- There is no need to wrap log statements in conditionals, due to the system being designed for performance and logs being rendered only when read.
- User privacy is carefully respected, with dynamic string content needing to be explicitly marked as public, else they are redacted in any logs.
Before we continue: Please check out the article on my blog, Lord Codes, you will find code snippets with themed syntax highlighting and much more, it is definitely my preferred way to read it! 👍
Let’s get logging
Using unified logging from Swift is as simple as using the
os_log function, which we will quickly notice takes a
StaticString as an argument rather than a regular
String. The easiest way to log messages, is to place the
String constant directly in the function call. Extracting the message to a property is possible, however, we will need to define its type as
Due to the
StaticString requirement, instead of using string interpolation we will need to use format arguments. This may feel jarring initially, however, it isn’t too hard to adapt to and it provides benefits to user privacy that we will discuss a bit later on. We have access to all of the standard format arguments, as well as a number of extra value type decoders. The idea is that the logging system handles as much formatting for you as possible, to make the system even more efficient.
It’s good to be organised
os_log can specify an
OSLog to use, containing a specific subsystem and category. This information is invaluable when filtering, searching and trying to understand our logs later. When the log argument isn’t specified a default one is used, which has no subsystem or category configured.
The subsystem groups all the logs for a particular app or module, allowing us to filter for all of our own logs. From evaluating Apple’s logs, the convention for subsystem is a reverse domain style, such as the Bundle Identifier of the app or framework itself. If the app is modularised into frameworks, it is a good idea to use the Bundle Identifier of the framework to split logs into their corresponding components.
Categories are used to group logs into related areas, to help us narrow down the scope of log messages. The convention for categories is to use human-readable names like
User. We could group logs into layers across multiple subsystems or features, such as
Contacts. Alternatively, we could group all the logs for a particular class, such as
Contacts Repository. It would be perfectly acceptable to combine both approaches in the same project, we should simply use the most appropriate categories to allow us to understand the context of the project’s log messages.
We can add our different categories and subsystems as an extension of
OSLog, making them easily accessible across the app. Storing them in one place avoids creating
OSLog instances all over the codebase and helps keep the different categories in use nicely organised.
The unified logging system employs a set of different logging levels at which we can target different types of messages. The levels control how messages are displayed to us, how and when they are persisted and whether they are captured in different environments. How the system handles each level can even be customised through the command-line on our machine. It is a good idea to use the most appropriate logging level for each message, to get the most we can out of the logging system.
Default: To capture anything that might result in a failure and essentially a fall-back if no other level seems appropriate. Unless changed, messages are stored in memory buffers and persisted when they fill up.
Info: To capture anything that may be useful, but is not directly used to diagnose or troubleshoot errors. Unless changed, no persistence is used, messages are just stored in memory buffers and purged as they fill up.
Debug: To capture information during development to diagnose a particular issue, whilst actively debugging. They aren’t captured unless enabled by a configuration change.
Error: To capture application errors and failures, in particular anything critical. Messages are always persisted, to ensure they are no lost.
Fault: To capture system-level or multi-process errors only, likely not of use in our app code. As with the error level, messages are always persisted.
Logging at each level is as simple as specifying the corresponding
OSLogType as an argument to the
To ensure private user data is not accidentally persisted to application logs, which may be shared with other people, the unified logging system has a public and private argument process. By default, only scalar (boolean, integer) values are collected and dynamic strings or complex dynamic objects are redacted. If it is necessary, dynamic string arguments may be declared public and scalar arguments could also be declared private.
It is important we resist making all arguments public, as it could easily result in private company or user data being exposed within device logs.
Whilst the debugger is attached, log messages will be shown in the Xcode console. The best way to read our logs, however, is with the Console MacOS application. Here we will be able to sort, filter and search our logs, as well as view them more easily.
- Display logs in a table, making each piece of data easy to read
- Search and filter by subsystem and category
- Show and hide different fields for each log message
- Turn on and off debug and info level messages
- Save search patterns to access them more easily in the future
Unified logging is a promising and powerful logging solution, especially when it comes to performance and filtering your log messages. Initially it might seem like it has quirks or differences to what you may be used to from
os_log with subsystems, categories and making good use of logging levels, you will find it makes working with logs significantly easier. If you want more log coverage with better performance, you can ditch
os_log into your apps.
OSLog is Apple’s current recommended logging approach
There is much more to
OSLog than we have explored here, such as signposts to monitor app performance and so it is likely we come back to this topic in another article in the future.
Have you started using
OSLog, if so what do you think of it? If you aren’t going to use it, what are your reasons for making that decision? If you have any suggestions for other developers trying to use
OSLog I would love to hear about them. Please feel free to reach out to me on Twitter @lordcodes with any questions or thoughts you have, or about anything else.
Thanks for reading and happy coding! 🙏