可记录 (Loggable)


Swift 宏,用于消除函数日志记录中的样板代码。允许在任何类 (Class)、Actor、结构体 (Struct) 或枚举 (Enum) 中进行函数日志记录,并支持每个函数的独立日志记录,同时允许您忽略特定的函数。该宏可以处理静态、抛出 (throwing)、异步 (async) 和泛型 (generic) 函数,以及标准参数、inout 参数、闭包和 @autoclosures。此外,它不将您绑定到任何底层日志记录机制,因此您可以轻松地实现自己的逻辑。


动机

与任何其他宏一样,我的主要动机是消除函数日志记录中涉及的样板代码,尤其是那些存在于由古老的程序员以单例模式编写的遗留模块中的函数。此外,我不想将任何人限制在我选择的日志记录机制中,因此 Loggable 的设计允许您实现最适合您项目的日志记录机制,无论是 Sentry、swift-log 还是任何其他日志记录库。


用法

有三个宏:@Logged@Log@Omit@Logged@Log 都允许您指定一个继承自 Loggable 的类来提供您自己的实现,而 @Omit 用于禁用该函数的日志记录。

例如,@Logged 可以附加到类。应用后,它会自动用 @Log 注释该类中的每个函数。 @Logged 宏还允许您指定在调用函数时应调用哪个底层日志记录逻辑。 默认情况下,它使用 default 参数,该参数使用 os_log 进行日志记录。 您可以通过继承 Loggable 类并向 @Logged 宏提供自定义参数来覆盖此行为,例如

@Logged(using: .custom)

在此示例中,.custom 将是 Loggable 类的扩展。

@Logged 提供自定义参数会自动将其传播到其范围内的每个函数。 如果您想从日志记录中排除特定函数,只需使用 @Omit 注释它。 相反,如果您需要该范围内函数的不同日志记录机制,或者只想记录单个函数,则可以使用 @Log 注释该函数,这也允许您指定所需的日志记录机制。

目前,有三种方法可以覆盖

open func log(at location: String, of declaration: String)

当函数既不返回值也不抛出错误时,将调用此方法。

open func log(at location: String, of declaration: String, error: any Error)

当使用 throw 关键字标记函数时,无论它是否返回值,都将调用此方法。

open func log(at location: String, of declaration: String, result: Any)

最后,当函数指定返回值时,将调用此方法。


示例

日志记录很简单,只需使用 @Logged 注释类型,它会自动将 @Log 注释添加到内部的每个函数。

@Logged
struct Foo { 
  // ...
}

如果您不想记录位于用 @Logged 注释的范围内的函数,请使用 @Omit 标记它,如下所示,在宏扩展时它将被忽略。

@Logged
struct Foo { 
  func bar() { 
    // ...
  }

  @Omit
  func baz() {
    // ... 
  }
}

当您有一个独立的函数或希望仅记录特定的函数时,请使用 @Log 宏,例如

extension Foo {
  @Log
  static func bar() { 
    // ...
  }
}

要实现自定义的日志记录机制,首先要继承 Loggable 类并提供您的实现。

class Custom: Loggable, @unchecked Sendable {
  override func log(at location: String, of declaration: String) {
    // Handle 
  }
  
  override func log(at location: String, of declaration: String, error: any Error) {
    // Handle
  }
  
  override func log(at location: String, of declaration: String, result: Any) {
    // Handle
  }
}

然后,为了使用更友好的语法,创建一个 Loggable 的扩展

extension Loggable {
  static let custom: Loggable = Custom()
}

⚠️请注意,custom 被隐式指定为 Loggable。如果没有这个隐式规范,使用 @Logged(using: .custom) 会触发编译器错误,即使它在 @Log(using: .custom) 中可以正常工作。我还不太确定为什么会发生这种情况,但我会尽量解决这个问题。

之后,您就可以开始了。 简单地说,如上所述,注释所需的类、结构体或函数,并将 custom 实现作为参数传递,例如

@Logged(using: .custom)
struct Foo {
  func someVoidFunction() {
    // ...
  }

  func someThrowingFunction() throws -> String {
    // ...
  }
}

如果您需要在用 @Logged 注释的类型中使用不同的记录器,只需使用 @Log 注释特定函数并提供不同的参数即可。

@Logged(using: .custom)
struct Foo {
  // ...

  @Log(using: .different)
  func someSpecialFunction() async throws {
    // ...
  }
}

未来方向

我不太确定将 Loggable 用作类是否是最好的解决方案,但它确实允许使用更好的语法,例如 @Log(using: .custom),而不是像使用协议那样需要指定隐式类型。此外,虽然我不完全确定这种方法有多稳固,但默认实现可以被覆盖,或者更准确地说,可以被阴影化

extension Loggable {
  static let `default` = Custom()
}

这种方法允许我们交换 @Log@Logged 使用的默认逻辑,而无需传递参数 - 在这种情况下,我们不需要隐式地将 Loggable 指定为类型。 此外,我想为传递给函数的参数添加日志记录。 但是,我预见到潜在的问题 - 例如,当传递一个闭包时,需要首先计算该闭包。 如果您有任何建议,请告诉我 😉