MacKuba

Kuba Suder's blog on Mac & iOS development

Explore logging in Swift

Categories: Foundation, Logging, WWDC 20 0 comments Watch the video

Xcode 12 introduces new APIs for the unified logging system:

import os

let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards")
logger.log("Started a task")

It's now much easier to pass arguments to logs using new Swift interpolation than the old os_log API with C-style formatters:

logger.log("Started a task \(taskId)")

Like in os_log, it does not generate the resulting formatted string at the moment of logging, but stores an optimized representation and defers formatting to a later moment

You can pass any values like numeric types, ObjC types that implement description and any type conforming to CustomStringConvertible

Non-numeric data is private by default:

logger.log("Paid with bank account \(accountNr)")
// -> Paid with bank account <private>

To actually display the values, use the privacy: parameter:

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

You can also log private data in an obfuscated form, but hashed in such a way that each value gets its unique hash which you can track through a chain of logs:

logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")
// -> Paid with bank account <mask.hash:'CSvWylJ63...'>

Collecting recent logs from a connected device to a file:

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

Log levels:

  • debug – useful only during debugging
  • info – helpful but not essential for troubleshooting
  • notice (default) – essential for troubleshooting
  • error – expected errors
  • fault – unexpected errors, assumptions that weren’t true, potential bugs

The Logger has a separate method for each log level:

logger.debug(…)
logger.error(…)
// etc.

Take into account that not all logs are persisted after the app finishes execution, some can only be live streamed

debug  ⭢  not persisted by default

info  ⭢  saved to memory, only persisted if collected using log collect before they disappear

notice  ⭢  persisted up to a storage limit, older messages are purged after some time

error, fault  ⭢  persisted for a longer time than notice messages (typically a few days)

The level also affects logging performance: debug logs are the fastest, error and fault are the slowest

It’s safe to call even some slower code within log messages that will not be logged – the code isn’t run unless the log is enabled:

logger.debug("\(slowFunction(data))")

Logger provides a number of built-in formatters that let you customize how an interpolated value is displayed:

logger.log("\(cardId, align: .left(columns: 5))")
logger.log("\(seconds, format: .fixed(precision: 2))")
logger.log("\(data, format: .hex, align: .right(columns: 4))")

+ others like decimal, exponential, octal

Existing os_log(…) function also accepts the new interpolating parameters (when running on latest OS versions)