I'm trying to update my app to use OSLog (Logger). The system I'm currently using allows me to use simple string interpolation and I was expecting the same with OSLog, but I'm seeing all types of errors on a simple test:
import SwiftUI
import OSLog
extension Logger {
static let statistics = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NS")
}
struct MyCustom: CustomStringConvertible {
let description = "My Custom description"
}
struct MyDebug: CustomDebugStringConvertible {
let debugDescription = "My Debug description"
}
struct NoneOfTheAbove {
var defaultValue = false
}
struct Person: Identifiable {
let id = UUID()
let index: Int
let name: String
let age: Int
static let maxNameLength = 15
}
@main
struct OggTstApp: App {
let myCustom = MyCustom()
let myDebug = MyDebug()
let noneOfTheAbove = NoneOfTheAbove()
var optionalCustom: MyCustom?
var optionalDebug: MyDebug? = MyDebug()
init() {
print("init")
Logger.statistics.debug("debug init")
}
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
testLogs()
}
}
}
func testLogs() {
print("structs")
Logger.statistics.error("\(myCustom)")
// Logger.statistics.error("This is a test: \(myDebug)") // Type of expression is ambiguous without a type annotation
let string = "\(myDebug)"
Logger.statistics.error("\(string)")
// Logger.statistics.error(noneOfTheAbove) // Cannot convert value of type 'NoneOfTheAbove' to expected argument type 'OSLogMessage'
// Logger.statistics.error("\(noneOfTheAbove)") // Type of expression is ambiguous without a type annotation
let noneOTA = "\(noneOfTheAbove)"
// Logger.statistics.error(noneOTA) // Cannot convert value of type 'String' to expected argument type 'OSLogMessage'
Logger.statistics.error("\(noneOTA)")
// Logger.statistics.warning(optionalCustom) // Cannot convert value of type 'MyCustom?' to expected argument type 'OSLogMessage'
let optCust = "\(optionalCustom)" // Warning
Logger.statistics.warning("\(optCust)")
// Logger.statistics.log("Optional not nil: \(optionalDebug)") // No exact matches in call to instance method 'appendInterpolation'
let optNotNil = "\(optionalDebug)" // Warning
Logger.statistics.log("\(optNotNil)")
let aPerson = Person(index: 2, name: "George", age: 21)
let people = [aPerson]
people.forEach {
testLog($0)
}
}
func testLog(_ person: Person) {
Logger.statistics.debug("\(person.index) \(person.name) \(person.id) \(person.age)")
// Logger.statistics.debug("\(person.index) \(person.name, align: .left(columns: Person.maxNameLength)) \(person.id) \(person.age, format: .fixed(precision: 2))") // No exact matches in call to instance method 'appendInterpolation'
}
}
Dong that double string interpolation to make it work feels really painful. The warnings are kind of expected, although I wish I could write some extensions to make them go away, but for now my focus are the errors.
Am I doing something wrong? Is there a trick to this? Btw, I'm only using these logs in the console, I don't care to much about being able to retrieve them (I'm ok with keeping interpolated string values as private, in case that matters).
Two observations:
String interpolation in Logger
’s OSLogMessage
requires CustomStringConvertible
conformance. (See below.) So we would generally just extend any types that we want to log to conform to CustomStringConvertible
and be done with it. And that eliminates the need for you to create temporary strings for logging purposes.
The problem with the Person
example is a little different: You are using an OSLogFloatFormatting
option (the precision
parameter) with a non-float type. Given that you are dealing with an integer type, the idea of specifying the number of decimal places does not make sense.
Regarding the CustomStringConvertible
conformance requirement, see the definition of interpolation with the OSLogInterpolation
:
extension OSLogInterpolation {
/// Defines interpolation for values conforming to CustomStringConvertible. The values
/// are displayed using the description methods on them.
///
/// Do not call this function directly. It will be called automatically when interpolating
/// a value conforming to CustomStringConvertible in the string interpolations passed
/// to the log APIs.
///
/// - Parameters:
/// - value: The interpolated expression conforming to CustomStringConvertible.
/// - align: Left or right alignment with the minimum number of columns as
/// defined by the type `OSLogStringAlignment`.
/// - privacy: A privacy qualifier which is either private or public.
/// It is auto-inferred by default.
public mutating func appendInterpolation<T>(
_ value: @autoclosure @escaping () -> T,
align: OSLogStringAlignment = .none,
privacy: OSLogPrivacy = .auto
) where T : CustomStringConvertible
…
}
The success of your MyCustom
example (which is the only one that conforms to CustomStringConvertible
) illustrates this. Also, this CustomStringConvertible
conformance requirement is discussed in the 2020 WWDC video Exploring logging in Swift. But it does not support CustomDebugStringConvertible
.
Now, it seems that the elegant solution would be to extend OSLogInterpolation
to support interpolation of other types (e.g. CustomDebugStringConvertible
). But, having tried that, that results in is a compiler error error that suggests that they have chosen to explicitly forbid that:
/…/MyApp/ContentView.swift:70:53: error: invalid log message; extending types defined in the os module is not supported
Logger.statistics.error("MyDebug: \(myDebug)") // Type of expression is ambiguous without a type annotation
^
That having been said, you could write a Logger
extension to accept other values/strings, explicitly setting privacy
to the .private
:
import os.log
extension Logger {
public func error<T: CustomStringConvertible>(value: T) {
error("\(value, privacy: .private)")
}
public func error<T: CustomDebugStringConvertible>(value: T) {
error("\(value.debugDescription, privacy: .private)")
}
public func error(string: String) {
error("\(string, privacy: .private)")
}
…
}
You could repeat this pattern for warning
, log
, etc.
Anyway, assuming you have a Logger
instance, logger
, you could then do things like:
logger.error(value: myCustom)
logger.error(value: myDebug)
logger.error(string: "My debug: \(myDebug)")
logger.error(string: "\(noneOfTheAbove)")
logger.error(value: optionalCustom)
logger.error(value: optionalDebug)
All that having been said, I confess this is not a pattern that I would be inclined to adopt. There are two issues:
The whole motivating idea of OSLogMessage
(rather than letting these methods take a String
parameter) is to be able to specify privacy settings for individual values within the broader Logger
message. You suggest that you are OK with that, but it is a shame to unnecessarily lose this aspect of logging messages.
One of my favorite features in Xcode 15 is the ability to control-click (or right-click) on an Xcode logging message and choose “Jump to Source”. Once you start using this feature, it becomes an invaluable part of the debugging process. (E.g., “hey, I see an error reported in the Xcode console; let me jump to the offending code.”)
If you call Logger
’s built-in methods, it will take you to the appropriate point in the code. But, if you call one of the above extension methods, Xcode will take you to the Logger
extension, not where the error was actually reported.