【译】Swift记录

180 阅读9分钟

原文地址 Swift Logging

2002年,美国国会颁布了**萨班斯–奥克斯利法案,该法案针对安然MCI世通等公司的会计丑闻引入了对公司的广泛监督。该法案,即PCIHIPAA,为从网络泡沫**中崛起的新一代信息技术公司形成了监管背景。

大约在同一时间,我们看到了短暂的分布式基础设施的出现—我们现在所说的**“云计算”**—一种使系统更有能力但也更复杂的范例。

为了解决21世纪的监管和物流挑战,我们的领域围绕应用程序日志建立了最佳实践。许多相同的工具和标准今天仍然在使用。

本周在NS Hipster上,我们’来看看Swift Log:一个社区驱动的、开源的Swift登录标准。

由Swift on **Server社区开发并得到SSWG(Swift Server工作组)的认可,它的好处’仅限于在服务器**上使用。事实上,任何打算从命令行运行的Swift代码都将受益于采用Swift Log。请继续阅读以了解如何使用。

和往常一样,一个例子将有助于指导我们的讨论。本着透明和怀旧的精神,让我们想象编写一个Swift程序,审计一家’00年代财富500强公司的财务状况。

import Foundationstruct Auditor {    func watch(_ directory: URL) throws { … }    func cleanup() { … }}do {    let auditor = Auditor()    defer { auditor.cleanup() }    try auditor.watch(directory: URL(string: "ftp://…/reports")!,                     extensions: ["xls", "ods", "qdf"]) // poll for changes} catch {    print("error: \(error)")}

审核器类型轮询目录*(FTP服务器,因为它是2003年)的更改。每次添加、删除或更改文件时,都会对其内容进行差异审核。如果遇到任何财务问题,’使用打印功能重新记录。连接到FTP*的问题或程序可能遇到的任何其他问题也是如此—所有内容都使用打印记录。

很简单。我们可以像这样从命令行运行它:

$ swift run auditstarting up...ERROR: unable to reconnect to FTP# (try again after restarting PC under our desk)$ swift run audit+ connected to FTP server! accounting discrepancy in balance sheet ** Quicken database corruption! **^Cshutting down...

这样的程序可能在技术上是兼容的,但它留下了很大的改进空间:

  • 首先,我们的输出’没有任何与之相关的时间戳。没有办法知道一个问题是在一小时前还是上周检测到的。

  • 另一个问题是我们的输出缺乏任何连贯的结构。乍一看,没有简单的方法将程序噪声与实际问题隔离开来。

  • 最后,—,这主要是由于一个未指定的例子—不清楚如何处理这个输出。这个输出要去哪里?它是如何收集、聚合和分析的?

好消息是,所有这些问题(以及许多其他问题)都可以通过在项目中采用正式的日志基础结构来解决。

在您的Swift程序中采用Swift Log

将Swift Log添加到现有的Swift包是一件轻而易举的事情。您可以增量合并它,而无需对代码进行任何根本性的更改,并在几分钟内使其正常工作。

添加斯威夫特日志作为包依赖项

在您的Package.swift清单中,将斯威夫特日志添加为包依赖项,并将日志模块添加到目标的依赖项列表中。

// swift-tools-version:5.1import PackageDescriptionlet package = Package(    name: "Auditor2000",    products: [        .executable(name: "audit", targets: ["audit"])    ],    dependencies: [        .package(url: "https://github.com/apple/swift-log.git", from: "1.2.0"),    ],    targets: [        .target(name: "audit", dependencies: ["Logging"])    ])

创建一个共享的全局记录器

记录器提供了两个初始化器,其中较简单的一个采用单个标签参数:

let logger = Logger(label: "com.NSHipster.Auditor2000")

**在POSIX系统中,程序在三个预定义的流**上运行:

File HandleDescriptionName0stdinStandard Input1stdoutStandard Output2stderrStandard Error

默认情况下,记录器使用内置的Stream Log Handler类型将记录的消息写入标准输出(stdout)。我们可以通过使用更复杂的初始化器来重写此行为,而不是写入标准错误(stderr),该初始化器接受一个工厂参数:一个接受单个字符串参数(标签)并返回一个符合Log Handler的对象的闭包。

let logger = Logger(label: "com.NSHipster.Auditor2000",                    factory: StreamLogHandler.standardError)

用日志语句替换打印语句

将记录器声明为顶级常量可以让我们在模块中的任何地方调用它。让我们重新审视我们的示例,并用我们的新记录器对其进行修饰:

do {    let auditor = Auditor()    defer {        logger.trace("Shutting down")        auditor.cleanup()    }    logger.trace("Starting up")    try auditor.watch(directory: URL(string: "ftp://…/reports")!,                      extensions: ["xls", "ods", "qdf"]) // poll for changes} catch {    logger.critical("\(error)")}

跟踪、调试和关键方法在各自的日志级别记录消息。Swift Log定义了七个级别,从跟踪到关键按严重程度升序排列:

LevelDescription.traceAppropriate for messages that contain information only when debugging a program..debugAppropriate for messages that contain information normally of use only when debugging a program..infoAppropriate for informational messages..noticeAppropriate for conditions that are not error conditions, but that may require special handling..warningAppropriate for messages that are not error conditions, but more severe than .notice.errorAppropriate for error conditions..criticalAppropriate for critical error conditions that usually require immediate attention.

如果我们使用新的日志框架重新运行审计示例,我们可以看到日志行中清晰标记的不同严重性级别的直接好处:

$ swift run audit2020-03-26T09:40:10-0700 critical: Couldn't connect to ftp://…# (try again after plugging in loose ethernet cord)$ swift run audit2020-03-26T10:21:22-0700 warning: Discrepancy in balance sheet2020-03-26T10:21:22-0700 error: Quicken database corruption^C

除了仅仅标记消息之外,日志级别本身—不会让我们误解*—本身就有足够的好处,日志级别提供了可配置的披露级别。请注意,使用跟踪方法记录的*消息’出现在示例输出中。这是因为记录器默认只显示记录为信息级别或更高级别的消息。

您可以通过设置记录器的log Level属性来配置它。

var logger = Logger(label: "com.NSHipster.Auditor2000")logger.logLevel = .trace

进行此更改后,示例输出将如下所示:

$ swift run audit2020-03-25T09:40:00-0700 trace: Starting up2020-03-26T09:40:10-0700 critical: Couldn't connect to ftp://…2020-03-25T09:40:11-0700 trace: Shutting down# (try again after plugging in loose ethernet cord)$ swift run audit2020-03-25T09:41:00-0700 trace: Starting up2020-03-26T09:41:01-0700 debug: Connected to ftp://…/reports2020-03-26T09:41:01-0700 debug: Watching file extensions ["xls", "ods", "qdf"]2020-03-26T10:21:22-0700 warning: Discrepancy in balance sheet2020-03-26T10:21:22-0700 error: Quicken database corruption^C2020-03-26T10:30:00-0700 trace: Shutting down

一次使用多个日志处理程序

回想一下我们在原始示例中的反对意见,唯一剩下的问题是我们实际上如何处理这些日志。

根据**12 Factor App**原则:

十一.日志

[…]

一个包含12个因素的应用程序从不关心其输出流的路由或存储。它不应该尝试写入或管理日志文件。相反,每个正在运行的进程将其事件流无缓冲地写入stdout。

在分布式系统中收集、路由、索引和分析日志通常需要一组开源库和商业产品。幸运的是,这些组件中的大多数都以共享的**syslog消息流通—多亏了Ian Partridge的这**个包,Swift也可以。

也就是说,很少有工程师能够从**Splunk这样的人那里检索到这些信息并活着讲述这个故事。对于我们这些普通人来说,我们可能更喜欢威尔·利萨克的这个包,它向Slack**发送日志信息。

好消息是,我们可以同时使用两者,而不需要通过使用日志模块的另一个部分来改变消息在调用站点的日志记录方式: Multi lex Log Handler。

import struct Foundation.ProcessInfoimport Loggingimport LoggingSyslogimport LoggingSlackLoggingSystem.bootstrap { label in    let webhookURL = URL(string:        ProcessInfo.processInfo.environment["SLACK_LOGGING_WEBHOOK_URL"]!    )!    var slackHandler = SlackLogHandler(label: label, webhookURL: webhookURL)    slackHandler.logLevel = .critical    let syslogHandler = SyslogLogHandler(label: label)    return MultiplexLogHandler([        syslogHandler,        slackHandler    ])}let logger = Logger(label: "com.NSHipster.Auditor2000")

有了所有这些,我们的系统将以syslog格式将所有内容记录到标准输出(stdout),在那里可以由其他系统收集和分析。

但是这种记录方法的真正优势是它可以扩展到满足任何环境的特定需求。您的系统可以发送电子邮件、打开销售力量票或触发网络钩子来激活某些物联网设备,而不是将syslog写入stdout或Slack消息。

以下是如何通过编写自定义日志处理程序来扩展Swift Log以满足您的需求:

创建自定义日志处理程序

Log Handler协议指定了可由Logger注册为消息处理程序的类型的要求:

protocol LogHandler {    subscript(metadataKey _: String) -> Logger.Metadata.Value? { get set }    var metadata: Logger.Metadata { get set }    var logLevel: Logger.Level { get set }    func log(level: Logger.Level,             message: Logger.Message,             metadata: Logger.Metadata?,             file: String, function: String, line: UInt)}

在撰写本文的过程中,我创建了自定义**处理**程序,用于格式化GitHub Actions的日志消息,以便它们’像这样出现在GitHub的UI上:

img

如果你’有兴趣制作自己的日志处理程序,你可以通过浏览**这个项目的代码**学到很多。但是我确实想在这里指出几个兴趣点:

条件引导

在引导日志系统时,您可以定义一些如何配置的逻辑。例如,对于特定CI供应商的日志格式化程序,您可以检查环境,看看您’是在本地还是在CI上运行,并相应地进行调整。

import Loggingimport LoggingGitHubActionsimport struct Foundation.ProcessInfoLoggingSystem.bootstrap { label in    // Are we running in a GitHub Actions workflow?    if ProcessInfo.processInfo.environment["GITHUB_ACTIONS"] == "true" {        return GitHubActionsLogHandler.standardOutput(label: label)    } else {        return StreamLogHandler.standardOutput(label: label)    }}

测试自定义日志处理程序

测试比最初预期的更具挑战性。我可能遗漏了一些明显的东西,但是似乎’一种方法来创建关于写入标准输出的文本的断言。所以我做了以下事情:

首先,创建一个接受Text Output Stream参数的内部初始化器,并将其存储在私有属性中。

public struct GitHubActionsLogHandler: LogHandler {    private var outputStream: TextOutputStream    internal init(outputStream: TextOutputStream) {        self.outputStream = outputStream    }    }

然后,在测试目标中,创建一个采用Text Output Stream的类型,并将记录的消息收集到存储的属性中供以后检查。通过使用声明GitHub Actions Log Handler的模块的@teable**导入**,我们可以从之前访问该内部初始化器,并传递Mock Text Output Stream的实例来拦截记录的消息。

import Logging@testable import LoggingGitHubActionsfinal class MockTextOutputStream: TextOutputStream {    public private(set) var lines: [String] = []    public init(_ body: (Logger) -> Void) {        let logger = Logger(label: #file) { label in            GitHubActionsLogHandler(outputStream: self)        }        body(logger)    }    // MARK: - TextOutputStream    func write(_ string: String) {        lines.append(string)    }}

有了这些部分,我们终于可以测试我们的处理程序是否像预期的那样工作了:

func testLogging() {    var logLevel: Logger.Level?    let expectation = MockTextOutputStream { logger in        logLevel = logger.handler.logLevel        logger.trace("🥱")        logger.error("😱")    }    XCTAssertGreaterThan(logLevel!, .trace)    XCTAssertEqual(expectation.lines.count, 1) // trace log is ignored    XCTAssertTrue(expectation.lines[0].hasPrefix("::error "))    XCTAssertTrue(expectation.lines[0].hasSuffix("::😱"))}