OSLog (Логгер) выдает ошибки с простой интерполяцией строк

Вопрос или проблема

Я пытаюсь обновить свое приложение, чтобы использовать 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'
    }
}

Делать двойную интерполяцию строк, чтобы это работало, кажется действительно неприятным. Предупреждения были ожидаемыми, хотя я бы хотел записать несколько расширений, чтобы они исчезли, но сейчас моя основная проблема — это ошибки.

Я делаю что-то не так? Есть ли какой-то трюк к этому? Кстати, я использую эти логи только в консоли, мне не так важно их извлечение (меня устраивает хранение интерполированных строк как приватных, если это имеет значение).

Два замечания:

  1. Интерполяция строк в Logger’s OSLogMessage требует соответствия CustomStringConvertible. (Смотрите ниже.) Поэтому мы обычно просто расширяем любые типы, которые мы хотим записать в журнал, чтобы они соответствовали CustomStringConvertible, и на этом все. Это устраняет необходимость создавать временные строки для целей журналирования.

  2. Проблема с примером 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)

Тем не менее, я признаю, что это не тот шаблон, который я был бы склонен использовать. Есть две проблемы:

  1. Вся основная идея OSLogMessage (вместо того, чтобы позволять этим методам принимать параметр String) заключается в том, чтобы иметь возможность указывать настройки конфиденциальности для отдельных значений в широком контексте сообщения Logger. Вы утверждаете, что вас это устраивает, но было бы жаль ненужно терять этот аспект сообщений журнала.

  2. Одна из моих любимых функций в Xcode 15 — возможность control-кликнуть (или щелкнуть правой кнопкой мыши) на сообщении журнала Xcode и выбрать “Перейти к источнику”. Как только вы начинаете использовать эту функцию, она становится неоценимой частью процесса отладки. (Например, “Эй, я вижу сообщение об ошибке в консоли Xcode; давайте перейдем к проблемному коду.”)

    Если вы вызываете встроенные методы Logger, он перенаправит вас в соответствующее место в коде. Но если вы вызываете один из методов вышеуказанного расширения, Xcode перенаправит вас к расширению Logger, а не к месту, где ошибка была фактически зафиксирована.

Ответ или решение

Ваша проблема с использованием OSLog (Logger) в Swift, связанная с интерполяцией строк, довольно распространена, и действительно важно понимать, как работает логирование в этом контексте.

Основные наблюдения:

  1. Необходимость соответствия протоколу 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)") // Теперь работает корректно.
  2. Ошибка с 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 и избегать создания оберток для логирования, чтобы использовать возможность, которую предлагает система логирования, на полную мощность.

Оцените материал
Добавить комментарий

Капча загружается...