使用os_log记录函数参数或其他动态数据

15

我正在尝试像这样记录函数参数到os_log中:

func foo(x: String, y: [String:String]) {
    //...
    os_log("foo: \(x) \(y.description)", log: OSLog.default, type: .debug)
}

但是遇到错误:

无法将类型为 'String' 的值转换为预期的参数类型 'StaticString'

那么我该如何记录函数参数或任何其他动态数据呢?

6个回答

34

请参考日志记录:

格式化日志消息

要格式化日志消息,请使用标准的NSString或printf格式字符串,...

并使用字符串格式说明符来指定标准格式字符串说明符,如%@%d

在您的情况下:

os_log("foo: %@ %@", log: .default, type: .debug, x, y.description)

为了防止格式字符串指示符(format string specifiers)的意外扩展,格式字符串必须限制在静态字符串中。以下是一个示例,演示了使用NSLog()出现问题的情况,因为它不限制格式字符串只能是常量字符串:

The format string is restricted to static strings to prevent (unintentional) expansion of format string specifiers. Here is an example demonstrating the problem, using NSLog() because that does not restrict the format to constant strings:


(Note: The translated text is identical to the original except for minor formatting differences caused by Markdown syntax.)
let s = "50%"
NSLog("\(s)percent")
// Output: 500x0ercent

%p期望可变参数列表上提供一个指针变量,但实际未提供。这是未定义的行为,可能会导致崩溃或意外输出。


17

在 Xcode 12 / Swift 5.3 / iOS 14中,你无需直接调用os_log。相反,将你对 OSLog 类的使用替换为新的 Logger 类(在import os时可用)。以下是一个示例:

let myLog = Logger(subsystem: "testing", category: "exploring")

然后,您可以直接在Logger对象上调用方法,以使用该子系统和类别记录日志:

myLog.log("logging at \(#function)")

如果要记录除默认级别以外的级别,请使用该级别作为方法名称:

myLog.debug("logging at \(#function)")

在消息字符串中,正如您所看到的,Swift字符串插值是合法的。它允许使用Int、Double、具有description的Objective-C对象以及符合CustomStringConvertible协议的Swift对象。

Swift字符串插值的合法性在这里令人惊讶,因为os_log格式说明符的重点是推迟参数的评估,将其从您的应用程序中推出(以便您的应用程序不会因记录日志而变慢),并将其推入日志记录机制本身。好吧,惊喜!由于在Swift 5中引入了自定义Swift字符串插值钩子,插值确实被延迟了。

而在此处使用自定义字符串插值还有两个进一步的好处。首先,自定义字符串插值机制允许一个插值伴随着附加参数来指定它的行为。这就是你防止一个值被编辑的方式:

myLog.log("logging at \(#function, privacy: .public)")

您还可以使用其他参数执行各种字符串格式化,否则您将不得不使用NSLog格式说明符执行此操作,例如指定小数点后的数字位数以及其他类型的填充和对齐:

myLog.log("the number is \(i, format: .decimal(minDigits: 5))") // e.g. 00001
因此,您将永远不需要再直接调用os_log,也不必再使用NSLog类型的格式说明符。


iOS 13及其之前的旧答案:

在Martin R的答案基础上扩展两点:

os_log("foo: %@ %@", log: .default, type: .debug, x, y.description)

您可以省略type:参数,但不能忽略log:参数;必须包含log:标签,否则os_log会误解您的意图。

此外,log:值不一定需要是.default。通常先创建一个或多个OSLog对象,用作log:参数的参数。这样做的好处是,您可以为OSLog对象指定Subsystem和Category,从而允许您在Xcode控制台或Console应用程序中对结果进行过滤。


另外,关于pkamb的回答,如果我们知道我们的消息永远都是字符串,我们可以像这样编写OSLog扩展(利用新的Swift 5.2 callAsFunction方法):

extension OSLog {
    func callAsFunction(_ s: String) {
        os_log("%{public}s", log: self, s)
    }
}

结果是我们现在可以将我们的myLog对象本身视为一个函数:

myLog("The main view's bounds are \(self.view.bounds)")

这很好,因为它就像基本的print语句一样简单。我赞赏WWDC 2016对这种预格式化的警告,但如果这正是您在print语句中已经在做的事情,我想它并不会有太大的危害。


这非常有用。谢谢。 - thenakulchawla

4
这是我的方法:
import Foundation
import os.log

struct Log {
    enum LogLevel: String {
        case error = "⛔️"
        case warning = "⚠️"
        case debug = ""
    }

    static func debug(_ info: String, level: LogLevel = .debug, file: String = #file, function: String = #function, line: Int = #line) {
        os_log("%@ %@:%d %@: %@", type: .default, level.rawValue, (file as NSString).lastPathComponent, line, function, info)
    }

    static func warning(_ info: String, level: LogLevel = .warning, file: String = #file, function: String = #function, line: Int = #line) {
        os_log("%@ %@:%d %@: %@", type: .default, level.rawValue, (file as NSString).lastPathComponent, line, function, info)
    }

    static func error(_ error: NSError, level: LogLevel = .error, file: String = #file, function: String = #function, line: Int = #line) {
        os_log("%@ %@:%d %@: %@", type: .default, level.rawValue, (file as NSString).lastPathComponent, line, function, "\(error)")
    }
}

用法:

Log.debug("MyLog")

输出示例:

AppDelegate.swift:26 application(_:didFinishLaunchingWithOptions:):MyLog


不好的想法:苹果公司强烈反对包装 os_log(因为性能问题)。此外,在 iOS 14+ 上,Logger 允许在没有任何自定义覆盖的情况下执行此操作。 - timbre timbre
如果您在Mac日志控制台上查看您的消息,它将显示为:"<private data> <private data>:<private data> <private data>: <private data>" - timbre timbre

1
我曾因无法在os_log中使用"\(variable)"的Swift字符串插值而感到烦恼。
为了解决这个问题,我编写了一个小扩展程序:
import os.log

extension OSLog {
    
    static func log(_ message: String, log: OSLog = .default, type: OSLogType = .default) {
        os_log("%@", log: log, type: type, message)
    }
    
}

这确实会导致“私有”日志记录,这是预期的。
应用程序名称<私有> 在Console.app中,如何揭示<私有>标记实际指的是什么?
在苹果的WWDC 2016演示中"Unified Logging and Activity Tracing",他们说:

避免在其他函数中包装os log APIs。

如果您将其包装在另一个函数中,则会失去我们为您收集文件和行号的能力。

如果您确实需要包装我们的API,则请使用宏而不是函数进行包装。

因此,如果您担心额外收集的信息,则这可能不是最佳解决方案。即使使用标准的os_log,该信息仍然可能无法获得:How to find source file and line number from os_log()

如果有人想编写允许"\(variable)"替换的“宏”替代方法,则可以考虑使用。


除非Apple明确建议不要这样做:“避免在其他函数中包装os log API。如果您将其包装在另一个函数中,则会失去我们为您收集文件和行号的能力。如果您绝对必须包装我们的API,请使用宏而不是函数进行包装。”请参见https://developer.apple.com/videos/play/wwdc2016/721/。 - timbre timbre
@KirilS。这是很好的信息。我会编辑到答案中,也许有人可以提交一个宏替代方案。 - pkamb
虽然似乎从库存的 os_log 中也无法获得行号...?https://dev59.com/UZzha4cB1Zd3GeqPJr6z - pkamb
1
@pkamb 请看我对OSLog扩展想法的重新表述,链接在这里:https://dev59.com/JlQJ5IYBdhLWcg3w05ZM#62488271 - matt
@matt有没有关于如何编写“将它们用宏而不是函数包装”的扩展版本的想法? - pkamb

1
macOS 11 Big Sur发布说明中指出,现在可以通过Swift字符串插值传递os_log

https://developer.apple.com/documentation/macos-release-notes/macos-big-sur-11-beta-release-notes

日志记录

新功能

  • 现在可以使用 Swift 作为 os 框架的一部分,使用新的 API os_log 进行日志记录:

    • 使用子系统和类别可以实例化一个新类型的 Logger,并提供不同级别日志记录的方法 (debug(_:)、error(_:)、fault(_:))

    • Logger API 支持指定大多数格式化和隐私选项,这些选项受遗留的 os_log API 支持。

    • 新的 API 提供了比遗留 API 更显著的性能改进。

    • 现在您可以将 Swift 字符串插值传递给 os_log 函数。

注意:新的 API 无法进行后向兼容;但是,现有的 os_log API 可用于后向兼容。(22539144)


0
现在这个问题和接受的答案已经过时了。相反,使用支持字符串插值的Logger。使用它的方便方法是在应用程序中全局定义一个日志记录器的实例:
extension Logger {
    static let myApp = Logger(subsystem: "com.mycompany", category: "MYAPP")
}

然后将其用作
Logger.myApp.debug("foo: \(x) \(y.description)")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接