Willow
Willow 是一个由 Swift 编写的强大且轻量级的日志库。
功能
- 默认日志级别
- 自定义日志级别
- 使用闭包的简单日志函数
- 可配置的同步或异步执行
- 线程安全的日志输出(无日志破坏)
- 通过依赖注入的自定义写入器
- 通过依赖注入在每个写入器中自定义修饰符
- 支持多个同时写入
- 框架间的共享日志记录器
- 多个日志记录器间的共享锁或队列
- 全面的单元测试覆盖率
- 完整文档
需求
- iOS 9.0+ / Mac OS X 10.11+ / tvOS 9.0+ / watchOS 2.0+
- Xcode 9.3+
- Swift 4.1+
迁移指南
沟通
- 需要帮助?创建一个 issue。
- 有功能请求?创建一个 issue。
- 找到一个 bug?创建一个 issue。
- 想要贡献?Fork 仓库并提出 pull request。
安装
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 Package Manager
Swift Package Manager是一个用于自动分发Swift代码的工具,集成到了swift
编译器中。它还处于初期开发阶段,但Willow支持在支持的平台上使用它。
设置好Swift包配置后,将Willow作为依赖添加到Package.swift
中的dependencies
值即可。
dependencies: [
.package(url: "https://github.com/Nike-Inc/Willow.git", majorVersion: 5)
]
用法
创建日志记录器
import Willow
let defaultLogger = Logger(logLevels: [.all], writers: [ConsoleWriter()])
Logger
初始化器接受三个参数来定制日志记录器实例的行为。
-
logLevels: [LogLevel]
- 应该处理的日志消息级别。与当前日志级别不匹配的消息不会处理。 -
writers: [LogWriter]
- 要写入的编写器数组。编写器可以用于将输出记录到特定目的地,例如控制台、文件或外部服务。 -
executionMethod: ExecutionMethod = .synchronous(lock: NSRecursiveLock())
- 写入消息时使用的执行方法。
Logger
对象只能在初始化期间进行定制。如果需要在运行时更改 Logger
,建议创建一个具有自定义配置的额外日志记录器来满足您的需求。同时运行许多不同的 Logger
实例是完全可接受的。
线程安全
print
函数无法保证 String
参数将被完全记录到控制台。如果有两个来自两个不同队列(线程)的 print
调用同时发生,则消息可能会变得混乱或交织。Willow
保证一条消息在开始下一条之前将被完全写完。
请注意,通过创建多个
Logger
实例,您可能会丢失线程安全日志记录的保证。如果您想使用多个Logger
实例,应创建一个共享于两个配置的NSRecursiveLock
或DispatchQueue
。有关更多信息,请参阅高级用法部分。
日志记录消息和字符串消息
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接受自动闭包,第二组API接受闭包。
请随意使用您喜欢的语法来完成您的项目。另外,默认情况下,只有闭包返回的
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使得同步或异步记录消息变得很容易。您可以在创建LoggerConfiguration
时为您的Logger
实例定义此行为。
let queue = DispatchQueue(label: "serial.queue", qos: .utility)
let log = Logger(logLevels: [.all], writers: [ConsoleWriter()], executionMethod: .asynchronous(queue: queue))
同步日志
当您开发应用程序或库时,同步日志非常有用。日志操作会在执行下一行代码之前完成。这在使用调试器时非常有用。缺点是如果日志记录在主线程上,这可能会严重影响性能。
异步日志
应使用异步日志记录您的应用程序或库的部署构建。这会将日志操作卸载到不会影响主线程性能的单独的调度队列。这允许您以Logger
配置的方式捕获日志,但不会影响主线程操作的性能。
这些都是对一种方法相对于另一种方法的典型使用用例的大致描述。在做出最终决定之前,您应该真正详细地分析您的用例。
日志写入器
将日志消息写入各种位置是任何强大日志库的基本功能。这通过LogWriter
协议在Willow
中实现。
public protocol LogWriter {
func writeMessage(_ message: String, logLevel: LogLevel)
func writeMessage(_ message: Message, logLevel: LogLevel)
}
再次强调,这是一个非常轻量级的设计,以获得最大的灵活性。只要你的 LogWriter
类符合要求,你就可以对这些日志消息做任何你想要的事情。你可以将消息写出到控制台、追加到文件、发送到服务器等。以下是向控制台写入的一个简单示例。
open class ConsoleWriter: LogMessageWriter {
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。它接收消息和日志级别,并返回一个新的格式化的 String
。这几乎是最灵活的方式了。
作为一个额外的便利层,打算输出字符串(例如写入控制台、文件等)的作家可以遵守 LogModifierWriter
协议。 LogModifierWriter
协议会给 LogWriter
添加一个 LogModifier
对象数组,可以在使用 modifyMessage(_:logLevel)
API 在扩展中输出之前将其应用于消息。
让我们通过一个简单的例子来看看如何为 debug
和 info
日志级别向日志记录器添加前缀。
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
中的顺序执行修饰符。在先前的示例中,如果TimestampModifier
在EmojiModifier
之前插入,Willow 会对日志记录的消息产生很大的差异。
OS日志
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!")
多个写入器
那么同时在一个文件和控制台日志,没有问题。你可以将多个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
对象还可以根据特定的日志级别选择性地运行要运行的修饰符。所有示例都运行所有修饰符,但如果你愿意,也可以有选择性。
高级使用
创建自定义日志级别
根据情况,可能会出现需要支持更多日志级别的需求。Willow可以通过位掩码技术轻松支持更多日志级别。[位掩码技术](http://en.wikipedia.org/wiki/Mask_%28computing%29)允许Willow在每个单例Logger
中同时支持多达32个日志级别。由于有7个默认日志级别,每个单例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
并与之共享多个框架的实例可以非常有优势,尤其是在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
和它们自己的配置。您真正想要共享的是在它们上面运行的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应用中每个日志级别的心理分析(当然,这取决于你的用例)。
debug
- 关于上下文的详细信息info
- 关于上下文的摘要信息event
- 用户驱动的交互,例如按钮点击、视图转换、选择单元格warn
- 发生了错误,但可以恢复error
- 发生了不可恢复的错误
我应该何时使用Willow?
如果你是开始使用Swift的新iOS项目,并想利用语言的新约定和功能,Willow将是一个不错的选择。如果你仍在使用Objective-C,一个纯Objective-C库(例如CocoaLumberjack)可能更合适。
Willow这个名字的由来是什么?
Willow这个名字来源于柳树。
许可协议
Willow遵循MIT许可协议。详细信息请参阅LICENSE文件。