Xcode Logger Notes

A few useful notes for using the logging system in Swift apps on Apple platforms.

This post is to primarily serve as a repository for my notes on the Explore logging in Swift video originally presented at WWDC 2020. It provides a general overview of how to use the logging system, then touches on a few specific features to keep in mind.

Additional Resources


Creating a logger object

Here are some basic rules to follow when creating a new logger object.

  1. The subsystem should be the same for every logger in the project.
  2. The category describes the part of the app the logger is reporting on.

Below is the general architecture I like to use for orgniazing my loggers in apps.

// Logger-extension.swift
import Foundation
import os

extension Logger {
    static let subsystem = "com.joshuacook.MyApplication"

    // Logger for the User model class.
    static let userLogger = Logger(
      subsystem: subsystem, 
      category: "UserModel"
    )
    // Logger for the user preferences manager class.
    static let settingsLogger = Logger(
      subsystem: subsystem,
      category: "SettingsManager"
    )
}

These logging objects can then be used in the general form shown below.

// User.swift
import os

class User {
  let logger = Logger.userLogger
  // ...
  func changeName(to newName: String) {
    logger.info("Changing user name - old: \(self.name), new: \(newName)")
    // ...
  }
}

All non-numeric objects/types included as run-time info in a log message will be redacted by default. This can be adjusted by setting the privacy inside the parentheses of the string interpolation. This is covered in the Privacy section below.

Log levels

LevelUsePersistence
Debugfor messages that are useful only during debuggingnot stored
Infohelpful but not essential for troubleshootingonly persisted during “log collect”
Notice (defualt)essential for troubleshootingpersistent up to storage limit
Errorfor errors during executionpersistent up to storage limit
Faultfor bugs in the program (unexpected errors)persistent up to storage limit

Obtaining logs

The logs of a currently running process can be seen in the Console application. There are many ways of filtering the messages to only see the ones you are interested in, including filtering by subsystem, category, and type of log message. Helpfully, these filters can be saved and reused later by clicking the “Save” button under the search bar (see the image below).

Below is an example screenshot of the log messages as I am debugging a macOS application I am currently developing.

Brief console example

The logs can be gathered from a test device using the following command. Make sure to set the start time a few minutes before seeing the bug or other event of interest.

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

The output (“app-name.logarchive” in this case) can then be opened in the Console application.

Formatting log messages

Log messages can be aligned or formatted in a variety of ways using the align and format arguments. The options for these parameters depend on the type of the input variable; here are the links to the documentation for aligning strings and formatting integers, floats, and booleans.

logger.log("\(number, align: .<methods>)")
logger.log("\(number, format: .<methods>)")

Below are some examples taken from Apple’s documentation. The first example shows how to specify the width of a variable in the message.

let shapeType: String = getShapeType()
let selectedColor: String = getSelectedColor()
logger.log("Shape type: \(shapeType, align: .right(columns: 15)) Color: \(selectedColor, align: .left(columns: 10))")

Integers can be formatted to decimals, hex, or octal values. The precision of floating point values can be refined, too.

let bigNumber = 1.0234e30
logger.log("The big number is \(bigNumber, format: .exponential(precision: 10, explicitPositiveSign: true, uppercase: false) )")

Boolean values can be formatted to values such as true/false or yes/`no.

let theAnswer = true
logger.log("The answer is \(theAnswer, format: .answer)")

Privacy

All non-numeric objects/types included as run-time info in log message will be redacted by default. This can be adjusted by setting the privacy inside the parentheses of the string interpolation.

The original string can be included using the .public privacy option. This should only be used for non-identifiable and non-sensitive data.

logger.log("my message: \(text, privacy: .public)")

Another useful option is to make a hash of the value so that it remains anonymous yet unique for debugging purposes.

logger.log("my message: \(word, privacy: .private(mask: .hash))")
Graduate Student

My research interests include cancer genetics and evolution. I also learning about programming and computer science in general.

comments powered by Disqus