Вопрос или проблема
Я пытаюсь обновить свое приложение, чтобы использовать OSLog (Logger). Система, которую я использую в данный момент, позволяет мне использовать простую интерполяцию строк, и я ожидал того же от OSLog, но при простом тесте я вижу все виды ошибок:
import SwiftUI
import OSLog
extension Logger {
static let statistics = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NS")
}
struct MyCustom: CustomStringConvertible {
let description = "Мое пользовательское описание"
}
struct MyDebug: CustomDebugStringConvertible {
let debugDescription = "Мое отладочное описание"
}
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("инициализация")
Logger.statistics.debug("отладка инициализации")
}
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
testLogs()
}
}
}
func testLogs() {
print("структуры")
Logger.statistics.error("\(myCustom)")
// Logger.statistics.error("Это тест: \(myDebug)") // Тип выражения неясен без аннотации типа
let string = "\(myDebug)"
Logger.statistics.error("\(string)")
// Logger.statistics.error(noneOfTheAbove) // Невозможно преобразовать значение типа 'NoneOfTheAbove' к ожидаемому типу аргумента 'OSLogMessage'
// Logger.statistics.error("\(noneOfTheAbove)") // Тип выражения неясен без аннотации типа
let noneOTA = "\(noneOfTheAbove)"
// Logger.statistics.error(noneOTA) // Невозможно преобразовать значение типа 'String' к ожидаемому типу аргумента 'OSLogMessage'
Logger.statistics.error("\(noneOTA)")
// Logger.statistics.warning(optionalCustom) // Невозможно преобразовать значение типа 'MyCustom?' к ожидаемому типу аргумента 'OSLogMessage'
let optCust = "\(optionalCustom)" // Предупреждение
Logger.statistics.warning("\(optCust)")
// Logger.statistics.log("Опциональное не nil: \(optionalDebug)") // Нет точных совпадений в вызове экземплярного метода 'appendInterpolation'
let optNotNil = "\(optionalDebug)" // Предупреждение
Logger.statistics.log("\(optNotNil)")
let aPerson = Person(index: 2, name: "Джордж", 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))") // Нет точных совпадений в вызове экземплярного метода 'appendInterpolation'
}
}
Делать двойную интерполяцию строк, чтобы это работало, кажется действительно неприятным. Предупреждения были ожидаемыми, хотя я бы хотел записать несколько расширений, чтобы они исчезли, но сейчас моя основная проблема — это ошибки.
Я делаю что-то не так? Есть ли какой-то трюк к этому? Кстати, я использую эти логи только в консоли, мне не так важно их извлечение (меня устраивает хранение интерполированных строк как приватных, если это имеет значение).
Два замечания:
-
Интерполяция строк в
Logger
’sOSLogMessage
требует соответствияCustomStringConvertible
. (Смотрите ниже.) Поэтому мы обычно просто расширяем любые типы, которые мы хотим записать в журнал, чтобы они соответствовалиCustomStringConvertible
, и на этом все. Это устраняет необходимость создавать временные строки для целей журналирования. -
Проблема с примером
Person
немного другая: вы используете опциюOSLogFloatFormatting
(параметрprecision
) с нечисловым типом. Учитывая, что вы имеете дело с целочисленным типом, идея указания количества десятичных знаков не имеет смысла.
Что касается требования соответствия CustomStringConvertible
, смотрите определение интерполяции с OSLogInterpolation
:
extension OSLogInterpolation {
/// Определяет интерполяцию для значений, соответствующих CustomStringConvertible. Значения
/// отображаются с использованием методов описания на них.
///
/// Не вызывайте эту функцию напрямую. Она будет вызвана автоматически при интерполяции
/// значения, соответствующего CustomStringConvertible, в строковых интерполяциях, переданных
/// в логи API.
///
/// - Параметры:
/// - value: Интерполированное выражение, соответствующее CustomStringConvertible.
/// - align: Левое или правое выравнивание с минимальным количеством столбцов, как
/// определено типом `OSLogStringAlignment`.
/// - privacy: Квалификатор конфиденциальности, который может быть либо приватным, либо публичным.
/// По умолчанию автоподбирается.
public mutating func appendInterpolation<T>(
_ value: @autoclosure @escaping () -> T,
align: OSLogStringAlignment = .none,
privacy: OSLogPrivacy = .auto
) where T : CustomStringConvertible
…
}
Успех вашего примера MyCustom
(который единственный соответствует CustomStringConvertible
) иллюстрирует это. Также это требование соответствия CustomStringConvertible
обсуждается в видео WWDC 2020 Изучение журналирования в Swift. Но он не поддерживает CustomDebugStringConvertible
.
Теперь, кажется, элегантным решением было бы расширить OSLogInterpolation
, чтобы поддерживать интерполяцию других типов (например, CustomDebugStringConvertible
). Но, попробовав это, я получил ошибку компилятора, которая предполагает, что они решили явно запретить это:
/…/MyApp/ContentView.swift:70:53: ошибка: недопустимое сообщение журнала; расширение типов, определенных в модуле os, не поддерживается
Logger.statistics.error("MyDebug: \(myDebug)") // Тип выражения неясен без аннотации типа
^
Тем не менее, вы можете написать расширение Logger
, чтобы принимать другие значения/строки, явно устанавливая privacy
на .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)")
}
…
}
Вы можете повторить этот шаблон для warning
, log
и т. д.
В любом случае, предположим, что у вас есть экземпляр Logger
, logger
, и вы можете делать такие вещи:
logger.error(value: myCustom)
logger.error(value: myDebug)
logger.error(string: "Моя отладка: \(myDebug)")
logger.error(string: "\(noneOfTheAbove)")
logger.error(value: optionalCustom)
logger.error(value: optionalDebug)
Тем не менее, я признаю, что это не тот шаблон, который я был бы склонен использовать. Есть две проблемы:
-
Вся основная идея
OSLogMessage
(вместо того, чтобы позволять этим методам принимать параметрString
) заключается в том, чтобы иметь возможность указывать настройки конфиденциальности для отдельных значений в широком контексте сообщенияLogger
. Вы утверждаете, что вас это устраивает, но было бы жаль ненужно терять этот аспект сообщений журнала. -
Одна из моих любимых функций в Xcode 15 — возможность control-кликнуть (или щелкнуть правой кнопкой мыши) на сообщении журнала Xcode и выбрать “Перейти к источнику”. Как только вы начинаете использовать эту функцию, она становится неоценимой частью процесса отладки. (Например, “Эй, я вижу сообщение об ошибке в консоли Xcode; давайте перейдем к проблемному коду.”)
Если вы вызываете встроенные методы
Logger
, он перенаправит вас в соответствующее место в коде. Но если вы вызываете один из методов вышеуказанного расширения, Xcode перенаправит вас к расширениюLogger
, а не к месту, где ошибка была фактически зафиксирована.
Ответ или решение
Ваша проблема с использованием OSLog
(Logger) в Swift, связанная с интерполяцией строк, довольно распространена, и действительно важно понимать, как работает логирование в этом контексте.
Основные наблюдения:
-
Необходимость соответствия протоколу
CustomStringConvertible
:
Как вы уже заметили, библиотекаOSLog
требует, чтобы любые типы, которые вы хотите логиировать с помощью интерполяции строк, соответствовали протоколуCustomStringConvertible
. Это позволяетOSLog
использовать методdescription
для отображения содержимого логируемых объектов.Если вы хотите, чтобы например ваши структуры
MyCustom
,MyDebug
и другие типы могли интерполироваться в строку логов без необходимости создавать промежуточные строки, вам нужно расширить все эти типы и реализовать протоколCustomStringConvertible
. Вот пример:struct MyCustom: CustomStringConvertible { var description: String { return "My Custom description" } } struct MyDebug: CustomStringConvertible { var description: String { return "My Debug description" } }
После этого вы сможете напрямую использовать их в логах:
Logger.statistics.error("\(myCustom)") // Теперь работает корректно. Logger.statistics.error("\(myDebug)") // Теперь работает корректно.
-
Ошибка с
Person
и форматированием дробных чисел:
Проблема сPerson
заключалась в использовании параметра форматаprecision
с целочисленным значением. Так как возраст (age
) и индекс (index
) являются целочисленными, попытка указать точность для целых чисел вызывает ошибку. Вместо этого просто логируйте их как есть:Logger.statistics.debug("\(person.index) \(person.name) \(person.id) \(person.age)")
Расширение для логирования:
Чтобы решить проблему с временными строками и улучшить читаемость кода, вы можете создать расширение для Logger
, которое будет обрабатывать различные типы, включая типы, соответствующие CustomDebugStringConvertible
. Однако это накладывает ограничения, так как вы не сможете расширить поведение OSLogInterpolation
напрямую.
Пример расширения:
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)")
}
// Аналогично можно создать методы для warning и log
}
Использование расширения:
Теперь вы можете использовать расширенные методы логирования, чтобы избежать ошибок и упростить код:
logger.error(value: myCustom)
logger.error(value: myDebug)
logger.error(string: "My debug: \(myDebug)")
logger.error(string: "\(noneOfTheAbove)") // Учтите, что `noneOfTheAbove` должно соответствовать протоколу.
Заключение:
Хотя добавление таких расширений может облегчить процесс логирования, важно помнить о преимуществах, которые предоставляет OSLog
, особенно в отношении конфиденциальности и возможности отслеживания местоположения логов в коде. Возможно, стоит рассмотреть применение протоколов к вашим структурам через CustomStringConvertible
и избегать создания оберток для логирования, чтобы использовать возможность, которую предлагает система логирования, на полную мощность.