SwiftLog
Swift的日志API包。版本1.0.0
需要Swift 5.0,但有一个0.x.y
系列版本可用于Swift 4,以简化过渡到Swift 5的过程。如果您打算为Swift 4使用或支持SwiftLog,请查阅文档末尾的段落。
首先,这是一个社区主导的开源项目,积极寻求贡献,无论是代码、文档还是想法。除了为SwiftLog
本身做出贡献外,目前还有一个巨大的差距:SwiftLog
是一个API包,试图为生态系统建立通用的API。为了让日志真正适应现实工作负载,我们需要兼容SwiftLog
的日志后端,这些后端可以将日志消息持久化到文件中、在终端中用更漂亮的颜色呈现,或者将它们发送到Splunk或ELK。
SwiftLog
目前提供的内容可以在API文档中找到。
入门
如果您有一个服务器端Swift应用程序,或可能是一个跨平台(例如Linux & macOS)应用程序/库,并且您想要记录日志,我们认为以这个日志API包为目标是一个很好的主意。下面是您开始所需的所有信息。
添加依赖项
SwiftLog
是为Swift 5设计的,版本1.0.0
需要Swift 5(但是我们将很快标记一个0.x
版本,它将在过渡期间与Swift 4一起使用)。要依赖于日志API包,您需要在Package.swift
中声明您的依赖项。
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
并将《Logging》添加到你的应用程序/库的目标的dependencies
中,例如像这样
.target(name: "BestExampleApp", dependencies: ["Logging"]),
让我们记录日志
// 1) let's import the logging API package
import Logging
// 2) we need to create a logger, the label works similarly to a DispatchQueue label
let logger = Logger(label: "com.example.BestExampleApp.main")
// 3) we're now ready to use it
logger.info("Hello World!")
输出
2019-03-13T15:46:38+0000 info: Hello World!
Logger
行为
默认SwiftLog
通过StreamLogHandler
提供基本的控制台日志输出。可以将默认输出更改为stderr
,如下所示:
LoggingSystem.bootstrap(StreamLogHandler.standardError)
StreamLogHandler
主要是一个便利工具,并不提供任何实质性定制。希望构建自己的日志后端以便集成和使用的库维护者应直接实现“在日志后端的实现”部分中概述的LogHandler
协议。
有关更多信息,请查阅API 文档。
选择日志后端实现(仅适用于应用程序)
由于API刚刚发布,目前尚无太多实现。如果你有兴趣实现一个,请查阅下面的“实现注意事项”部分,该部分解释了如何进行实现。现有与SwiftLog API兼容的库列表
存储库 | 处理器描述 |
---|---|
IBM-Swift/HeliumLogger | 在Kitura生态系统中被广泛使用的日志后端 |
ianpartridge/swift-log-syslog | 一个Syslog后端 |
Adorkable/swift-log-format-and-pipe | 允许自定义输出格式和目标的后端 |
chrisaljoudi/swift-log-oslog | 这是一个适用于苹果平台的OSLog统一日志后端统一日志。请注意:我们建议直接使用os_log,具体请参考此处。通过swift-log使用此后端调用os_log将降低效率,并阻止设置消息的隐私级别。后端始终使用%{public}@ 作为格式字符串,并将所有字符串插值尽快转换为字符串。这有两个缺点:1. 字符串插值的静态成分将被统一日志系统热心复制,这会导致性能下降。2. 它使得所有消息都公开,这改变了os_log的默认隐私策略,也不允许指定消息某些部分的细粒度隐私。在另一起正在进行的工程中,os_log的Swift API正在得到改进,并与swift-log API紧密对齐。参考资料:统一日志级别,使os_log接受使用编译时解释的字符串插值。 |
Brainfinance/StackdriverLogging | 用于Google Cloud Platform的,具备Stackdriver日志代理的JSON日志后端。 |
vapor/console-kit | 将日志消息以具有风格的(ANSI)输出打印到终端。 |
neallester/swift-log-testing | 提供访问日志消息的接口,用于断言(在测试目标中使用)。 |
wlisac/swift-log-slack | 将关键日志消息发送到Slack的日志后端。 |
NSHipster/swift-log-github-actions | 将日志消息转换为GitHub Actions工作流程命令的日志后端。 |
stevapple/swift-log-telegram | 将日志消息发送到任何Telegram聊天(受https://github.com/wlisac/swift-log-slack的影响并分叉开发)。 |
jagreenwood/swift-log-datadog | 将日志消息发送到Datadog日志管理服务的日志后端。 |
您的库? | 取得联系! |
什么是API包?
很高兴你问了。我们相信对于Swift on Server生态来说,一个可以被任何人采用的日志API至关重要,所以不同的库可以记录到共享的目标。更具体地说,这意味着我们相信来自所有库的所有日志消息最终都会集中在一个文件、数据库、Elastic Stack/Splunk实例,或者任何其他你可以选择的地方。
然而,在实际生活中,关于日志系统应该如何工作、日志消息应该如何格式化、应该在什么位置和以何种方式保留等方面有很多不同的意见。我们认为不可能等待一个日志包支持特定部署所需的所有功能,同时仍然足够易于使用并保持性能。这就是我们决定将问题分为两部分的原因
- 日志API
- 日志后端实现
此包仅提供日志API本身,因此 SwiftLog
是一个 '日志API包'。使用 LoggingSystem.bootstrap
可以配置 SwiftLog
来选择任何兼容的日志后端实现。这样,包可以实现该API,而 应用程序 可以选择任何兼容的日志后端实现,而无需对任何库进行任何修改。
仅为了完整性:实际上,此API包确实包含了一个过于简化的、不可配置的日志后端实现,它简单地将所有日志消息写入 stdout
。包含这个过于简化的日志后端实现的原因是为了改善首次使用体验。假设你开始一个项目,并第一次尝试使用 SwiftLog
,看到你记录的内容以简化格式出现在 stdout
上会比什么都没发生要好得多。对于任何实际应用,我们建议配置另一個日志后端实现,以记录你喜欢的格式。
核心概念
记录器
Logger
用于发射日志消息,因此在 SwiftLog
中是最重要的类型,因此其使用应尽可能简单。最常见的是,它们用于以特定的日志级别发射日志消息。例如
// logging an informational message
logger.info("Hello World!")
// ouch, something went wrong
logger.error("Houston, we have a problem: \(problem)")
日志级别
以下日志级别被支持
跟踪
调试
信息
通知
警告
错误
危急
给定记录器的日志级别可以更改,但这种更改只会影响您更改的具体记录器。可以说,从日志级别的角度看,Logger
是一个 值类型。
日志元数据
日志元数据是可以附加到记录器上的元数据,它包含在调试问题时至关重要的信息。在服务器中,一个常见的例子是将请求的 UUID 附加到记录器上,然后该 UUID 将出现在所有使用该记录器记录日志的消息中。示例
var logger = logger
logger[metadataKey: "request-uuid"] = "\(UUID())"
logger.info("hello world")
将打印
2019-03-13T18:30:02+0000 info: request-uuid=F8633013-3DD8-481C-9256-B296E43443ED hello world
使用与 SwiftLog
一起提供的默认日志后端实现。不用说,格式完全由您选择的日志后端定义。
LogHandler
)的实现
关于日志后端(注意:如果您不想实现自定义日志后端,本节中的所有内容可能都不是非常相关,因此请随意跳过。
要成为所有 SwiftLog
消费者都可以使用的兼容日志后端,您需要做两件事:1) 实现一个类型(通常是 struct
),该类型实现了 LogHandler
协议,这是 SwiftLog
提供的协议;2) 指示 SwiftLog
使用您的日志后端实现。
以下协议是一切 LogHandler
或日志后端实现应遵守的
public protocol LogHandler {
func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, file: String, function: String, line: UInt)
subscript(metadataKey _: String) -> Logger.Metadata.Value? { get set }
var metadata: Logger.Metadata { get set }
var logLevel: Logger.Level { get set }
}
指示 SwiftLog
使用您的日志后端作为整个应用程序(包括所有库)应该使用的后端非常简单
LoggingSystem.bootstrap(MyLogHandler.init)
实施注意事项
LogHandler
控制日志系统的绝大部分
LogHandler
控制下
在 配置
LogHandler
控制两个关键的 Logger
配置元素,即
- 日志级别(
logger.logLevel
属性) - 日志元数据(《logger[metadataKey:]》和《logger.metadata》)
但是,为了使系统正常工作,重要的是 LogHandler
将配置视为 值类型。这意味着 LogHandler
应该是 struct
,日志级别或日志元数据的更改只能影响已更改的 LogHandler
。
然而,在特殊情况下,一个 LogHandler
提供一些全局日志级别覆盖,可能会影响所有创建的 LogHandler
是可以接受的。
发射
- 发射日志消息本身
LogHandler
控制之下
不在LogHandler
不会控制是否应该记录消息。《Logger》仅当根据配置的日志级别确定应发出日志消息时,才会调用LogHandler
的log
函数。
来源与标签
《Logger》携带一个(不可变的)label
,每个日志消息携带一个source
参数(自 SwiftLog 1.3.0 起使用)。《Logger》的标签标识了《Logger》的创建者。如果您通过保留跨多个模块的元数据进行结构化日志记录,则《Logger》的label
不是一种良好的方式来识别日志消息的来源,因为它标识了《Logger》的创建者,而《Logger》通常在库之间传递以保留元数据和类似信息。
如果您想过滤所有来自某个子系统的日志消息,请根据source
进行过滤,默认值为发出日志消息的模块。
SwiftLog for Swift 4
首先,SwiftLog 1.0.x 和 SwiftLog 0.0.x 大部分兼容,所以不必担心。实际上,SwiftLog 0.0.0 与 SwiftLog 1.0.0 使用相同的源代码,只做了少量更改以保证与 Swift 4 兼容。
如何使用SwiftLog 0库或应用程序?
如果您有一个需要同时兼容Swift 4和5的应用程序或库,我们建议在您的Package.swift
中使用以下配置
.package(url: "https://github.com/apple/swift-log.git", Version("0.0.0") ..< Version("2.0.0")),
这将指导SwiftPM允许使用任何SwiftLog 0版本和任何SwiftLog 1版本。这种形式很特殊,因为通常包不支持多个主版本。但是由于SwiftLog 0和1的兼容性较好,这不应该成为问题,并且将使所有人都能获得最佳体验。如果使用Swift 4编译器编译,这将生成SwiftLog 0版本,但如果使用Swift 5编译器,所有人都能得到SwiftLog 1带来的最佳体验和性能。
在大多数情况下,您只需记住一件事:在日志方法中始终使用字符串字面量和字符串插值,不要依赖SwiftLog 0允许String
的事实。
良好
logger.info("hello world")
不良
let message = "hello world"
logger.info(message)
如果您从某个地方接收了一个String
,请使用
logger.info("\(stringIAlreadyHave)")
有关更多详细信息,请参阅下一节。
SwiftLog 1和0之间的区别是什么?
- SwiftLog 0没有使用
@inlinable
。 - 除了接受
Logger.Message
作为消息外,SwiftLog 0还有String
重载。 - 在SwiftLog 0中,
Logger.Message
不是ExpressibleByStringLiteral
或ExpressibleByStringInterpolation
。 - 在SwiftLog 0中,
Logger.MetadataValue
不是ExpressibleByStringLiteral
或ExpressibleByStringInterpolation
。
为什么有这些区别?
@inlinable
Swift 4.0 & 4.1不支持@inlinable
,因此SwiftLog 0不能使用它们。
日志.Message
由于所有 Swift 4 版本都没有(非已弃用)机制允许类型实现 ExpressibleByStringInterpolation
,所以我们无法使 Logger.Message
通过字符串字面量来表达。不幸的是,我们日志 API 的最基本形式是 logger.info("Hello \(world)")
。然而,为了让这个表达式生效,需要接受 "Hello \(world)"
并因为无法使 Logger.Message
实现 ExpressibleByStringInterpolation
,我们为所有日志方法添加了重载,使其也能接受 String
类型的参数。在大多数情况下,您甚至都不会注意到在使用 SwiftLog 0 时创建了一个 String
(然后转换成 Logger.Message
),而在 SwiftLog 1 中则是直接创建了一个 Logger.Message
。这是因为 String
和 Logger.Message
都能接受所有形式的字符串字面量和字符串插值。不幸的是,有些代码会使得这个看似微小的差异变得明显。如果您写
let message = "Hello world"
logger.info(message)
则这仅能在 SwiftLog 0 中工作,而不能在 SwiftLog 1 中工作。为什么?因为 SwiftLog 1 希望得到一个 Logger.Message
,而 let message = "Hello world"
会使得 message
成为 String
类型,而在 SwiftLog 1 中,日志方法不接受 String
。
因此,如果您希望在 SwiftLog 0 和 1 中同时保持兼容,请务必在日志方法中始终使用 字符串字面量 或 字符串插值。
如果您已经有一个想记录的 String
类型的字符串备用,请不要担心,只需使用
let message = "Hello world"
logger.info("\(message)")
再次,您在使用 SwiftLog 0 和 1 时将没有问题。
设计
这个日志 API 与 Swift on Server 社区的贡献者共同设计,并由 SSWG(Swift 服务器工作组) 批准,纳入 SSWG 的 孵化流程 的 '沙盒' 级别。