Willow

Build Status CocoaPods Compatible Carthage Compatible Platform

Willow 是一个强大且轻量级的日志记录库,使用 Swift 编写。

特性

需求

迁移指南

交流

安装

CocoaPods

CocoaPods 是 Cocoa 项目的依赖管理器。您可以使用以下命令安装它

[sudo] gem install cocoapods

需要 CocoaPods 1.3+。

要将 Willow 集成到您的项目中,请在您的 Podfile 中指定它

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!

pod 'Willow', '~> 5.0'

然后,运行以下命令

$ pod install

Carthage

Carthage 是一个去中心化的依赖管理器,它可以构建您的依赖项并为您提供二进制框架。

您可以使用 Homebrew 通过以下命令安装 Carthage

$ brew update
$ brew install carthage

要使用 Carthage 将 Willow 集成到您的 Xcode 项目中,请在您的 Cartfile 中指定它

github "Nike-Inc/Willow" ~> 5.0

运行 carthage update 以构建框架并将构建的 Willow.framework 拖到您的 Xcode 项目中。

Swift 包管理器

Swift 包管理器是一种用于自动分发 Swift 代码的工具,并已集成到 swift 编译器中。它还处于早期开发阶段,但 Willow 确实支持在支持的平台上使用它。

设置好 Swift 包后,添加 Willow 作为依赖项就像将其添加到 Package.swiftdependencies 值一样简单。

dependencies: [
    .package(url: "https://github.com/Nike-Inc/Willow.git", majorVersion: 5)
]

用法

创建 Logger

import Willow

let defaultLogger = Logger(logLevels: [.all], writers: [ConsoleWriter()])

Logger 初始化程序接受三个参数来定制 logger 实例的行为。

Logger 对象只能在初始化期间进行自定义。 如果需要在运行时更改 Logger,建议创建另一个具有自定义配置的 logger 以满足您的需求。 同时运行许多不同的 Logger 实例是完全可以接受的。

线程安全

print 函数不保证 String 参数将被完全记录到控制台。如果两个 print 调用同时从两个不同的队列(线程)发生,则消息可能会变得混乱或交织在一起。 Willow 保证在开始下一个消息之前完全完成消息的写入。

重要的是要注意,通过创建多个 Logger 实例,您可能会失去线程安全日志记录的保证。 如果要使用多个 Logger 实例,则应创建在两个配置之间共享的 NSRecursiveLockDispatchQueue。 有关更多信息,请参见高级用法部分。

记录消息和字符串消息

Willow 可以记录两种不同类型的对象:消息和字符串。

日志消息

消息是具有名称和属性字典的结构化数据。 Willow 声明了 LogMessage 协议,框架和应用程序可以将其用作具体实现的基础。 如果您想提供上下文信息以及日志文本(例如,将路由日志信息到 New Relic 之类的外部系统),则消息是一个不错的选择。

enum Message: LogMessage {
    case requestStarted(url: URL)
    case requestCompleted(url: URL, response: HTTPURLResponse)

    var name: String {
        switch self {
        case .requestStarted:   return "Request started"
        case .requestCompleted: return "Request completed"
        }
    }

    var attributes: [String: Any] {
        switch self {
        case let .requestStarted(url):
            return ["url": url]

        case let .requestCompleted(url, response):
            return ["url": url, "response_code": response.statusCode]
        }
    }
}

let url = URL(string: "https://httpbin.org/get")!

log.debug(Message.requestStarted(url: url))
log.info(Message.requestStarted(url: url))
log.event(Message.requestStarted(url: url))
log.warn(Message.requestStarted(url: url))
log.error(Message.requestStarted(url: url))

日志消息字符串

日志消息字符串只是没有其他数据的 String 实例。

let url = URL(string: "https://httpbin.org/get")!

log.debugMessage("Request Started: \(url)")
log.infoMessage("Request Started: \(url)")
log.eventMessage("Request Started: \(url)")
log.warnMessage("Request Started: \(url)")
log.errorMessage("Request Started: \(url)")

日志消息字符串 API 的末尾带有 Message 后缀,以避免与日志消息 API 产生歧义。如果没有后缀,多行转义闭包 API 会发生冲突。

使用闭包记录消息

Willow 的日志记录语法经过优化,可以使日志记录尽可能轻巧且易于记忆。开发人员应该能够专注于手头的任务,而不必记住如何编写日志消息。

单行闭包

let log = Logger()

// Option 1
log.debugMessage("Debug Message")    // Debug Message
log.infoMessage("Info Message")      // Info Message
log.eventMessage("Event Message")    // Event Message
log.warnMessage("Warn Message")      // Warn Message
log.errorMessage("Error Message")    // Error Message

// or

// Option 2
log.debugMessage { "Debug Message" } // Debug Message
log.infoMessage { "Info Message" }   // Info Message
log.eventMessage { "Event Message" } // Event Message
log.warnMessage { "Warn Message" }   // Warn Message
log.errorMessage { "Error Message" } // Error Message

这两种方法是等效的。 第一组 API 接受 autoclosures,第二组接受 closures。

可以随意为您的项目使用您喜欢的任何语法。 另外,默认情况下,只会记录闭包返回的 String。 有关自定义日志消息格式的更多信息,请参见日志修饰器部分。

两组 API 都使用闭包来提取日志消息的原因是为了提高性能。

在设计日志记录解决方案时,有一些非常重要的性能注意事项,在闭包性能部分中将对此进行更详细的描述。

多行闭包

记录消息很容易,但是知道何时添加必要的逻辑来构建日志消息并针对性能进行调整可能有点棘手。 我们要确保逻辑被封装并且非常高性能。 Willow 日志级别闭包使您可以干净地包装所有逻辑以构建消息。

log.debugMessage {
    // First let's run a giant for loop to collect some info
    // Now let's scan through the results to get some aggregate values
    // Now I need to format the data
    return "Computed Data Value: \(dataValue)"
}

log.infoMessage {
    let countriesString = ",".join(countriesArray)
    return "Countries: \(countriesString)"
}

与单行闭包不同,多行闭包需要 return 声明。

闭包性能

Willow 专门与日志记录闭包一起使用,以确保在所有情况下都能获得最佳性能。 闭包会将闭包内部所有逻辑的执行延迟到绝对必要时,包括字符串评估本身。 在 Logger 实例被禁用的情况下,与采用 String 参数的传统日志消息方法相比,日志执行时间减少了 97%。 此外,创建闭包的开销经测量比传统方法高 1%,因此可以忽略不计。 总而言之,闭包使 Willow 在所有情况下都具有极高的性能。

禁用 Logger

Logger 类具有一个 enabled 属性,使您可以完全禁用日志记录。 这对于在应用程序级别关闭特定的 Logger 对象很有帮助,或者更常见的是在第三方库中禁用日志记录。

let log = Logger()
log.enabled = false

// No log messages will get sent to the registered Writers

log.enabled = true

// We're back in business...

同步和异步日志记录

日志记录会极大地影响您的应用程序或库的运行时性能。 Willow 使您可以非常轻松地同步或异步记录消息。 您可以在为 Logger 实例创建 LoggerConfiguration 时定义此行为。

let queue = DispatchQueue(label: "serial.queue", qos: .utility)
let log = Logger(logLevels: [.all], writers: [ConsoleWriter()], executionMethod: .asynchronous(queue: queue))

同步日志记录

在开发应用程序或库时,同步日志记录非常有用。 日志记录操作将在执行下一行代码之前完成。 这在单步调试器时非常有用。 缺点是如果在主线程上进行日志记录,这可能会严重影响性能。

异步日志记录

异步日志记录应使用于您的应用程序或库的部署版本。 这会将日志记录操作卸载到单独的调度队列,该队列不会影响主线程的性能。 这使您仍然可以按照配置 Logger 的方式捕获日志,但不会影响主线程操作的性能。

这些是对一种方法与另一种方法的典型用例的大概概括。 在就何时使用哪种方法做出最终决定之前,您应该真正详细地分解您的用例。

日志写入器

将日志消息写入到各个位置是任何强大的日志记录库的基本功能。这在 Willow 中通过 LogWriter 协议实现。

public protocol LogWriter {
    func writeMessage(_ message: String, logLevel: LogLevel)
    func writeMessage(_ message: Message, logLevel: LogLevel)
}

同样,这是一个非常轻量级的设计,可提供最大的灵活性。 只要您的 LogWriter 类符合要求,您就可以使用这些日志消息做任何您想做的事情。 您可以将消息写入控制台,将其附加到文件,将其发送到服务器等。这是一个快速查看写入控制台的简单写入。

open class ConsoleWriter: LogWriter {
    open func writeMessage(_ message: String, logLevel: LogLevel) {
        print(message)
    }

    open func writeMessage(_ message: LogMessage, logLevel: LogLevel) {
        let message = "\(message.name): \(message.attributes)"
        print(message)
    }
}

日志修饰器

日志消息自定义是 Willow 擅长的。 一些开发人员希望在其库输出中添加前缀,一些人想要不同的时间戳格式,甚至有些人想要表情符号! 无法预测团队要使用的所有类型的自定义格式。 这就是 LogModifier 对象出现的地方。

public protocol LogModifier {
    func modifyMessage(_ message: String, with logLevel: LogLevel) -> String
}

LogModifier 协议只有一个 API。 它接收 messagelogLevel 并返回一个新格式化的 String。 这是您能获得的最大灵活性。

为了增加便利性,打算输出字符串(例如,写入控制台,文件等)的写入器可以遵守 LogModifierWritier 协议。 LogModifierWriter 协议将 LogWriterLogModifier 对象数组添加到可以使用扩展中的 modifyMessage(_:logLevel) API 在输出之前应用于消息。

让我们来看一个简单的示例,以将前缀添加到 debuginfo 日志级别的记录器。

class PrefixModifier: LogModifier {
    func modifyMessage(_ message: String, with logLevel: Logger.LogLevel) -> String {
        return "[Willow] \(message)"
    }
}

let prefixModifiers = [PrefixModifier()]
let writers = [ConsoleWriter(modifiers: prefixModifiers)]
let log = Logger(logLevels: [.debug, .info], writers: writers)

为了将修饰符一致地应用于字符串,LogModifierWriter 对象应调用 modifyMessage(_:logLevel) 以创建一个基于原始字符串的新字符串,并将所有修饰符按顺序应用。

open func writeMessage(_ message: String, logLevel: LogLevel) {
    let message = modifyMessage(message, logLevel: logLevel)
    print(message)
}

多个修饰符

可以将多个 LogModifier 对象堆叠到一个日志级别上以执行多个操作。 让我们来看一个结合使用 TimestampModifier(用时间戳作为消息前缀)和 EmojiModifier 的示例。

class EmojiModifier: LogModifier {
    func modifyMessage(_ message: String, with logLevel: LogLevel) -> String {
        return "🚀🚀🚀 \(message)"
    }
}

let writers: = [ConsoleWriter(modifiers: [EmojiModifier(), TimestampModifier()])]
let log = Logger(logLevels: [.all], writers: writers)

Willow 对可以应用于单个日志级别的 LogModifier 对象总数没有任何硬性限制。 请记住,性能是关键。

默认的 ConsoleWriter 将以将修饰符添加到 Array 中的相同顺序执行修饰符。 在前面的示例中,如果在 EmojiModifier 之前插入了 TimestampModifier,则 Willow 将记录一条非常不同的消息。

OSLog

OSLogWriter 类允许您在 Willow 系统中使用 os_log API。 要使用它,您只需要创建 LogModifier 实例并将其添加到 Logger 中。

let writers = [OSLogWriter(subsystem: "com.nike.willow.example", category: "testing")]
let log = Logger(logLevels: [.all], writers: writers)

log.debugMessage("Hello world...coming to your from the os_log APIs!")

多个 Writer

那么如何同时记录到文件和控制台呢?没问题。您可以将多个 LogWriter 对象传递到 Logger 初始化器中。 Logger 将按照传入的顺序执行每个 LogWriter。 例如,让我们创建一个 FileWriter 并将其与我们的 ConsoleWriter 结合起来。

public class FileWriter: LogWriter {
    public func writeMessage(_ message: String, logLevel: Logger.LogLevel, modifiers: [LogMessageModifier]?) {
	    var message = message
        modifiers?.map { message = $0.modifyMessage(message, with: logLevel) }
        // Write the formatted message to a file (We'll leave this to you!)
    }

    public func writeMessage(_ message: LogMessage, logLevel: LogLevel) {
        let message = "\(message.name): \(message.attributes)"
        // Write the formatted message to a file (We'll leave this to you!)
    }
}

let writers: [LogMessageWriter] = [FileWriter(), ConsoleWriter()]
let log = Logger(logLevels: [.all], writers: writers)

LogWriter 对象还可以有选择性地决定要为特定日志级别运行哪些 modifiers。 所有示例都运行所有 modifiers,但您可以根据需要进行选择。


高级用法

创建自定义日志级别

根据具体情况,可能需要支持其他日志级别。 Willow 可以通过位掩码轻松支持其他日志级别。 由于 LogLevel 的内部 RawValueUInt,因此 Willow 可以同时为单个 Logger 支持多达 32 个日志级别。 由于有 7 个默认日志级别,Willow 可以为单个 logger 支持多达 27 个自定义日志级别。 这应该足以处理即使是最复杂的日志记录解决方案。

创建自定义日志级别非常简单。 这是一个如何执行此操作的快速示例。 首先,您必须创建一个 LogLevel 扩展并添加您的自定义值。

extension LogLevel {
    private static var verbose = LogLevel(rawValue: 0b00000000_00000000_00000001_00000000)
}

最好将自定义日志级别的值设置为 var 而不是 let。 如果两个框架使用相同的自定义日志级别位掩码,则应用程序可以将其中一个框架重新分配给新值。

现在我们有了一个名为 verbose 的自定义日志级别,我们需要扩展 Logger 类以便能够轻松地调用它。

extension Logger {
    public func verboseMessage(_ message: @autoclosure @escaping () -> String) {
    	logMessage(message, with: .verbose)
    }

    public func verboseMessage(_ message: @escaping () -> String) {
    	logMessage(message, with: .verbose)
    }
}

最后,使用新的日志级别非常简单...

let log = Logger(logLevels: [.all], writers: [ConsoleWriter()])
log.verboseMessage("My first verbose log message!")

all 日志级别包含一个位掩码,其中所有位都设置为 1。 这意味着 all 日志级别将自动包含所有自定义日志级别。

框架间共享 Logger

定义单个 Logger 并在多个框架之间共享该实例可能非常有利,尤其是在 iOS 8 中添加了框架之后。 既然我们将在自己的应用程序中创建更多框架,以便在应用程序、扩展和第三方库之间共享,如果我们能共享 Logger 实例,岂不是很好?

让我们快速浏览一下 Math 框架与其父 Calculator 应用程序共享 Logger 的示例。

//=========== Inside Math.swift ===========
public var log: Logger?

//=========== Calculator.swift ===========
import Math

let writers: [LogMessageWriter] = [FileWriter(), ConsoleWriter()]
var log = Logger(logLevels: [.all], writers: writers)

// Set the Math.log instance to the Calculator.log to share the same Logger instance
Math.log = log

用新的 Logger 替换预先存在的 Logger 非常简单。

多个 Logger,一个队列

前面的示例展示了如何在多个框架之间共享 Logger 实例。 但更有可能的是,您希望每个第三方库或内部框架都拥有自己的 Logger 及其自己的配置。 您真正想要共享的是 NSRecursiveLock 或它们运行的 DispatchQueue。 这将确保您的所有日志记录都是线程安全的。 这是之前的示例,演示了如何创建多个 Logger 实例并仍然共享队列。

//=========== Inside Math.swift ===========
public var log: Logger?

//=========== Calculator.swift ===========
import Math

// Create a single queue to share
let sharedQueue = DispatchQueue(label: "com.math.logger", qos: .utility)

// Create the Calculator.log with multiple writers and a .Debug log level
let writers: [LogMessageWriter] = [FileWriter(), ConsoleWriter()]

var log = Logger(
    logLevels: [.all],
    writers: writers,
    executionMethod: .asynchronous(queue: sharedQueue)
)

// Replace the Math.log with a new instance with all the same configuration values except a shared queue
Math.log = Logger(
    logLevels: log.logLevels,
    writers: [ConsoleWriter()],
    executionMethod: .asynchronous(queue: sharedQueue)
)

Willow 是一个非常轻量级的库,但其灵活性使其在您希望的时候变得非常强大。


添加消息过滤器

有时您可能希望对何时包含某些日志消息进行更精细的控制。 例如,如果您想忽略具有给定属性的日志,基于您拥有的任何动态逻辑。

如果您有一种在 DEBUG/ADHOC 场景中打开/关闭应用程序中日志子系统的方法,这将非常有用。

要定义过滤器,请创建一个实现 LogFilter 协议的类型。

这是一个过滤器的示例,该过滤器可以有条件地排除分析子系统的嘈杂日志

struct AnalyticsLogFilter: LogFilter {
    let name = "analytics"
    
    func shouldInclude(_ message: LogMessage, logLevel: LogLevel) -> Bool {
        // only consider those with a given attribute
        guard message.attributes["subsystem"] == "analytics" else { return true }
        
        return logLevel != .debug
    }
    
    func shouldInclude(_ message: String, logLevel: LogLevel) -> Bool {
        // we don't have any additional context for string messages, so always include
        return true
    }
}

使用此过滤器,您现在可以有条件地将其添加到记录器

logger.addFilter(AnalyticsLogFilter())

或者以后如果您想删除它

logger.removeFilter(named: "analytics")
// or 
logger.removeFilters()

从过滤器返回 false 的消息将不会被发出。

在运行时更改日志级别

在 DEBUG 和 ADHOC 构建中,允许测试人员更改日志级别以包含原本默认情况下过于嘈杂而无法包含的消息可能是有利的。

您可以通过调用 logger.setLogLevels(...) 在运行时更改日志级别。 请注意,这里传递的是一个 OptionSet,因此您需要包含所有要包含的日志级别。

如果您使用的是默认选项集,则可以使用 .minimum 辅助方法来包含给定日志级别之上的所有级别。 例如,要包含 .info 及以上级别

logger.setLogLevels(.minimum(.info))

如果您使用自定义日志级别,则不支持此方法。

常见问题解答

为什么是 5 个默认日志级别? 为什么它们这样命名?

简单...简单和优雅。 从上下文中,如果您有过多的日志级别,则很难理解您需要哪个日志级别。 但是,这并不意味着这始终是每个人或每种用例的完美解决方案。 这就是为什么有 5 个默认日志级别,并支持轻松添加其他日志级别的原因。

至于命名,这是我们对 iOS 应用程序中每个日志级别的精神分解(显然这取决于您的用例)。

我应该什么时候使用 Willow?

如果您正在 Swift 中启动一个新的 iOS 项目,并且希望利用该语言的许多新约定和功能,那么 Willow 将是一个不错的选择。 如果您仍在 Objective-C 中工作,那么像 CocoaLumberjack 这样的纯 Objective-C 库可能更合适。

Willow 这个名字从何而来?

Willow 以唯一的一棵柳树命名。


许可证

Willow 在 MIT 许可下发布。 有关详细信息,请参见 LICENSE。

创建者