Explore logging in Swift

Written by Ammar AlTahhan

Description: Meet the latest generation of Swift unified logging APIs. Learn how to log events and errors in your app while preserving privacy. Take advantage of powerful yet readable options for formatting data — all without sacrificing performance. And we’ll show you how you can gather and process log messages to help you understand and debug unexpected behavior in your apps.

New Logging APIs

  • Record events as they happen
  • Archived on device for later retrieval
  • Low performance overhead

Adding Logs Steps:

  1. Import the os framework
import os
  1. Define a logger object with:
    • a subsystem identifier, which is used to make it clear that a message comes from our app.
    • a category, which differentiates messages coming from different parts of our app.
let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards")
  1. Add the logging action
logger.log("Started a task")

log behaves similar to a print statement, but it doesn't convert logs into Strings like print does, it optimizes the printed logs based on the type of logged data.

We can log anything that is:

  • A numeric type like Double and Int
  • Objective-C objects with -description
  • Any type that conforms to CustomStringConvertible

Nonnumeric types are redacted from the log output, to make sure that no personal information are mistakenly logged. To explicitly mark those types as safe to log, we can pass the optional parameter privacy as such:

logger.log("Ordered smoothie \(smoothieName, privacy: .public)")

Retrieving Logs

We can retrieve the logs by plugging the device into our mac and running the command:


log collect --device --start '2020-06-22 9:41:00' --output fruta.logarchive

Logs are streamlined into the Console.app when our device is connected to our mac

Log Levels

Those are the available logging levels sorted in an increasing order of their importance:

Log LevelDescriptionPersistence
DebugUseful only during debuggingNot persisted
InfoHelpful but not essential for troubleshootingPersisted only during log collect
Notice (default)Essential for troubleshootingPersisted up to a storage limit
ErrorError seen during executionPersisted up to a storage limit
FaultBug in programPersisted up to a storage limit
  • Not persisted or deleted logs can't be accessed later on. We can access not persisted logs only while streamlining logs directly from the device
  • As the log level increases, the performance of that logging decreases: Debug is the most performant log level, while Fault is the least performant
  • Because of the previous point, logging very slow functions at the Debug level is safe because the compiler will make sure that those aren't executed when the debug messages are discarded

Formatting Logs

Logger offers many APIs that we can use to format our logs, for example:

statisticsLogger.log(
    """
    \(taskID) 
    \(giftCardID, align: .left(columns: GiftCard.maxIDLength)) 
    \(serverID) 
    \(seconds, format: .fixed(precision: 2))
    """
)
  • Formatting data has no cost at run-time
  • There are many other formatting APIs and alignment options
  • It's very important to remember that we shouldn't log any private information
  • We can use the hash formatting option as a parameter to the privacy level of our logs to detect when two logged values are the same, without revealing the actual value:
logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")

Which produces an output similar to:


Paid with bank account 

API Availability

iOS 14, tvOS 14, watchOS 7, and macOS Big Sur:

  • New Logger APIs
  • New os_log() overloads that accept string interpolations

Prior releases:

  • os_log() overloads that accepted only formatted static strings

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Ammar AlTahhan

Ammar AlTahhan

Software Engineer